# 验证记录(dsh-plugin-backup 0.2.0) 本文件记录本插件在**实际环境**中的验证过程与结果,包括发现的问题与修复。 所有命令均在 Windows + PowerShell 7 下执行。 > 本文是**一次适配的流水账**;提炼成可复用流程(含已验证的命令与判定矩阵)见 > [plugin-development-guide.md](plugin-development-guide.md)。 ## 环境 | 项 | 值 | |---|---| | Node | v24.18.1 | | DSH | 0.1.7-rc.2(CLI `@deepseek-ai/dsh`)与 **0.2.0-rc.2**(桌面端内嵌运行时) | | DSH_HOME | `C:\Users\<用户名>\.dsh` | | profile | `web`(0.1.7 实例)与 `desktop`(0.2.0 桌面端) | | 包来源 | `file:` | | GUI | 0.2.0 桌面端 `DeepSeek Harness`(本地 19387);验证用独立实例 19399 | ## 1. 离线测试 ```powershell npm test ``` 结果:**50 项全部通过**(`node --test test/*.test.mjs`)。 覆盖范围与"使用真实官方包"的边界: | 测试文件 | 内容 | 用到的真实依赖 | |---|---|---| | `test/tar.test.mjs` | 缓冲与流式两套 tar 收发;路径穿越/校验和/截断拒绝;超限中止不留残file | — | | `test/manifest.test.mjs` | 命名、manifest 校验、路径展开、原子写 | — | | `test/engine.test.mjs` | run/verify/restore 往返、篡改检测、保留策略、单飞锁、下载票据、偏好持久化、无配置层时的默认值、GitHub 前置校验 | — | | `test/plugin.test.mjs` | 真实 Cordis 激活、宿主描述符校验、真实注册表注册、真实 Cordis 挂载、6 个工具 schema 编译、下载路由端到端、重复激活保护、客户端命名空间保留名断言、`setPreferences` 返回形状锁定 | `dsh-typert-loader`、`dsh-typert-registry`、`dsh-tools`、`cordis` | | `test/client.test.mjs` | 模块装载器下求值、设置页注册、描述符逐字段比对宿主、用迷你 hooks 运行时渲染页面、保存后不复白屏、残缺载荷与残缺行降级、渲染崩溃被本节区兜住并可重试恢复 | — | | `test/package.test.mjs` | manifest / bundle 补丁 / Config schema 三者一致,所有声明文件存在,且 dsh peer 窗口同时放行 `0.1.7-rc.2` 与 `0.2.0-rc.2`、排除 `0.3.x` | `yaml` | 同时 `node --check` 通过全部 12 个 `.js` 文件。 ## 2. 安装 ```powershell dsh plugin --profile web add file: ``` 输出要点:`+ dsh-plugin-backup file:…`,并把包名追加进 `dsh.profile.bundles`。 `--dump-config` 能看到该行与解析后的默认配置: ``` - id: dsh-plugin-backup name: dsh-plugin-backup config: backupPath: '' intervalMs: 0 categories: { settings: true, skills: true, plugins: true, sessions: false, storages: false } retention: 20 verifyOnCreate: true maxArchiveBytes: 536870912 github: { enabled: false, repo: '', branch: main, subdir: dsh-plugin-backup, tokenEnv: DSH_BACKUP_GITHUB_TOKEN } ``` ## 3. 运行中实例(HMR) 用户正在使用的 GUI 未重启。profile 清单变化后 DSH 现场重组合,宿主半成功挂载: ```powershell curl.exe -s -i "http://127.0.0.1:3080/dsh-plugin-backup/download" ``` ``` HTTP/1.1 404 Not Found content-type: text/plain; charset=utf-8 dsh-plugin-backup: download ticket is unknown, already used, or expired ``` 这条响应体只可能来自本插件的路由处理器,因此可确认:profile 已重组、宿主半已 `apply()` 到注册下载路由那一步(其前一步就是注册 6 个 `backup_*` 工具)。 ## 4. 真实 $DSH_HOME 测量 ```powershell node test/local/measure-real-home.local.mjs settings plugins node test/local/measure-real-home.local.mjs skills ``` | 分类 | 文件数 | 原始 | 归档 | 耗时 | 校验 | dry-run 恢复 | |---|---|---|---|---|---|---| | `settings` | 5 | 2.0 KB | 1.3 KB | 216 ms(含 `plugins`) | 通过 | 通过 | | `plugins` | 39 | 1.24 MB | 1.12 MB | 同上 | 通过 | 通过 | | `skills` | 29 356 | 342.5 MB | 144.8 MB | 49.9 s | 通过 | 通过(29 356 个文件) | `skills` 一次运行的内存:`rss 57.5 MB → 158.0 MB`,`heapUsed +14.9 MB`。 测量脚本在结束时删除了临时备份与临时目录。 ## 5. 发现的问题与修复 | # | 现象(真实数据/运行中暴露) | 修复 | |---|---|---| | 1 | 默认单类上限 64 MiB 在真实技能树上直接 `too-large`(实测归档 144.8 MB) | 默认 `maxArchiveBytes` 提到 512 MiB(schema/补丁/`settings.js` 三处同步) | | 2 | 缓冲式打包会把整个分类读进内存(`skills` 原始 342.5 MB) | 拆分为流式 `createArchive` / `scanArchive`:创建、校验、恢复全部恒定内存 | | 3 | 运行中实例的插件曾一度消失(同一进程内下载路由从有响应变为 SPA 的空 404);离线复现出"重复激活"会抛 `service "backup" has been registered`,即新 fiber 挂载失败 | `apply()` 先检查 `ctx.get('backup')`:已有实例则告警并复用,不再让新 fiber 失败;并新增"重复激活"回归测试(含路由不重复注册、不重复注册工具) | | 4 | 工具 schema 里写 `required: false` 被 `defineTool` 拒绝(`parameters.categories.required must be true when present`) | 可选属性一律省略 `required` | | 5 | JSDoc 注释里写了 `a/*/b` 形态的 glob,其中的 `*/` 提前终止了块注释,导致文件语法错误 | 改写注释措辞(同类 `*/` 序列已全仓排查) | | 6 | pnpm 的 `file:` 安装是**快照**:profile 里是副本,改完源码不会自动生效 | 明确文档化,并在改动后 `dsh plugin --profile web remove` + `add` 重新安装;已核对安装副本与工程文件 sha256 完全一致 | | 7 | 用户报告:页面报 `读取备份状态失败:client api: method "backup/remove" conflicts with its namespace service` | 定位到客户端载体的 `RemoteNamespaceService` 自带 `remove` 成员(`REMOTE_NAMESPACE_FIELDS` + 类成员),同名远端方法在 `$mount` 时被拒。删除端点线上名改为 `deleteBackup`(宿主描述符、客户端描述符、调用处同步),并新增保留名断言防回归 | | 8 | 默认备份路径与用户预期不符(原为 `$DSH_HOME/backups`) | 默认改为 `<用户目录>/dsh_backups`,Windows 即 `C:\Users\<用户名>\dsh_backups`(`lib/core/util.js` 的 `defaultBackupPath()`,schema/补丁注释/README/测试同步) | | 9 | 备份分类只能改"默认开关",无法在一次备份里挑选分类 | 页面改为**分类备份选择**:勾选即选择,「全选/全不选」一键切换,按钮显示「立即备份(N 个分类)」,未选时禁用;`run` 显式带上所选分类,保存后同时作为定时备份的默认分类 | | 10 | 备份路径不可用时,整个设置页只报一句"读取备份状态失败",看不到其余内容;`backup_run` 还可能抛出未归一的 `ENOTDIR` | `snapshot()` 把不可用路径降级为状态(`state.backupPathExists/backupPathProblem`)并返回空列表,页面单独提示;`run`/`sync` 统一经 `#root()` 归一为 `BackupFailure`;新增"路径不可用"用例 | | 11 | 动作完成后刷新快照会把表单里刚做的选择覆盖回已保存的默认值(真机与测试都复现) | 动作触发的快照刷新改用 `seedDraft: false`;读取 effect 不再依赖 `t` 的引用身份(避免宿主重渲染导致表单被重置) | ## 5.4 第四轮:保存白屏与排版风格(0.1.4) 用户报告两条:**点击「保存设置」后设置页白屏**,以及**排版与风格需要修整**。 | # | 现象 | 定位过程 | 修复 | |---|---|---|---| | 15 | 点击保存后**设置分区整片变空白**(左侧导航仍在“数据备份”,右侧内容区空),刷新整页才恢复;控制台无提示 | 顺着点击链读代码:`saveSettings()` 把 `props.setPreferences()` 的**返回值直接当作快照** `accept()`;而宿主 `typert.js` 的 `setPreferences` 声明并返回的是 **PREFERENCES**(`{settings, catalog, github}`,见 `typert.js` 的 `PREFERENCES` schema 与 `setPreferences` 签名),**没有 `rows`/`total`/`state`**。于是渲染到 `rows.length` 时抛 `TypeError`。`act()` 的 `try/catch` 只包住异步调用体,覆盖不到随后的同步渲染,所以界面直接消失且无任何提示。DSH 壳层 `settings.section` 的错误边界捕获后会把该条目**永久除名(abdicate)**,因此关闭再打开设置也不会恢复——表现就是“白屏到刷新为止” | ① `saveSettings` 改为与其它动作一致:`setPreferences` 只用于判定成功,随后 `props.load()` 重读完整快照(顺带让表单回填真正落盘的值,例如展开后的绝对路径)。② `accept()` 增加载荷归一化:`settings`/`state`/`github` 非对象则兜底,`catalog`/`rows` 非数组则 `[]`,`total` 非数字则取行数——形状不合时降级为空列表而非崩页。③ 新增极小的类组件错误边界 `BackupBoundary` 包住本页:任何渲染异常都降级为一条提示 + 「重试」,**不再触发槽位除名** | | 16 | 排版与风格偏离设计系统:卡片/圆角用自造值、等宽字体 token 不存在(一直回退到浏览器默认 monospace)、`max-width:860px` 永不生效、路径输入框 `min-width:320px` 在窄面板溢出、手写输入控件无焦点环、GitHub 字段缺 `htmlFor`、备份行每行最多约 10 个同权重按钮、同屏两个 primary 按钮 | 逐一比对官方 token 与相邻设置页(`ui-theme` 的 `base.css`/`design-platform.css`、`ui-primitives` 的 `Button/Input/Checkbox/Switch.module.css`、`ui-settings-account` 与 `ui-settings-web-search` 的用法) | 卡片改用 `--dsw-alias-settings-card-stroke/-fill`,圆角走 `--dsw-radius-sm/md/lg`,等宽字体改真实存在的 `--ds-font-family-code`;删掉失效的 `max-width:860px` 与致溢的 `min-width:320px`,改 `width:100%` + `min-width:0`,节区加 `min-width:0`;文本/数字输入改用 primitives 的 `Input`,「创建后立即验证」改 `Checkbox`,「启用同步」改 `Switch`(并补可见文字,因为 `Switch` 的 `label` 只是无障碍名);补 `:focus-visible` 焦点环与 GitHub 四个 `htmlFor`;备份行的按分类下载与下载 manifest 收进一个 `Menu`(`dense`、`align:'end'`),删除按钮加危险色,仅「备份设置」卡保留 primary 保存;详情块加 `max-height` 与滚动 | **测试盲区的修补**(原先测不出来的原因): - 假 `remote` 的 `setPreferences` 返回的是**完整 view**(含 `rows`/`total`/`state`),恰好把宿主的真实返回形状掩盖了 → 现改为与线上契约一致的 PREFERENCES 形状。 - 原先**没有任何用例点击过保存按钮**(旧用例只断言“存在一个 save 按钮”)→ 新增“保存后页面仍在、列表仍在、且确实重读了快照”的回归用例;把 bug 改回去时该用例立即失败(已实测)。 - 新增“残缺载荷(只有 settings/catalog/github)降级为空列表而不崩”与“渲染抛错被本节区兜住并可重试恢复”两条用例;迷你 hooks 运行时补了最小类组件支持(错误边界需要)。 - `test/plugin.test.mjs` 新增断言:`setPreferences`/`preferences` 的返回键集必须是 `{catalog, github, settings}`,且**不得**含 `rows`/`total`/`state`;`snapshot` 则必须含这三者。 ## 5.2 第三轮:真机定位到的两个致命错误(0.1.3) 用户在 GUI 里报了两条:`读取备份状态失败:cannot get property "remote.backup" without inject` 与"分类备份无法勾选"。用**真实启动 + 临时埋点**定位,两条其实是同一根因族:Cordis 的 `ctx` 是 代理,**服务属性读**只在"该名字已注入当前 fiber"时才合法。 | # | 现象 | 定位过程 | 修复 | |---|---|---|---| | 12 | 客户端:`cannot get property "remote.backup" without inject` | `client.js` 的 `call()` 读 `context.remote.backup`;该命名空间由本插件自己 `$mount` 出来,无法写进 `inject`(写了就死锁:没有别的提供者),于是激活之后的属性读被代理拒绝 | 改用 `context.get("remote.backup")`(免注入读取);`inject` 保持 `["slots","locale","remote"]` 并写明原因 | | 13 | 分类无法勾选 | 上一条让 `load()` 失败 → `view === null` → `catalog` 为空 → 分类卡里根本没有复选框可点(不是控件失效) | 修 #12 后分类正常渲染;另加"分类列表不可用时给出明确提示",并给复选框补上 16×16 + `accent-color` 的设计系统样式 | | 14 | 宿主:**插件整体不激活**(下载路由静默消失,进而整页读不到状态)——同一进程时而正常时而失效 | 在 `apply` 里写临时埋点到 `%TEMP%`,真实启动读到三行:`enter tools=true webServer=false backup=false` / `exit ok` / 0.76 s 后 `route registered from the waiting child fiber`。即 `webServer` 服务比本插件晚约 0.8 秒可用,早先在 `apply` 里 `ctx.get('webServer')` 读到 `undefined` 就**静默跳过**了路由注册;而把 `webServer` 写进 `inject` 又会让非 Web profile 永久 pending | 路由注册改为 `ctx.inject(['webServer'], cb)` 的**子 fiber**:服务一出现就注册。工具注册表同样处理(有则立即挂、无则等)。`inject` 现为 `[]`,插件不依赖任何服务即可激活 | 复核(与真实启动同环境、独立端口 3099,验证完已关闭): ``` enter tools=true webServer=false backup=false exit ok route registered from the waiting child fiber ← 0.76 s 后 GET /dsh-plugin-backup/download → 404 + "dsh-plugin-backup: download ticket is unknown, already used, or expired" 启动输出无 "entry did not activate" 警告 ``` 客户端产物也直接核对过:从 `__DSH_BOOT__` 取出本插件的组合脚本 URL 抓下来,内容含 `exports.apply`、`TYPERT_REMOTE`,以及修复后的 `context.get?.("remote.backup")`。 **测试保真度提升**(这两类错误原先测不出来):`test/plugin.test.mjs` 与 `test/client.test.mjs` 现在都在**真实 Cordis 上下文**里激活插件——客户端用真实 `Service` 承载 `remote`,并让 `$mount` 真的 `provide('remote.backup', …)`,因此"属性读服务"会像线上一样 抛错;宿主侧新增"工具注册表缺席也能挂载、晚到再挂工具"的用例。 修复后再次安装并复核: ``` package.json index.js cordis.patch.yml client.js typert.js lib/core/tar.js lib/core/engine.js lib/tools.js → profile 副本与工程文件 SHA256 全部一致 → --dump-config 显示 maxArchiveBytes: 536870912 → 运行中实例的下载路由恢复正常响应 ``` ## 5.1 第二轮回修(0.1.2)的复核证据 | 修复 | 复核方式与结果 | |---|---| | 默认备份路径改为用户目录 | 用真实 `DSH_HOME=C:\Users\<用户名>\.dsh` 实例化引擎并读取 `snapshot`:`默认路径 = C:\Users\<用户名>\dsh_backups`;同时 `test/engine.test.mjs` 新增"无任何配置层时回落文档默认值"用例,断言默认分类为 `settings/skills/plugins = true`、`sessions/storages = false` | | `backup/remove` 命名空间冲突 | `test/plugin.test.mjs` 新增断言:9 个线上方法名都不得落在载体保留集合 `{ctx, empty, invokeRemote, methods, name, namespace, assertMethodAvailable, has, install, installDirect, installScoped, remove, constructor}` 内;`describe` 列表同时锁定删除端点名为 `deleteBackup`。离线也复现了原错误(把方法名改回 `remove` 时该断言立即失败) | | 分类备份选择 | `test/client.test.mjs` 用迷你 hooks 运行时渲染页面,点击「立即备份(1 个分类)」后断言宿主收到 `{ categories: ['settings'] }`;点击「全选」后重渲染计数变为 2,再点击则宿主收到 `{ categories: ['settings','skills'] }`。同时修掉一个真实副作用:动作完成后的快照刷新不再覆盖表单中的选择(`seedDraft: false`),且刷新 effect 不再依赖 `t` 的引用身份 | | profile 副本刷新 | `remove` + `add` 后逐文件比对 SHA256:`package.json / index.js / client.js / typert.js / lib/core/util.js` 全部一致;`cordis.patch.yml` 注释已说明新默认路径;运行中实例的下载路由仍正常响应 | ## 5.5 第五轮:适配 Desktop 0.2.0(0.2.0) 用户报告:桌面端 0.2.0 下插件装了不生效(安装被拒)。定位与修复如下。 **根因**(桌面端自己的日志,非猜测):`~/.dsh/profiles/desktop/.plugin-manager/logs/operation-zIqIli/pnpm.log` ``` dsh: installation rejected: Plugin dsh-plugin-backup@0.1.4 is incompatible with dsh 0.2.0-rc.2: peerDependencies {"@deepseek-ai/dsh-tools":"0.1.7-rc.2","@deepseek-ai/dsh-typert-protocol":"0.1.7-rc.2"}. … Exact-version exemption: not active. dsh: nothing was installed. ``` 0.2.0 的启动链新增了**安装前兼容性预检**:`dsh-app-boot` 的 `evaluatePluginCompatibility()` 把每个 `@deepseek-ai/dsh*` peer 与运行中的 dsh 版本比对,任一不满足即拒绝该 bundle (`loadProfileDirectory` 跳过、`preflight` 禁用行,除非用户按 插件名@版本 + 精确 dsh 版本授予 `allow-version` 豁免)。本插件当时把 `dsh-tools` / `dsh-typert-protocol` 精确锁在 `0.1.7-rc.2`,在 0.2.0-rc.2 上必然被拒。 **API 面核对**(确认除预检外无需改代码):把桌面端 `app.asar` 里的 `@deepseek-ai/dsh*` 0.2.0-rc.2 包逐个解包,与 web profile 中 0.1.7-rc.2 的对应包做 逐文件 diff: | 包 | 0.1.7-rc.2 → 0.2.0-rc.2 | |---|---| | `dsh-tools`(`defineTool`)、`dsh-typert-protocol`(`TypertRemoteService`)、`dsh-typert-loader`(`validateTypertManifest`)、`dsh-typert-registry`、`dsh-client-ui-slots`、`dsh-client-ui-settings`(`settings.section` 槽位与 `inject` 契约)、`dsh-client-connection`(`remote.$mount`)、`dsh-client-modules`(`dsh.client` 的 `platform`/`inject`/`external`、seed 模块表)、`dsh-host-webserver`(`register({kind:'exact'})`) | JS 主体逐字节一致,仅内联 CSS 类名/路径等构建产物差异 | | `dsh-client-ui-primitives` | `Button`/`Input`/`Checkbox`/`Switch`/`Tag`/`StateDot`/`Menu` 的 props 与签名一致;新版本增加 `MenuGroup`、`Input` 转 `forwardRef`、`TextShimmer` 重构 | | `dsh-client-locale` | 仅 CSS 模块哈希不同;`register`/`bind`/`resolveText` 不变 | 因此适配为**声明层修复**,不改运行时行为。 | # | 现象 | 修复 | |---|---|---| | 17 | 桌面端 0.2.0 安装直接被拒(上表日志) | `peerDependencies` 中 `dsh-tools` / `dsh-typert-protocol` 由 `0.1.7-rc.2` 精确锁改为 `>=0.1.7-rc.2 <0.3.0`;`engines.dsh` 同步为 `>=0.1.7-rc.2 <0.3.0`;包版本 `0.1.4 → 0.2.0` | | 18 | 上述窗口若无回归保护,日后又会被改窄 | `test/package.test.mjs` 新增用例:范围必须放行 `0.1.7-rc.2`、`0.2.0-rc.2`、`0.2.1-alpha.1`,且必须排除 `0.3.0`;`engines.dsh` 必须与 peer 窗口一致 | | 19 | 上一条用例本身有漏洞:它只比较 `>=`/`<` 两个端点,按运行时语义验证,漏掉了 npm 的预发布准入规则 | 1.0.0 复现并修正:在 0.2.0-rc.2 主机上 `npm install dsh-plugin-backup@<1.0.0` 报 `ERESOLVE`(`pnpm` 宽容,故未被发现)。peer 与 `engines.dsh` 改为 `^0.1.7-rc.2 \|\| ^0.2.0-rc.1`;用例重写为同时断言 **npm 解析语义**与 **运行时预检语义**(含放行 `0.2.0-rc.1`/`0.2.0-rc.2`、排除 `0.1.7-rc.1`/`0.3.0-rc.1`),并用真实 `semver.satisfies` 对 126 组「版本 × 范围 × 模式」做交叉核对(0 处不一致) | **1.0.0 复审(peer 窗口)**: - 复现:在装有 `@deepseek-ai/dsh-tools@0.2.0-rc.2` 的项目里 `npm install <旧包>` → `ERESOLVE … peerOptional @deepseek-ai/dsh-typert-protocol@">=0.1.7-rc.2 <0.3.0"`; 同一条目换 `pnpm add` 成功——差异正是两套预发布语义(§4.5)。 - 修正后:同一项目 `npm install` 与 `pnpm add` 均成功,且 `evaluatePluginCompatibility()` 在 `0.1.7-rc.2` 与 `0.2.0-rc.2` 两个运行时下仍判 compatible。 **复核证据**: - 用桌面端运行时自己的 `evaluatePluginCompatibility()` 跑候选声明: 旧声明在 `0.2.0-rc.2` 下报同样的 incompatible 文案;新声明在 `0.2.0-rc.2` 与 `0.1.7-rc.2` 两个运行时下都判 compatible。 - `dsh plugin --profile desktop add file:…`(桌面端 0.2.0-rc.2 的 CLI)→ 安装成功, `desktop/package.json` 的 `dependencies` 与 `dsh.profile.bundles` 都出现 `dsh-plugin-backup`;`dsh plugin --profile desktop list` 显示 `dsh-plugin-backup@file:…`。 - 用桌面端 CLI 新建临时 profile(`--from-default-profile web`)装入本插件后 `--dump-config`:插件行与其 `config` 默认值被正确解析(`maxArchiveBytes: 536870912` 等)。 - **真实启动**(0.2.0-rc.2 运行时,独立端口 19399):无 "entry did not activate"、 无 FAILED fiber,启动输出正常;`GET /dsh-plugin-backup/download` 返回本插件自己的 `404 dsh-plugin-backup: download ticket is unknown…`(证明宿主半已 `apply()` 到路由注册)。 - **浏览器端到端**(0.2.0-rc.2 的 boot manifest + 真实 GUI): - 插件行进入 `__DSH_BOOT__`:`{"id":"dsh-plugin-backup", …,"inject":["@deepseek-ai/dsh-client-ui-settings","@deepseek-ai/dsh-client-locale"],"external":["react","react/jsx-runtime","@deepseek-ai/dsh-client-ui-primitives","@deepseek-ai/dsh-client-ui-slots"]}`; - 设置面板出现「数据备份」分区,页面完整渲染:备份路径(回填真实 `C:\Users\<用户名>\dsh_backups`)、间隔下拉、保留份数、五个分类勾选(已选 3/5)、 真实备份列表(`dsh-backup-20260929-183613`,验证通过,145.9 MB,29400 文件)、 下载菜单(含 per-category 与 manifest 四项)、GitHub 卡(令牌未配置提示); - 点「立即备份(3 个分类)」→ 真实创建 `dsh-backup-20261011-163402`(102.6 MB / 18404 文件, 即时验证通过),列表由 1 份刷新为 2 份; - 点「删除」→ 按钮切换为「确认删除」(保留策略允许删非最新),二次确认后仍未删除是 **最新备份保护**(`force` 才可删),属预期行为; - 验证完成后删除该测试备份、停用临时实例、移除临时 profile 与安装副本。 **测试保真度提升**:新增的 peer 窗口用例直接锁住 0.2.0 预检的通过条件; 把范围改回精确锁时该用例立即失败(已实测)。 ## 6. 未覆盖 / 已知限制 - **浏览器页面仍未做视觉核对(旧记录)**:0.1.4 时的排版与深色/浅色观感是靠代码比对 设计系统 token 保证的。0.2.0 轮已在桌面端真实 GUI 中过目(见 §5.5);如要复核, 打开 **设置 → 数据备份** 再确认一次卡片圆角与描边、备份行的「下载」菜单、 GitHub 卡的开关与 2×2 字段排布即可。 - **GitHub 推送/拉取未对真实仓库执行**:没有可用的仓库与令牌。已验证"未启用"、 "无令牌"两条前置拒绝路径与参数校验,请求构造以 Contents API 为准;首次真实推送后 建议核对远端目录结构与 `manifest.json`。 - **`sessions` / `storages` 分类未在真实 home 上跑**:真实数据测量只跑了 `settings` / `plugins` / `skills`;这两个分类的遍历与恢复由夹具测试覆盖。 - **下载路由不做登录校验**:票据本身是一次性凭证(24 字节随机、120 秒、消费即失效)。 请保持 Web 服务只监听回环地址。 - **未在非 Web profile 验证**:无 `webServer` 时插件按设计只关闭"下载",其余功能不变; 这一分支未在真实非 Web profile 上执行。 - **备份行的危险操作仅靠颜色区分**:删除按钮在二次确认时改为危险色,但恢复 仍是普通 outline;若误触风险偏高,后续可考虑把恢复/删除一并移入菜单。 ## 7. 复现步骤 ```powershell cd npm test node --check index.js dsh plugin --profile web add file: dsh --profile web --dump-config | Select-String backup curl.exe -s -i "http://127.0.0.1:3080/dsh-plugin-backup/download" node test/local/measure-real-home.local.mjs settings plugins skills ``` 桌面端 0.2.0 的复现与核对(用桌面端自带的 CLI,即 0.2.0-rc.2 运行时): ```powershell $dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" # 安装(应被放行并把包名写进 dsh.profile.bundles) & $dsh plugin --profile desktop add file: & $dsh plugin --profile desktop list # 独立实例真实验证(不会动用户正在用的 19387 实例) & $dsh plugin --profile scratchweb --from-default-profile web add file: & $dsh --profile scratchweb --dump-config | Select-String -Context 0,16 dsh-plugin-backup & $dsh --profile scratchweb --no-open --port 19399 # 另开窗口;打开输出里的带 token URL curl.exe -s -i "http://127.0.0.1:19399/dsh-plugin-backup/download" # 收尾:删掉临时 profile(其安装副本随之移除) Remove-Item -Recurse -Force "$env:USERPROFILE\.dsh\profiles\scratchweb" ``` 然后在 GUI 中打开 **设置 → 数据备份**:改一次备份路径与间隔并保存(0.1.4 起**不应再白屏**, 应出现「设置已保存。」且列表仍在)→ 立即备份 → 对新备份用「下载」菜单取某一分类 → 验证 / 仅预览恢复 / 恢复 / 删除。