给 DeepSeek Harness 用的离线 / 在线导入导出插件:把一台机器上正在工作的 DSH 主目录打包成单个…
36.4/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-portablenpm 源 · dsh-portable 的发布者账号已校验与本仓库一致
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · other
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 2 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包dsh-portable @ 0.1.10
✓Node 引擎要求 ^22.15.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/3 07:16:20
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 75 处,展示前 62 条)
- 读取本地文件index.js:36
const raw = readFileSync(path.join(here, 'package.json'), 'utf8'); - 写入本地文件index.js:140
void mkdir(store.dirs.exports, { recursive: true }).catch(() => {}); - 写入本地文件index.js:141
void mkdir(store.dirs.uploads, { recursive: true }).catch(() => {}); - 写入剪贴板lib/client.js:306
navigator.clipboard.writeText(text), - 发起网络请求lib/client.js:351
const response = await fetch(API_PREFIX + path, init); - 读取本地文件lib/routes.mjs:130
for (const entry of await readdir(dir, { withFileTypes: true })) { - 读取本地文件lib/routes.mjs:154
names = await readdir(dir); - 读取本地文件lib/routes.mjs:165
info = await stat(filePath); - 读取本地文件lib/routes.mjs:205
const entries = await readdir(profilesDir, { withFileTypes: true }); - 读取本地文件lib/routes.mjs:270
const info = await stat(target.filePath).catch(() => null); - 读取本地文件lib/routes.mjs:286
await pipeline(createReadStream(target.filePath), response); - 删除本地文件lib/routes.mjs:302
await rm(target.filePath, { force: true }); - 写入本地文件lib/routes.mjs:378
await mkdir(path.dirname(aside), { recursive: true }); - 写入本地文件lib/routes.mjs:379
await copyFile(target, aside); - 写入本地文件lib/routes.mjs:388
await mkdir(path.dirname(target), { recursive: true }); - 写入本地文件lib/routes.mjs:389
await copyFile(source, target); - 写入本地文件lib/routes.mjs:562
await mkdir(path.dirname(destination), { recursive: true }); - 写入本地文件lib/routes.mjs:576
await pipeline(request, limiter, createWriteStream(part, { flags: 'w' })); - 删除本地文件lib/routes.mjs:582
await rm(part, { force: true }).catch(() => {}); - 删除本地文件lib/routes.mjs:593
await rm(part, { force: true }).catch(() => {}); - 读取本地文件lib/share.mjs:77
const info = await stat(filePath); - 读取本地文件lib/share.mjs:257
const info = await stat(share.filePath).catch(() => null); - 读取本地文件lib/share.mjs:296
const stream = createReadStream(share.filePath, range === null ? undefined : { start: range.start, end: range.end }); - 读取本地文件lib/archive.mjs:32
const stream = createReadStream(longPath(absolute)); - 写入本地文件lib/archive.mjs:95
await mkdir(longPath(path.dirname(outPath)), { recursive: true }); - 删除本地文件lib/archive.mjs:97
await rm(longPath(partial), { force: true }); - 写入本地文件lib/archive.mjs:100
const output = createWriteStream(longPath(partial)); - 删除本地文件lib/archive.mjs:163
await rm(longPath(partial), { force: true }).catch(() => {}); - 删除本地文件lib/archive.mjs:173
await rm(longPath(outPath), { force: true }).catch(() => {}); - 写入本地文件lib/archive.mjs:174
await rename(longPath(partial), longPath(outPath)); - 读取本地文件lib/archive.mjs:175
const info = await stat(longPath(outPath)); - 读取本地文件lib/archive.mjs:184
const source = createReadStream(longPath(filePath)); - 读取本地文件lib/archive.mjs:233
const info = await stat(longPath(filePath)); - 写入本地文件lib/archive.mjs:240
if (!dryRun) await mkdir(longPath(destDir), { recursive: true }); - 写入本地文件lib/archive.mjs:263
await mkdir(longPath(safeJoin(destDir, fields.path)), { recursive: true }); - 写入本地文件lib/archive.mjs:282
await mkdir(longPath(path.dirname(absolute)), { recursive: true }); - 读取本地文件lib/archive.mjs:322
const source = createReadStream(longPath(filePath)); - 读取本地文件lib/collect.mjs:80
info = await lstat(longPath(absolute)); - 读取本地文件lib/collect.mjs:126
entries = await readdir(longPath(absoluteDir), { withFileTypes: true }); - 读取本地文件lib/collect.mjs:140
info = await lstat(longPath(absolute)); - 读取本地文件lib/collect.mjs:150
target = await readlink(longPath(absolute)); - 读取本地文件bootstrap/bootstrap.mjs:113
await access(args.packagePath); - 读取本地文件lib/installer.mjs:37
await access(longPath(target)); - 写入本地文件lib/installer.mjs:106
await mkdir(longPath(targetDir), { recursive: true }); - 读取本地文件lib/installer.mjs:109
entries = await readdir(longPath(sourceDir), { withFileTypes: true }); - 读取本地文件lib/installer.mjs:128
const linkTarget = await readlink(longPath(source)); - 删除本地文件lib/installer.mjs:129
if (await exists(target)) await rm(longPath(target), { force: true }); - 写入本地文件lib/installer.mjs:139
await copyFile(longPath(source), longPath(target)); - 写入本地文件lib/installer.mjs:156
await mkdir(path.dirname(target), { recursive: true }); - 读取本地文件lib/installer.mjs:158
const info = await stat(longPath(source)); - 写入本地文件lib/installer.mjs:160
else await copyFile(longPath(source), longPath(target)); - 读取本地文件lib/installer.mjs:176
names = await readdir(longPath(dir)); - 删除本地文件递归lib/installer.mjs:189
await rm(longPath(path.join(dir, name)), { recursive: true, force: true }).catch(() => {}); - 删除本地文件递归lib/installer.mjs:227
await rm(longPath(renamedDir), { recursive: true, force: true }); - 写入本地文件lib/installer.mjs:228
await rename(longPath(stagedProfileDir), longPath(renamedDir)); - 读取本地文件lib/installer.mjs:243
for (const entry of await readdir(longPath(stageDir), { withFileTypes: true })) { - 写入本地文件lib/installer.mjs:254
await mkdir(path.dirname(aside), { recursive: true }); - 删除本地文件递归lib/installer.mjs:255
await rm(longPath(aside), { recursive: true, force: true }); - 删除本地文件递归lib/installer.mjs:281
await rm(longPath(stageDir), { recursive: true, force: true }).catch((error) => { - 读取本地文件lib/rewrite.mjs:140
text = await readFile(longPath(absolute), 'utf8'); - 读取本地文件lib/rewrite.mjs:166
targetParsed = JSON.parse(await readFile(longPath(targetPackagePath), 'utf8')); - 写入本地文件lib/rewrite.mjs:187
await writeFile(longPath(absolute), next, 'utf8');
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
DeepSeek Harness 的离线/在线导入导出与迁移:配置档、插件、技能、钩子、看板
最近更新
在 GitHub 查看全部 →v0.1.102026-10-20
· 0.1.10 — 2026-10-20
· 修复
· 上传输入没有禁用(lib/client.js):上传进行中仍能再次选择文件,第二次点击会被 uploadBusy 静默丢弃,用户看不出为什么没反应。现在上传或任务进行中文件输入框是 disabled。
· 删除包用的是闭包里的旧选中值(lib/client.js 的 deleteArtifact):回调捕获的 selected 可能是上一次渲染的值,删掉当前选中的包时不会清空导入卡片。现在改用函数式 setSelected,删除任何包后都清空预览结果。
· 刷新后丢失正在运行的任务(lib/client.js 的 refresh):面板重载只恢复数据、不恢复 busyJobId,进度条消失、按钮全部可点。现在 refresh 读到 status.busyJobId 且当前没有轮询时,取 /jobs/detail 重新接管该任务。
· 导入与恢复完成后没有提示重启(lib/client.js):写入的配置要重启 DSH 才生效,面板此前只报「已恢复」。现在导入完成与备份恢复的提示都带「需要重启 DSH 才会生效」。
· 包读取失败被伪装成「缺 manifest」(lib/archive.mjs 的 readManifest / extractPackage):createReadStream 的错误没有转发给 gunzip,pipe 会吞掉它,调用方拿到的是 missing dsh-portable/package.json(指向包内容而不是不可读的路径),还可能因未处理的 error 事件让进程退出。现在 source 的错误直接 gunzip.destroy(error)。
· 测试
· 新增 1 项(缺失文件抛 ENOENT 而不是「缺 manifest」),全量 60 项通过。
v0.1.92026-10-20
· 0.1.9 — 2026-10-20
· 修复
· 成功提示永不消失(lib/client.js):导出完成、上传完成、备份已恢复这些 notice 提示此前会一直挂在面板顶部,下一次操作的反馈只能叠在旧提示下面。现在 4 秒后自动收起,重设或组件卸载时清掉计时器。
· 复制超时的计时器不释放(lib/client.js 的 copyText):1.5 秒超时用的 setTimeout 在 navigator.clipboard.writeText 成功时不会被清理,页面里留下一个悬空计时器。现在用 try/finally 保证 clearTimeout。
· 测试
· 无新增(纯前端计时器行为,node --check + 59 项全量通过)。
v0.1.82026-10-20
· 0.1.8 — 2026-10-20
按「同类问题全项目审计」的结论逐项收口:4 个 P1 正确性缺陷、8 个 P2 健壮性缺陷、6 个 P3 细节与 2 个疑似项。没有新功能,全部是让既有行为按文档所写的方式兑现。
· 修复(正确性)
· package.json 合并从来没有执行过(lib/rewrite.mjs / lib/installer.mjs):mergeProfilePackage 写好了、HANDOFF 也宣称「按目标机现状合并」,但 install 路径只调用了 rewriteExtractedTree,后者只做路径替换。结果导入会把源机的 dependencies 直接盖在目标机上,目标机后来装的插件在下次 npm install 时会被当成 extraneous 删掉。现在 rewriteExtractedTree 接收 targetPackagePath,读目标机 package.json 后按 mergeProfilePackage 合并,结果写入 report.rewrites.merged 并在面板显示(保留 / 缺失的 bundle)。
· 备份恢复会覆盖无法回滚的文件(lib/routes.mjs 的 /backups/restore):pre-restore 拷贝失败时被 catch(() => {}) 吞掉,接着照样把备份写回去。现在失败项进入 failed 并跳过,绝不覆盖没有留下应急副本的文件。
· jobs.dispose() 返回 undefined(lib/jobs.mjs):index.js 写的是 void jobs.dispose().catch(...),worker 优雅退出路径根本没有被等待。现在 dispose() 收集每个 worker.terminate() 并返回 Promise.all。
· Range 下载不计入次数(lib/share.mjs):浏览器断点续传会发 Range: bytes=0-,此前只看「有没有 Range 头」,于是一次性链接可以无限下载。现在只有完整区间(range === null 或 0..size-1)才计数。
· worker 退出未上报结果时任务永久 running(lib/jobs.mjs):原逻辑只在退出码非 0 时标失败,code 0 会让任务卡在 running、面板按钮全禁用。现在任何「running/queued 状态下退出且没上报结果」都记为 failed 并带退出码。
· 修复(面板)
· 刷新会清掉刚显示的错误(lib/client.js 的 refresh):成功分支的 setError(null) 会吃掉 follow 刚设置的失败信息(0.1.7 只修了时序,本次从根上去掉)。
· 上传没有进度、也没有防重入(lib/client.js):fetch 改 XMLHttpRequest,upload.onprogress 驱动百分比;uploadBusy ref 挡住连点。
· 轮询可能重叠(lib/client.js 的 follow):前一次 jobs/detail 未返回时不会发起下一次(pollBusy ref)。
· 请求没有超时(lib/client.js 的 call):统一 60 秒 AbortController,超时错误写明 timeout after 60s: 。
· 任务无法取消:JobBar 增加取消按钮(POST /jobs/cancel),running 时才显示。
· 导出没有百分比(lib/archive.mjs):write 阶段进度补 percent,面板显示 已完成 / 总数 files。
· JOB_BUSY 被当成 500(lib/routes.mjs):现在回 409 并带 busyJobId。
· 修复(边界与清理)
· /artifacts/delete 删除不存在的包:404,不再是静默成功。
· /import 预检:包不在磁盘上直接 404,不进 worker。
· /share/remove 移除不存在的分享:404。
· /artifacts/download 支持 HEAD(返回头、空 body),handle 把 HEAD 映射到 GET 路由。
· 备份保留策略(lib/installer.mjs):keep: 0 现在真的清空普通快照;*-pre-restore 目录永不被预算修剪(否则「恢复」唯一能撤销的手段会被自己删掉)。
· 导入中途失败不再留下 portable-staging 目录:staging 清理移入 finally。
· settings() 优先取 profileContext.dshHome,
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。
README
由 DeepSeek 最新模型翻译生成dsh-portable
给 DeepSeek Harness 用的离线 / 在线导入导出插件:把一台机器上正在工作的 DSH 主目录打包成单个 .dshpkg,在另一台机器上还原 —— 不需要联网、不需要 pnpm install、不需要手工改路径。
English · CHANGELOG · 发布流程 · 接力文档
为什么要有它
把 DSH 环境搬到另一台机器,麻烦有三层:
1. 一个能用的 profile 不止一个文件。 它是 profiles/<name>/ 加上带 node_modules 的插件、技能、hooks、任务板、存储、AGENTS.md、已下载运行时、会话日志。
2. 插件是靠 pnpm 联网装的。 内网机、被锁的办公电脑、代理坏掉的机器根本跑不了 dsh plugin add。
3. 配置里写死了绝对路径(C:\\Users\\me\\.dsh、/home/me/.dsh)和 profile 名。直接复制目录树,换台机器启动就是一堆坏掉的插件路径。
dsh-portable 三件事一起解决:把白名单内的根打成单个自描述归档;任何时刻都不走出白名单;导入时只重写真正含本机路径的那六个配置文件。
展开完整文档(README 剩余 1 节)
dsh-portable
给 DeepSeek Harness 用的离线 / 在线导入导出插件:把一台机器上正在工作的 DSH 主目录打包成单个 .dshpkg,在另一台机器上还原 —— 不需要联网、不需要 pnpm install、不需要手工改路径。
English · CHANGELOG · 发布流程 · 接力文档
为什么要有它
把 DSH 环境搬到另一台机器,麻烦有三层:
1. 一个能用的 profile 不止一个文件。 它是 profiles/<name>/ 加上带 node_modules 的插件、技能、hooks、任务板、存储、AGENTS.md、已下载运行时、会话日志。
2. 插件是靠 pnpm 联网装的。 内网机、被锁的办公电脑、代理坏掉的机器根本跑不了 dsh plugin add。
3. 配置里写死了绝对路径(C:\\Users\\me\\.dsh、/home/me/.dsh)和 profile 名。直接复制目录树,换台机器启动就是一堆坏掉的插件路径。
dsh-portable 三件事一起解决:把白名单内的根打成单个自描述归档;任何时刻都不走出白名单;导入时只重写真正含本机路径的那六个配置文件。
包里装什么
| 根 | 路径 | 默认 |
| profile | profiles/<name>(配置和插件的 node_modules) | 总是 |
| skills | skills | 总是 |
| skills-disabled | skills-disabled(宿主已禁用的技能) | 可选,默认不含(includeDisabledSkills) |
| hooks | hooks | 总是 |
| storages | storages | 总是 |
| 任务板 | task-board / todo-board 存储 | 总是 |
| 内置浏览器宿主 | dsh-builtin-browser-host | 总是 |
| 智能体指令 | AGENTS.md | 总是 |
| 会话日志 | sessions | 可选(includeSessions) |
| 运行时 | 已下载的运行时 | 可选(includeRuntimes) |
| 凭据 | .credentials.yaml | 可选,默认关闭 |
| source map | 包内 .map | 默认包含(includeSourceMaps) |
| 备份文件 | 配置旁 .bak- | 默认不含(includeBackups) |
skills-disabled 曾是默认包含(在真实机器上占整包 86%),现在与其它可选根一致:默认排除,勾选「已禁用的技能」或传 includeDisabledSkills: true 才打包。
表外的路径永远不会被采集 —— 代码里不存在"把整个主目录递归复制一遍"的步骤。
安装
dsh plugin --profile desktop add dsh-portable
插件必须登记在 profile 自己的 package.json 里 —— dependencies 和 dsh.profile.bundles 两处都要。把包目录手工拷进 profiles/<profile>/node_modules 看起来能跑,但该 profile 下次执行 npm install 时,npm 会把没声明的包当 extraneous 删掉;那之后面板照常渲染卡片,但每个请求都回 404。dsh plugin add(或在 profile 里 npm install dsh-portable)会把这两处一起写好。
然后打开 DSH 设置 → 便携迁移。设置页区块注册在 settings.section、顺序 70,中英文随宿主语言自动切换。
不按按钮就什么都不会写进 DSH 主目录。
面板怎么用
六个卡片区:
1. 导出 —— 勾选可选根、给文件起名、点导出。打包跑在 worker 线程里;进度条实时显示条目数,20 MB / 14 000 条目的包约 3 秒。填加密口令(留空则不加密)可以把整包用 AES-256-GCM 加密——口令丢失无法恢复。
2. 包列表 —— <DSH_HOME>/dsh-portable/exports 下的所有 .dshpkg,显示大小、修改时间,按钮为 选择 / 下载 / 分享 / 删除(删除需二次确认),加密包带「已加密」标记。选择把包标记给下面的导入卡片;下载在面板内分段拉取并显示进度条,支持断点续传(优先直写磁盘,过大的包交给浏览器原生下载器)——局域网分享只在两台机器同网时可用。
3. 上传 —— 选一个 .dshpkg,流式落盘(先写 .part 再改名),带百分比进度,2 GB 的包也不会进内存;加密包同样可以上传。
4. 导入 —— 选 merge 或 replace(当前选中项高亮),先预览(零写入),再点「执行导入」,此时会先弹出内联确认块,确认后才真正写入。子路径选择:预览后可勾选要导入的顶层项、或在附加框里填包内子路径(如 skills/my-skill),两者取并集,留空表示整个包;replace 模式只会把选中的路径移去备份。加密包先填解密口令。结果以键值表格呈现:落地项、被重写的路径数、备份目录、导入范围与 warning,并额外显示插件清单合并结果(本机保留的 bundle、包需要但本机缺失的 bundle)。导入完成后需要重启 DSH,写入的配置才会生效。
5. 局域网分享 —— 把包发布成一次性下载链接(http://<内网IP>:<端口>/d/<token>)。默认 1 次下载、60 分钟,支持 Range/HEAD,过期或下完返回 410。复制会把链接写进剪贴板,且只有真的写成功才显示「已复制」(纯 HTTP 来源下退回 textarea 方案)。无中继、无云端、无账号。
6. 备份 —— 导入前自动生成的备份(<DSH_HOME>/portable-backups/<stamp>),保留最近 5 份。每份都有恢复按钮:把该备份里的文件写回主目录,写回前会先把当前版本另存到 <stamp>-pre-restore。恢复后同样需要重启 DSH。
界面截图
截自 DSH 设置面板;图中的主目录用户名与局域网分享链接均为示例值。
导出卡片与包列表
上传、导入、局域网分享与备份
HTTP API
前缀:/dsh-portable/api。除下载路由与上传流式路由外,响应都是 JSON。鉴权复用宿主的 connection.requestRejection(cookie + 可信来源),该服务不存在时降级为仅回环地址可访问。
| 方法 | 路径 | 用途 |
| GET | /status | 宿主信息、包数量、当前任务、分享服务状态 |
| GET | /profiles | 本机 DSH 主目录里的 profile |
| GET | /jobs | 最近的任务(导出 / 预览 / 导入) |
| GET | /jobs/detail?id= | 单个任务的实时进度 |
| POST | /jobs/cancel | 取消任务 |
| POST | /export | 开始导出 |
| GET | /artifacts | 可导入的包列表(含 encrypted 标记) |
| GET | /artifacts/meta?id= | 单个包的 manifest(加密包回 400 ENCRYPTED,口令不进 URL) |
| GET | /artifacts/download?id= | 以附件形式把包流给浏览器;支持 Range/If-Range(206 断点续传)与 HEAD |
| POST | /artifacts/delete | 删除包(UI 里二次确认) |
| PUT | /upload/<name> | 上传包,流式落盘 |
| POST | /import/preview | 两阶段安装第一阶段(零写入) |
| POST | /import | 两阶段安装第二阶段(必须 confirm:true) |
/export、/import/preview、/import 共用这些可选字段:include(包内逻辑路径前缀数组,如 ["skills/my-skill"],只导出/导入所选子路径)、passphrase(加密包口令,随请求体走,不进 URL、不进任务描述)。
| GET | /backups | 自动备份列表 |
| POST | /backups/restore | 把某份备份写回主目录(写回前先另存当前文件) |
| POST | /share | 生成一次性局域网下载链接 |
| GET | /share | 列出有效链接 |
| POST | /share/remove | 撤销链接 |
所有涉及"哪个包"的参数都只接受 artifactId(如 exports:machine-20261018.dshpkg),绝不接受文件路径。 /、\\、.. 与控制字符一律拒绝,所以模型或网页都没法借这个插件读写任意文件。
模型工具
注册到宿主的 tools 服务(服务不存在时静默跳过):
| 工具 | 行为 |
| portable_export | 打包当前 profile,返回 id、大小、条目数、各根统计;支持 passphrase(加密)与 include(子路径) |
| portable_inspect | 只读 manifest,不导入;加密包传 passphrase |
| portable_import | 不带 confirm:true 就是 dry-run;带上才真正两阶段安装;支持 include(子路径)与 passphrase |
| portable_share | 生成一次性局域网链接(1 次下载、60 分钟) |
无宿主引导安装(目标机没装 DSH)
Windows
powershell -ExecutionPolicy Bypass -File bootstrap\install.ps1 preview .\machine.dshpkg
powershell -ExecutionPolicy Bypass -File bootstrap\install.ps1 install .\machine.dshpkg --yes
macOS / Linux
sh bootstrap/install.sh preview ./machine.dshpkg
sh bootstrap/install.sh install ./machine.dshpkg --yes
参数:--home <目录>(默认 $DSH_HOME 或 ~/.dsh)、--profile <名>、--mode merge|replace、--keep <n>、--passphrase <口令>(加密包)、--include <子路径>(可重复)、--yes。退出码:0 成功、1 失败、2 用法错误;非 TTY 且没有 --yes 时拒绝写入。
包格式
.dshpkg 就是 gzip 压缩的 tar,manifest 是第一条目:
.dsh-portable/manifest.json
profiles/desktop/...
skills/...
hooks/hooks.json
...
{
"format": "dsh-portable/1",
"createdAt": "2026-10-18T02:11:03.000Z",
"tool": { "name": "dsh-portable", "version": "0.1.0" },
"source": {
"hostname": "WORKSTATION",
"platform": "win32",
"arch": "x64",
"node": "v22.23.2",
"dshHome": "C:\\Users\\me\\.dsh",
"profile": "desktop"
},
"options": { "includeNodeModules": true, "includeSessions": false },
"stats": { "entries": 13774, "files": 13482, "dirs": 292, "links": 0, "bytes": 20664511 },
"roots": [{ "id": "skillsDisabled", "logical": "skills-disabled", "files": 13193, "bytes": 44615569 }]
}
因为就是普通 tar,tar -xzf machine.dshpkg 在任何机器上都能直接看包里的内容 —— 哪怕那台机器从没听说过 DSH。长路径走 PAX 扩展头,读取端是增量解析,PAX 正文跨 chunk 也能正确处理。
加密包(导出时填了口令)整体是一层 AES-256-GCM 容器:magic "DSHPKG-ENC1\n" + scrypt 盐(32B) + iv(12B) + 密钥确认块 + 密文 + 包尾 16B 认证标签,密钥由口令经 scrypt(N=2¹⁵)派生,全程流式。加密包不是明文 tar,tar -xzf 看不到内容;口令错误在读取最开始就会被拒绝(报「wrong passphrase or corrupted package」),篡改任何一个字节都无法通过认证。口令没有恢复途径——丢了就等于丢了包。
导入流水线
预览: 读 manifest ──► 每个顶层项的文件数/字节数/目标机是否存在 ──► 报告(零写入)安装: 解包到 <home>/portable-staging/<stamp>(边写边校验 sha256) ──► 目标 profile 名不同则重命名目录 ──► 重写 6 个配置文件里的本机路径 ──► 这 6 个文件备份到 <home>/portable-backups/<stamp> ──► 逐顶层项落地(merge:只增不减;replace:先移走再落) ──► 清理旧备份 ──► 删除暂存区
包尾的 .dsh-portable/checksums.json 记录每个文件的 sha256。导入时边写边算,对不上就直接拒绝该包,被篡改或传坏的包不会被静默还原成"看起来正常"的数据。没有该条目的包(旧包、手工打的包)仍可导入,但报告里是 verified: false,不会假装校验过。
磁盘余量。 导入会先把整包解开到 portable-staging/<stamp>,再合并到目标位置,因此峰值占用约为"解压后体积"叠加在现有 profile 之上。这次双写换来的是失败可回滚——落地前目标目录一点都没动。
取消导入时先让 worker 自行中止并退出(最多等 5 秒),确认退出不了才强制终止,因此暂存目录一定会被清掉:取消一个 1 GB 的导入不会留下 1 GB 垃圾。
路径重写刻意收窄:只碰 profiles//cordis.patch.yml、cordis.yml、package.json、pnpm-workspace.yaml、hooks/hooks.json、hooks/settings.json。插件包内部不重写,第三方完整性校验与 source map 不受影响。
安全模型
- 白名单采集。 代码里不存在递归遍历整个主目录的逻辑。
- 只认 artifactId,不认路径。 见上。
- 导入必须确认。 缺 confirm:true 的路由返回 400 CONFIRM_REQUIRED;模型工具默认 dry-run。
- 可选口令加密。 导出时可给整包加 AES-256-GCM(scrypt 派生密钥,见「包格式」);错误口令立即拒绝,篡改无法通过认证。口令随请求体传递,不进 URL、不进任务描述。
- 解包防护。 200 000 条目上限、单文件 2 GB、总解压 8 GB、压缩比 200(输出超 64 MB 后才生效)、wx 不覆盖、safeJoin 挡 ..、符号链接为绝对路径或越界时拒绝;合并落地时再次校验符号链接目标,绝不还原指向主目录之外的链接。
- 逐文件校验。 包尾 checksums.json 收录每个文件的 sha256,导入时比对,不一致直接失败。
- 写之前先备份。 六个配置文件先复制到备份目录;replace 模式把要替换的目录移进 portable-backups/<stamp>/replaced/,不是删除;子路径选择时只移走所选路径本身。
- 凭据默认不进包。 .credentials.yaml 只有显式勾选才打包,面板上的勾选框直接标注风险。导出是纯本地操作,不上传任何数据。
- 分享链接范围极小。 一次下载、60 分钟、单文件、token 门禁,之后 410。没有目录列举,没有中继。
开发
npm test # 89 项:tar、archive、crypto、installer、jobs、routes、share、inspect
npm run check # 语法检查 + import 冒烟
npm run sync # 把本包拷进某个 profile 的 node_modules 做真机联调
npm run verify # 发布门禁:版本一致、文件清单、测试
没有构建步骤:index.js(宿主端)、lib/client.js(浏览器端 bundle,纯 React.createElement)、lib/.mjs 都是直接发布的源码。这是刻意约束 —— 插件必须能在包版本未知的宿主上加载,所以客户端半边只依赖 react 和 --dsw-alias-* CSS 变量。
兼容性
- DSH >= 0.1.7-rc.2
- Node ^22.15.0 || >= 24.0.0
- Windows / macOS / Linux(Windows 长路径走 \\?\ 前缀)
已知限制
- 会话日志默认不进包,因为里面有对话内容。
- 加密是可选项且无签名:不填口令的包是明文 tar,含凭据时请加密导出或放在加密介质上;口令一旦丢失无法恢复。
- 子路径选择器按逻辑路径前缀匹配(目录/文件级),不支持通配符。
- 指向 DSH 主目录之外的符号链接会被打进包,但在 Windows 上没有开发者模式时无法重建 —— 这种情况记 warning,不中断导入。
许可
MIT,见 LICENSE。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
wjf1/dsh-portable
给 DeepSeek Harness 用的离线 / 在线导入导出插件:把一台机器上正在工作的 DSH 主目录打包成单个…
相关插件 · 其他
模型破甲清洗YuJunZhiXue/dsh-purge
⚔️ DeepSeek Harness 网络安全红队工具dsh-purge
★ 16,148已汉化DeepSeek 客户端
73
女娲蒸馏alchaincyf/nuwa-skill
输入名人名字,提炼其思维模型与决策方式供你调用
★ 37,308已汉化DSH
72
逆向分析桥接Fanqi89/dsh-rea
REA(Reverse Engineer Anything)MCP 桥接,用来把 REA 逆向工具接入 DSH 插件
★ 51已汉化DSH
72
语音女友beiyege-01/dsh-voice-ai-girlfriend
语音 AI 女友Voice AI girlfriend for DeepSeek Harness:Whisper…
★ 888已汉化DSH
71.6
红队越狱测试套件Minglink/dsh-infinite-gen-5
DeepSeek v4.1 flash 逆向工具无限五代 求 Star 收藏 ⭐欢迎大家提交项目的改进
★ 3,759已汉化DSH
71.4
协同办公空间dream-num/univer-workspace
一个让人类与 AI Agent 共同创作、协作和审阅的开源 Office 工作空间。
★ 2,266已汉化DSH
70.7