Files

461 lines
47 KiB
Markdown
Raw Permalink Normal View History

# 工作日志 · 2026-09(第 8 片)
> ⚠️ **本目录日志已按【月】分片**(2026-09-15 用户定,单文件 ≤50 KB):同月续片 `2026-09.md` / `2026-09-下.md` / `2026-09-下2.md` / `2026-09-下3.md` / `2026-09-下4.md` / `2026-09-下5.md` / `2026-09-下6.md` / `2026-09-下7.md` / `2026-09-下8.md` / `2026-09-下9.md` / `2026-09-下10.md` / `2026-09-下11.md` / `2026-09-下12.md` / `2026-09-下13.md` / `2026-09-下14.md` / `2026-09-下15.md` / `2026-09-下16.md` / `2026-09-下17.md` / `2026-09-下18.md` / `2026-09-下19.md` / `2026-09-下20.md` / `2026-09-下21.md` / `2026-09-下22.md` / `2026-09-下23.md`
> **写入约定**:一律 append 到**当月最后一片**;该片超 50 KB ⇒ 新建 `2026-09-下N.md`;⛔ **不要再按日新建 `2026-09-DD.md`**。
> **覆盖来源**:2026-09-12.md
> **上一片**:`2026-09-下6.md` **下一片**:`2026-09-下8.md`
---
## N. 更正:平台**确实**实现了「失败自动禁用插件」—— M 段结论错误(14:34)
**用户追问**:「之前不是说做了 失败标记为禁用的功能吗」→ **用户是对的,我错了**。
### 重新核实的代码证据(`src/web/routes/business-plugins.ts`,门户启用 API `POST /api/plugins/mine/apply`)
| 行 | 内容 |
|---|---|
| `:498-520` | **档案 34 快照机制** —— `SNAPSHOT_FILES = ['package.json','pnpm-lock.yaml','cordis.patch.yml']` + `snapshotProfile()` / `restoreProfile()` |
| `:566-567` | 应用前先 `snapshotProfile(dir)` |
| `:604-609` | 整批先试一次 → `restartProbe()`(即 `restartAndProbe`,定义在 `orchestrator.ts:193`) |
| **`:612-629`** | **探活失败 → `restoreProfile` 回滚 → 逐插件隔离:每个单独 install + restartProbe,失败者 `setStage("插件 X 与实例不兼容,已自动禁用")` + `uninstall(X)` + `audit('plugin_incident')`** ← **这就是"失败自动禁用"** |
| `:630-632` | 隔离循环后**再确认一次**实例能起 |
| `:649-657` | 兜底:任何异常都 `restoreProfile` + 重启,**绝不让实例停在半应用状态** |
### 正确结论
| 机制 | 是否存在 | 触发条件 |
|---|---|---|
| crash 熔断 | ✅ | 任何实例崩溃(**只止损**) |
| **「探活失败 → 自动禁用 + 回滚」** | ✅ **确实做了(档案 34)** | **仅当走门户「功能管理」启用 API** |
**本次事故的缺口不是"功能没做",而是"被绕过"**:anysearch 由 `ensure-anysearch-admin.cjs` **脚本直铺**(`grep -c probe` = **0**),**完全没走 `/api/plugins/mine/apply`** → 探活 / 隔离 / 自动禁用**没有机会执行** → 实例崩到熔断。
**改进建议(修正版)**:① **让 `ensure-*.cjs` 直铺脚本复用门户这套链路**(或至少铺完跑一次 `restartAndProbe`,失败则 `restoreProfile`);② **收敛入口**:profile 变更统一走 API,禁止脚本绕过;③ 插件安装前做**与当前 dsh 版本的 import 兼容性预检**。
**⚠️ 方法论教训(写给自己)**:上轮我**只读了 `crash-policy.ts` 就断言"平台没有这个机制"** —— 典型的"查了半个证据链就当成全部",被用户一句反问推翻。**凡"有没有某功能"的判断,必须搜到调用方(不能只搜定义),且至少覆盖两条可能路径(自动触发 / API 触发)。**
### N2. 「为什么没生效」的精确答案(14:36)
`ensure-anysearch-admin.cjs` 主流程(尾部实测):**写 key → 改 patch → `pnpm add` → `reconcileBundles`(把包加进 bundles,`:119-129`)→ `systemctl stop` 实例 → done**。
对照门户 API 的同名步骤:
| 步骤 | 门户 API `/api/plugins/mine/apply` | `ensure-anysearch-admin.cjs` |
|---|---|---|
| 装 + 对齐 bundles | ✅ `pnpm add` + `reconcileBundles` | ✅ **完全相同**(同一函数) |
| **让新 bundle 生效** | **`restartAndProbe`(启动 + 探活)** | **`systemctl stop`(只停,等下次访问自动拉起)** |
| **失败判定** | **探活失败 → `restoreProfile` + 逐个隔离 + `uninstall` + "已自动禁用"** | **无**(`grep -c probe` = 0) |
| 异常兜底 | `restoreProfile` + 重启 | 仅 `console.log('✗ 安装失败')` |
⇒ **判定的前置条件从未出现**:没有任何一步"启动并探活",所以"探活失败"永远不会发生 → **回滚 / 隔离 / 自动禁用无从触发**。脚本自己在注释里写着「**下次访问自动拉起,新 bundle 才生效**」—— **把验证推迟给了用户的下一次访问**,而那条路径只有 `crash-restart`(只止损)+ 熔断(**不禁用**)。
**一句话**:**两套路径装插件的方式一模一样,差别只在"怎么让它生效"—— 门户是"起+探活+验",脚本是"停+等下次",而"验"的那半恰恰是全部安全网所在。**
### N3. 修复方案分析(14:38,**待用户定方案**)
**技术前提(实证)**:
- `restartAndProbe`(`orchestrator.ts:193`)的能力**完全在编排器进程内** —— `this.launch(userId, …)` / `this.restartMain` + 轮询 `mains.status` / `launchToken` + 2.5s 稳定窗口。
- **所有 launch 入口都要认证**:`/api/dsh/enter` 是 `preHandler: requireAuth`(`dsh.ts:168`),`launch` 调用点在 `dsh.ts:75`(同一 handler 内)。
- ⇒ **root 侧脚本无法用 HTTP 触发起实例** ✗(这是方案 A 的先决条件不成立)
**三个候选方案**:
| 方案 | 改什么 | 覆盖面 | 代价 / 阻碍 |
|---|---|---|---|
| **A 只补脚本** | `ensure-*.cjs` 铺完后主动"启动+探活",失败则回滚 | 仅"脚本直铺"这一条路径 | **先决条件不成立**:脚本调不到 launch;要么给平台加"仅本地可调"的内部端点,要么脚本自己复刻 launch(易漂移) |
| **B 编排器自愈(推荐)** | 崩溃处理里**归因"插件树加载失败"类错误 → 自动把肇事插件从 profile 摘掉 + 重启** | **所有路径**(门户 / 脚本直铺 / 手工编辑) | 改 TS → **编译 + 重启 `dshs`**(**R8:会中断当前在线用户**) |
| **C 门户 API 化** | `ensure-*.cjs` 改为"铸造临时会话 → 调 `/api/plugins/mine/apply`" | 脚本路径 | 需实现会话铸造;完全复用现有逻辑 ✓ |
**推荐 B**:唯一覆盖"所有插件装法"的方案;与 `restartAndProbe` 同层、可复用错误归因;**本次事故若 B 在位,admin 会被自动摘掉 anysearch 并恢复**。代价是必须重启服务(当前有 guest 实例在跑)→ **R8 得先定时段**。
**可与 T04④ 那批 13 项未提交(已含 `orchestrator.ts` / `ensure-*.cjs`)合并落地**,避免重复重启。
### O. 方案 B 落地:编排器「插件加载失败自愈」(14:45-14:51)
**实现**(`src/supervisor/orchestrator.ts`):
- **新增 `tryHealPluginFailure(userId, lastError)`**:从 `failed to import loader entry \S+ \(([^)]+)\)` 解析肇事包名(含包名形状校验)→ 从 profile 的 `package.json` 的 `bundles` 摘掉 → 清 `cordis.patch.yml` 里 `name: "<pkg>"` 条目(并回退删相邻的 `- id:` 行)→ **只动 profile 文本文件,不碰 node_modules**。
- **`scheduleCrashRestart` 开头调用**:命中则 `crashLog({ event: "plugin-auto-disabled", userId, plugin })` + `resetCrashState(userId)` → 给一次干净重启(不必等 5 次重试后熔断)。
**过程中踩的两个坑**:
1. **服务器与本机镜像不同步**(服务器有 06:48 未提交的 TS 改动)→ **不能整文件覆盖** → 改为**锚点精确插入**(先核对锚点文本 + `existsSync/readFileSync/writeFileSync`、`join`、`userRoot` 均已 import 才动手)。
2. 首次插入**漏删锚点里的原行** → `const history = this.crashHistory.get(userId) ?? []` **重复一行** → `error TS1005: ',' expected` → 去重后 `tsc` 通过。
**⚠️ 连带生效(必要副作用,必须记)**:`lib/` 停在 **01:28**,而 `src/{orchestrator,proxy}.ts` 是 **06:48** → 本次 `npm run build` **让那批 8 小时前未编译的改动一起生效**(正是 T04④ 记录的"服务器 13 项未提交"里的两个核心文件)。已留**全量回滚点**:`/root/lib-backup-20260912-144605.tgz` + `/root/orchestrator.ts.bak-*`。
**重启与验收**:`systemctl restart dshs` → **`portal HTTP 200`** ✓、监听 3080 ✓、日志无 error ✓(重启瞬间的 `HTTP 000` 只是"尚未监听",约 3 秒后正常)。重启前在线实例 2 个(guest + admin)→ 按 R8 已先向用户说明并获授权。
**遗留**:① **本机镜像的同一改动内容与服务器版不一致**(本机版含单引号/反引号分支,服务器版已简化为双引号分支)→ 需对齐;② **自愈逻辑尚未做"真实坏插件"端到端验证**;③ 编译连带把别人的改动推上线,需观察是否引入新行为。
### P. 创建「遗留项自动推进」自动化(14:52)
**用户诉求**:每次都要手动开新会话才能接着处理遗留项,希望能自动推进。
**能力边界(已如实告知)**:
- ✅ automation **能做到**:定时起一个**独立会话**,读「`交接单/README §一` + 当日日志遗留段」→ 自动做低风险项 + 汇总需决策项;
- ❌ **做不到**:接管/续接那个**已中断的会话** —— 自动化会话是全新且自包含的,prompt 必须写清"读哪些文件、做什么"。
**已创建**(用户选定范围 = **只做低风险自愈**):
- id `1e1db4eb-aba1-423c-a0d3-34a94c265d6b`|名称「遗留项自动推进(DSH 平台)」|cwds = `D:\AI技能\aliyun-dsh-server`
- **调度**:`FREQ=HOURLY;INTERVAL=8`(**≈每天 3 次**)
- ⚠️ **RRULE 实测约束**:**`BYHOUR` 不接受逗号列表**(`BYHOUR=9,14,21` 报错 "BYHOUR must be an integer between 0 and 23")→ 要表达"一天多次"只能用 `FREQ=HOURLY;INTERVAL=n`;**只有 BYDAY / BYMONTHDAY 支持逗号**。
- prompt 含:两处遗留来源、可自动做的判定标准(本机+可逆+零生产影响)、**只汇总不执行**的需决策项、五条红线(不 commit/push、不批量写、不重启服务、先跑 handoff-guard 看锁、共享文档只 Edit)、汇报格式。
**⚠️ 本次提问被系统拦截过一次,值得记住**:把"运行频率"作为选项上抛给用户 **不合规** —— 额度消耗/调度频率属**技术实现项,有客观优劣可判,应由 AI 自主决定**;"只汇总不执行"也不该作为备选项给出。**今后凡"技术实现/资源调参"类决策,自己定并说明理由,不进提问。**
### Q. 释放 T03 占用锁(14:55)
**用户提醒**:「有标记任务锁吗 结束任务 需要解除锁定」。
**释放前状态**:本会话持有 `交接单/.doing-T03`(OWNER = `exec-session-B(当前会话,接管自 exec-session-A)`)。
**⚠️ 关键判断:T03 未完成,不能直接 rmdir** —— 否则接手者会误以为要从头做。所以**先把状态写成"可接手",再释放**:
1. `交接单/README.md §一` 的 T03 行:🔄 执行中 → **⏳ 待续做**(明确"步骤 1–9 已完成 / **14:55 释放锁** / 接手者**直接续做、勿从头来**"),占用者改 `—`(已释放);
2. `T03-*.md` 头部状态行:改为「⏳ **待续做** —— 步骤 1–9 已完成(建包 / host 面 / client 合并 / P0 适配 / 打包 / 扫描 / smoke 全过),剩:上传候选池 → `guest` 启用 → 启动实例 → §八验收 → 归档;已释放占用锁」;
3. `bash scripts/handoff-guard.sh --release T03` → **✓ 已释放**。
**验证**:`ls -d 交接单/.doing-*` → **无锁** ✓;guard【1】→ **✓ 无人占用** ✓。
**顺带发现**:`handoff-guard.sh` 已被(并行会话)更新,**新增【1c】全局执行锁(`交接单/.exec-lock`)** —— 当前「✓ 无全局锁(没有执行会话在跑)」。该机制与 T04④ 那批未提交改动同批落地。
## 调研:平台联网搜索现状 + anysearch 接入可行性(10:10-10:45,只读)
**用户问**:「dsh 服务器是否配置网络搜索功能、用的是什么搜索、现在没有地方能管理这类功能、能否安装 anysearch 的搜索服务」。
**取证方式**:本地 dsh 源码 `D:\github\deepseek-harness`(docs + packages/web)+ 平台代码 `D:\github\dsh_shenxian` + **服务器只读 ssh**(未改任何东西)。临时探测文件已清理。
### A. 现状:已配,用 DeepSeek 官方搜索
服务器 `@deepseek-ai/dsh` = **0.1.2-rc.1**,`/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/` 下 web 相关包 = `dsh-web` / `dsh-tool-web` / `dsh-web-search-deepseek` / `dsh-web-fetch-http` / `dsh-web-app` / `dsh-web-frontend` / `dsh-host-webserver`。
**搜索 provider 只有 1 个**:`dsh-web-search-deepseek`(id `deepseek-official`)→ 即**用 DeepSeek 官方联网搜索**,不是自建爬虫。官方另有 `web-search-exa` / `web-search-perplexity` 包,**本部署未装**。
- 机制:POST `https://api.deepseek.com/anthropic/v1/messages`,Anthropic 兼容格式 + 原生 `web_search_20250305` server tool(`max_uses` 默认 5);复用 `DEEPSEEK_API_KEY`(**不**复用 `DEEPSEEK_BASE_URL`)。
- 平台在 `orchestrator.ts#baseEnv` 每用户注入自己的 key(`…(apiKey !== null ? { DEEPSEEK_API_KEY: apiKey } : {})`)。
- **成本特征:一次搜索 = 一次模型 turn**(比专用搜索 API 贵)。
- 实测可用(档案 37a:guest 会话 `web_search` 2 次、`web/deepseek-search-llm-request` 事件 3 次)。
### B. 「没有地方管理」的真相 = 平台 patch 主动隐藏,不是没做
dsh **自带**管理入口:「设置 → 插件 → 插件配置 → Web search」(改 endpoint / model / apiKeyEnv / maxUses)。
但 `ensure-role-profile-patch.cjs` 对**非 admin** 把 4 个设置 UI 插件 `disabled: true`:`ui-settings-models`、**`ui-settings-plugins`(← 就是它)**、`ui-settings-plugin-inventory`、`ui-cordis`(理由:148 个官方插件都是运行骨架,用户禁用任一都可能搞坏实例)。
**实测印证**:guest 的 `profiles/web/cordis.patch.yml` 含禁用块;**admin 的 patch 里没有** → **admin 在实例内能改搜索配置,普通用户看不到**。门户侧则**确实没有任何 web search 管理面**。
→ 结论:不是缺失,是「只给了 admin,且藏在 dsh 原生设置页里」。普通用户想调搜索,目前无路径。
### C. anysearch 能装,且有官方插件
- **`@anysearch/anysearch-dsh`**(AnySearch 官方 anysearch-team 维护 / MIT / npm 最新 **0.1.4**,实测 registry 元数据确认 `dsh.bundle.patch: ./cordis.patch.yml` = 标准 dsh 插件):同时提供 `ctx.web` 的 **search + fetch** provider(id `web-search-anysearch`)+ 高级工具 `anysearch_capabilities` / `anysearch_search` / `anysearch_batch_search`;**无 key 可用**(匿名配额),配 key 后 1000 次/天;凭据 `ANYSEARCH_API_KEY`。另有第三方纯 provider 版 `dsh-web-search-anysearch`(插件市场有 L5 运行验证)。
- **接口极简**(源码实证):`WebSearchProvider = { id, available(): boolean, search(req, signal) }`,注册 = `ctx.web.registerSearchProvider(...)`(Cordis 插件,`inject = ['web']`)。**自研 provider 约 100 行**,`packages/web/web-search-deepseek/src/provider.ts` 可直接当模板。
- 平台投放路径现成:门户 `plugins.html` 上传 tgz → 候选池 → 实例「功能管理」批量启停。
### D. 四个必须先解决的坑(未动手,仅记录)
1. **P0 provider 歧义**:装上后 `ctx.web` 有 **2 个可用 search provider** → dsh 选择规则是「无显式 `searchProvider` 且有多个可用 → `WEB_PROVIDER_AMBIGUOUS` **报错**」(**不会自动选一个**)→ 必须同时显式设 `web.searchProvider`,否则**连原有搜索一起挂**。
2. **P0 凭据归属**:普通用户看不到插件配置页 → 无法自助填 key,须平台侧注入(`baseEnv` 加 `ANYSEARCH_API_KEY` 或写实例 `.credentials.yaml`)。全租户共用一把 key = 配额/账单耦合 + 外泄面(与档案 23 已记的 `DEEPSEEK_API_KEY` 同类问题)。**匿名模式看似零成本,但所有实例同 IP 出网 → 1000 次/天配额是全体共享**,要先想清楚。
3. **P0 fetch provider 一并被替换**:anysearch 插件同时接管 fetch。dsh 现有 `dsh-web-fetch-http` 是**本地实现且带 SSRF 防护**(拒非公网地址、逐跳校验重定向)→ 换成 AnySearch Extract = 抓取外发第三方、防护逻辑改变。须明确**只换 search、保留本地 fetch**。
4. **P1 内存**:每实例 `MemoryMax` 384 MiB(档案 58),多一个常驻 provider 的增量需实测(用 `scripts/probe-instance-mem.cjs`)。
**未做**:未装、未改、未 scp、未重启、未 commit(纯调研,符合规划会话边界)。
## 调研(续):B 方案细化 + 前置验证全绿(10:16-11:05,只读取证)
**用户指令**:「B 方案,能否优先使用这个 [AnySearch API Key]」→ 进入 B 方案落地准备。
**新增事实(本地 dsh 源码 + 插件包 + 服务器只读 ssh)**:
1. **插件包已下载分析**:`@anysearch/[email protected]`(168 KB / 33 文件,MIT,预构建 `lib/*.js`)。**自带的 `cordis.patch.yml` 直接解决了 provider 歧义** —— 它显式写 `- id: web / config: {searchProvider: anysearch, fetchProvider: anysearch}` + `insert: web-search-anysearch`(`apiKeyEnv: ANYSEARCH_API_KEY`)。**但同时也把 fetch provider 换成 anysearch**(本地 `dsh-web-fetch-http` id 是 **`http`**,带 SSRF 防护,会被替换)。
2. **`available()` 不校验 key**(`endpoint(baseURL,'/v1/search') !== undefined`)→ 装上后 search registry = {deepseek-official, anysearch} 两个可用,**必须靠显式 patch 消歧**(插件自带的那段)。
3. **安全审查 P2 通过**:`lib/` 无 `child_process` / `eval` / `new Function` / `require()` / `process.env` / 文件写 / `chmod`,唯一外联 `https://api.anysearch.com`。
4. **前置验证 4 项全绿**:① key 本机有效(HTTP 200 `code:0` 1.4s)② 服务器网络可达(拿到 request_id;DNS 走 CloudFront)③ **服务器+key 联合可用**(HTTP 200 `code:0` 2.6s)④ 包安全。
5. **踩坑记录**:本机 `Invoke-RestMethod` 打该端点**必超时**(两次),换 `curl` 正常;PowerShell 传参给 ssh 会破坏内层双引号 → JSON body 用 `$(printf '{\042query\042:...}')` 八进制引号绕过;`base64 -d | bash` 会被安全策略拦。
6. **平台铺功能插件的官方姿势**(`scripts/ensure-biz-plugins.cjs`):tgz 暂存 `<home>/.dsh-stage/`(chmod 444 + chown uid)→ `setpriv --reuid <uid> --regid <uid> --clear-groups env HOME=<home> pnpm add --store-dir <沿用旧> --cache-dir <...> file:<staged>` → `reconcileBundles`(deps 里带 `dsh.bundle.patch` 的进 `pkg.dsh.profile.bundles`)→ 停 `dsh-<uid>-*.scope`。产物目录 `/opt/dsh/artifacts/`;候选池 tgz 在 `/var/lib/dshs/business-plugins/`。
7. **凭据文件格式**(`<home>/.credentials.yaml`,600,用户属主):`version: 1` + `records: {client-connection/browser-session: {kind: grant, payload: {version, secret}}}` —— **写入须合并,不能覆盖**。
8. **风险点**:profile 的 `package.json` 带 `patchReload: live` → bundle patch 有热加载可能 → **执行顺序必须「先写 key 再装插件」**,避免"provider 已切换、key 未配"的窗口。
**产出**:档案 **`04-调整方案/64-接入AnySearch搜索provider.md`**(现状 + 前置验证 + 插件分析 + 3 个必答点 + 执行步骤 S0-S5 + 回滚 + 红线)。**3 个必答点中 2 个待用户拍板**:①【P0】key 给谁用(全平台统一 / 仅 admin / 接受外泄面)②【P0】fetch 是否一并换 AnySearch(建议保留本地 `http`)③【P1】投放方式(建议平台级统一铺,同 portal-entry 姿势)。
**状态**:⏸ 前置全绿,等用户确认后执行。tgz 暂存 `_tmp_anysearch/anysearch-dsh-0.1.4.tgz`。
**未做**:未装、未传、未写凭据、未重启、未 commit。
## 执行:AnySearch 接入 admin 实例(B 方案落地,10:30-11:25)
**用户裁决 3 点**:① **分两步走**(先只装 admin 验证,通过后再铺普通用户)② **保留本地抓取**(只换 search)③ 现在就执行。
**已完成(仅 admin `cce6d1cd…`,uid 114801)**:
| 步 | 结果 |
|---|---|
| 传包 | `/opt/dsh/artifacts/anysearch-dsh-0.1.4.tgz`(168851 B) |
| 凭据 | `refs.ANYSEARCH_API_KEY` 写入 `<home>/.credentials.yaml`,**600 + uid 114801** |
| patch | `<profile>/cordis.patch.yml` 加 `platform: anysearch-search` 覆写段 |
| 依赖 | `@anysearch/anysearch-dsh` 装好(`setpriv` + 沿用旧 store,ws 保持干净) |
| bundles | 6 项,含该包 ✅ |
| 停实例 | **0 个** —— 执行时 admin 实例未运行,下次访问自动拉起新配置 |
**载体**:新建 **`dsh_shenxian/scripts/ensure-anysearch-admin.cjs`**(仿 `ensure-biz-plugins.cjs`;支持 `--apply` / `--restart` / 干跑 / 指定用户名;key 从 `ANYSEARCH_API_KEY` 环境变量读,不落盘)。
### 关键实测发现(推翻了我自己的假设,务必记住)
> **profile 层 `- id: web / config:` 对 `dsh-web` 是「整体替换 config」,不是字段级合并。**
第一次只写 `fetchProvider: http` → **把插件自带 patch 的 `searchProvider: anysearch` 也冲掉了**(dump-config 里该条目只剩 `fetchProvider`)。这会上线即坏:search registry 上 `deepseek-official` 与 `anysearch` 都 `available()=true`,又无显式选择 → **`WEB_PROVIDER_AMBIGUOUS` 报错(不是自动选一个)**。
**修正**:覆写块必须两个字段写全(`searchProvider: anysearch` + `fetchProvider: http`);脚本 `ensurePatch` 已改为**幂等整块替换**(新增 `updated` 动作)。
### 验证(全绿,除端到端)
`dsh --profile web --dump-config` 实测 → `web.config = {searchProvider: anysearch, fetchProvider: http}` + `web-search-anysearch` 条目已注册;凭据文档通过 dsh 严格解析(无 warning);权限 600 + uid;服务器侧 curl `HTTP 200 code:0 2.6s`。
**⏳ 未做**:实例内实跑 `web_search`(需 admin 首次访问实例时确认)。
**附注**:dump 里 `tool-web` 带 `disabled: true`(来自 `@deepseek-ai/dsh-web-app`)—— **既有状态**,非本次引入。
### 回滚 / 待办
- **回滚**:profile package.json 摘 `@anysearch/anysearch-dsh`(deps + bundles)→ 停实例;删 `.credentials.yaml` 的 `refs.ANYSEARCH_API_KEY`;删 patch 的 `anysearch-search` 标记块(备份 `<profile>/cordis.patch.yml.bak-anysearch`)。
- ⚠️ **`.dsh-stage/anysearch-dsh-0.1.4.tgz` 不可删** —— profile 的 `file:` 依赖指向它,删了会重现档案 63 的断裂依赖。
- 含密钥的 `.credentials.yaml.bak-anysearch` 已在验证后**删除**。
- **待办**:① admin 端到端确认 → ② 铺普通用户(`--apply --restart guest`)→ ③ 观察配额。
- **未 commit**(红线):`dsh_shenxian` 工作区有 1 个新文件(`scripts/ensure-anysearch-admin.cjs`)+ 文档库档案 64 未提交。
## 核实:候选池「用户自助启停」机制(10:38,只读代码)
**用户问**:插件能否由 admin 导入「插件管理」候选池、用户自己控制启用/禁用。
**核实结果(读 `src/web/routes/business-plugins.ts`)**:
- 机制**确实存在**(档案 16 三层模型,已实施):admin 门户上传 tgz → `business_plugins` 表 → 用户在实例「功能管理」勾选 → `/api/plugins/mine/apply` → 重启实例。
- **禁用 = `pnpm remove <id>` + `reconcileBundles`(L399-402)** —— **真卸载**。
> ⚠️ **`04-调整方案/16-…md` §7.1-5 写的「禁用 = 保留包 + cordis patch `disabled: true`(不删 node_modules)」与现实现不符** → 该条需修订。**已报告,未擅自改档案 16**(R7)。
- **推论(重要)**:AnySearch 若改走候选池,用户一禁用就会:bundle 移除 → 插件自带 patch 的 `searchProvider: anysearch` 不再加载,而 **profile 覆写段仍写死该值** → 指向未注册 provider → **`WEB_PROVIDER_CONFIGURED_MISSING` 报错(不回落)**,且用户自己修不了。
- **三种配置策略都有坏格子**("两 provider 共存 + 用户可启停"在 dsh 语义下无解):写死→禁用即 MISSING;什么都不写→共存即 AMBIGUOUS;禁掉 deepseek→禁用 anysearch 即 UNAVAILABLE。要优雅解决需**平台在 apply 时同步维护 web 配置**(属平台改造)。
**产出**:档案 64 新增 **§九**(两条路差异 + 禁用即坏的证据 + 三策略对比表 + 结论建议:保持平台级直铺;若要自助启停须先补平台能力)。
## 开发:功能插件启停与 web provider 配置联动(档案 65,10:45-11:05)
**用户决策**:AnySearch **改走候选池统一机制**,"有问题解决问题"。
**做了什么**:改 `src/web/routes/business-plugins.ts`(**单文件**)
- 模块级导出 `collectWebProviderClaims()` / `syncWebProviderPatch()` + 3 个 marker 常量(刻意不放进闭包,便于被真实产物验证 + 后续加单测)
- `install()` / `uninstall()` 里在 `reconcileBundles(dir)` 之后各加一行 `syncWebProviderPatch(dir)`
**核心机制**:插件启用 → 托管段写 `searchProvider: <插件自带 patch 的声明>` + `fetchProvider: http`;插件禁用 → **整段删除**(回到"只有一个可用 → 自动选")。**平台策略:`fetchProvider` 恒为本地 `http`,忽略插件对 fetch 的声明** —— 这是「保留本地抓取 + SSRF 防护」的落地位置,以后接别的 provider 插件自动适用。
**自检与验证**:`typecheck`/`build` 均 exit 0;用**编译产物 + admin 真实数据**在 `/tmp` 副本上验四态(启用 / 禁用 / 幂等 / 无残留)**全绿** —— 启用态自动清掉了我早期的手工直铺段(**迁移顺带完成**),禁用态托管段整段消失。
**可复用技巧**:验证生产逻辑时**不要覆盖生产文件** —— 把编译产物以 `xxx.new.js` 名义放到**同目录**(相对 import 才能解析),用一次性脚本在 `/tmp` 副本上跑真实数据,用完即删。
**状态 / 待办**:① **待部署**(`lib/` → 服务器 + 重启 `dshs`,R8 待用户确认窗口)② 投放候选池 ③ **退役 `scripts/ensure-anysearch-admin.cjs` 的手工覆写职责**(否则与平台托管段并存、后者覆盖前者 → 难察觉的配置漂移)④ admin 在「功能管理」里启用/禁用各验一次。
**未 commit**。
## 功能开发:业务插件 P0 误报 → admin 显式信任(档案 66,11:05-11:25)
**背景**:投放 AnySearch 被安全检测 **P0 阻断** —— 规则 `/\.credentials\.yaml/` 是**纯字面量匹配、扫所有文本文件**,第三方插件的 README 只要教用户"把 key 写进凭据文件"就被判「读取实例会话密钥」。按 P0 全集扫该包:**命中 4 处全是 `.md` 文档、代码文件 0 命中**。(第二次同类误报,档案 29 记过 `dsh-memory` 的 example.yml。)
**用户方案(比我提的"收窄规则"更优)**:让 admin 在门户手动标注信任 —— 不动防线、把判断权交给本就可信且能看到证据的 admin。
**实现(3 文件,P0 规则一行未动)**:
- `security-scan.ts`:抽出 `walk()` 共享遍历;新增 `scanDirDetailed()`(**收集 P0 不抛**);**`scanDir()` 语义完全不变**(技能上传等路径仍"命中即 400")。
- `business-plugins.ts`:`StagedPlugin` 增 `blocked`/`warnings`;`stageTgzArchive()` 改用收集模式;POST 接受 `trust.confirmed` —— 缺省 **fail-closed 409 + 逐条回显**,声明信任后放行并写两条 audit。
- `portal.html`:抽出 `doUpload(file, trust)`;新增 `renderScanBlocked()` 展示命中详情 + 信任理由输入 + 确认按钮。
**验证(部署后实测)**:不带信任 → 拒绝并列出 4 处命中(exit 2);带 `--trust` → 放行;audit 两条(`upload_business_plugin{trustedOverride:true}` + `trust_business_plugin{blocked:[4],reason}`);候选池已含 `@anysearch/anysearch-dsh @ 0.1.4`;admin 的 bundles 含它 → 门户「功能管理」显示已启用。
**部署**:3 个文件(`business-plugins.js` / `security-scan.js` / `portal.html`)+ 重启。**这次重启只用了 3 秒**(上次 90 秒是因为要 drain 运行中的实例)—— 说明重启耗时取决于当时有没有活跃实例。
**可复用手法(部署前基线校验)**:用 **git blob hash**(服务器 `git hash-object <file>` vs 本机 `git rev-parse HEAD:<file>`)判断"服务器上的工作区文件是否就是我的改前基线"——**比 md5 可靠**(不受换行 / 编码影响,前几轮就因此误判过一次)。
**待办**:① `whitelist/import`(官方目录批量导入)路径**未接信任入口** ② 信任状态未持久化(建议与「默认开启」合并成一次 migration v6)③ `scripts/ensure-anysearch-admin.cjs` 的手工覆写段待退役。
**未 commit**(红线)。平台侧本轮共改 3 文件 + 新增 2 个脚本(`ensure-anysearch-admin.cjs` / `ensure-anysearch-pool.mjs`)。
## 核实:实例内「功能管理」section 的 UI 现状(11:48,只读)
**用户问**:之前在**已删除的会话**里提过"设置中「功能管理」页 UI 交互太简陋、没参考 UI 规范、插件没重点显示中文说明",让我核实是否成立。
**结论:两点都成立。** 全程只读,未改任何代码。
### 1. UI 没按规范 —— 成立
`06-工作台UI规范.md` **L5 明确适用范围含"其 client 面 section"** → 「功能管理」在约束内,无争议。逐项对照 `poc/business-plugins/lib/client.js`:
| 规范 | 现状 |
|---|---|
| 正文 15-16px | 容器 **13px** |
| 次要 13-14px | 副行 / 徽章 **11px**、按钮 12px(**低于规范字号阶梯下限**) |
| 空状态:图标 + 标题 17px/600 + 说明 14px + 按钮,padding 56px | **一行 dim 文字** 了事 |
| 按钮 `.btn` 15px / `.btn-sm` 14px | 12px,padding 5px 10px |
| 徽章 `2px 8px` r10 13px | `1px 7px` r9 11px |
| 列表行 hover 反馈 | 无 |
| **L7 强制**加载 `impeccable` + `taste-skill` 做视觉判断 | 无任何痕迹 |
> **一处是对的、应保留**:配色用了官方 `--dsw-*` design token(跟随 dsh 主题亮/暗适配),而非规范里的 `#2f6fed` 硬编码 —— 这是 v0.2.0 的有意决策,不必回退。
### 2. 没重点显示中文说明 —— 成立,且是**三层**问题
- **层 1 · 主次颠倒**:实例内 `name` 是主视觉(13px 正常色)、`description` 是 **11px dim 副行**;而**门户** `portal.html` L483-484 注释明写「主视觉 = 中文说明(一眼知道这插件干什么);插件名 / npm 包名 / 版本退为副行小字」→ **两处做法相反**。
- **层 2 · 数据源是英文**:`/api/plugins/mine` 的 `description` 来自插件 `package.json`(anysearch = `"AnySearch web search and fetch providers plus advanced tools for DeepSeek Harness"`)。
- **层 3 · 中文在导入时被丢弃(这是真 bug)**:官方目录 `awesome-dsh-plugin` **自带双语** `description: { zh?, en? }`(`whitelist.ts` L55 类型定义可证),门户列表页显示的正是中文;**但 import 入库时写的是 `staged.description`(从 tgz 的 package.json 读到的英文),而不是 `entry.description.zh`**(`whitelist.ts` L271-274)→ 中文在这一步丢掉,实例内永远拿不到。
→ **但必须说准确**:目录里**确实有中文**(3455 条全带 `zh`),它只出现在门户「**官方推荐插件**」列表;**导入写进候选池 DB 的是英文**。实测三个插件的 DB 说明**全是英文**,而目录对应条目**全是中文**:
| 插件 | DB(候选池) | 目录(zh) |
|---|---|---|
| `@anysearch/anysearch-dsh` | "AnySearch web search and fetch providers plus advanced tools for DeepSeek Harness" | **「基于 AnySearch 的实时网页与垂直搜索插件,为 DeepSeek Harness 提供搜索工具。」** |
| `@liustack/modlens` | "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI" | **「为纯文本模型架起视觉桥梁:粘贴图片,输出结构化 JSON 证据(OCR、版面、语义)。」** |
| `dsh-univer-office` | "DSH × Univer integration with a bundled collaboration Gateway…" | **「与 DeepSeek Harness 打造一个真正的办公环境。…」** |
→ 定性应为「**中文一直存在,只是导入那一步没取用**」,而非"不存在中文"。实例内「功能管理」读的是 DB 的英文,所以也是英文。
(教训:先给出结论再核实的顺序容易出错;这条结论是在用户质疑后实测修正的。)
## 执行(方案第一步):修复「目录中文说明在导入时被丢弃」(11:55-12:10)
**用户指令**:「按照方案执行」→ 按我提的顺序,先做**零风险、立刻可见**的那一步。
**改动**:`src/web/routes/whitelist.ts` 导入入库那一行 ——
```ts
- description: staged.description, // tgz 内 package.json(英文)
+ description: entry.description !== '' ? entry.description : staged.description, // 目录中文优先
```
**根因确认**:`getIndex()` 建索引时**已经**按 `zh ?? en` 归一(`whitelist.ts` L133 `asString(e.description?.zh) !== '' ? …zh : …en`),所以 `entry.description` **本来就是中文** —— import 却没用它。一行修复。
**顺带回填**:一次性脚本把已入库的 3 条也换成中文,并写 audit(`backfill_plugin_description`)→ 实测 **3 行全部回填成功**(anysearch / @liustack/modlens / dsh-univer-office 的说明从英文变中文)。
**部署**:备份 `whitelist.js`(+`.map`+`.d.ts`,后缀 `.bak-20260912`)→ scp(**md5 一致** `edecf8e1…`)→ 重启(`active` + `HTTP=200`)。回填脚本跑完即删。
**方案后续步骤**:② 实例内「功能管理」UI 按规范重做 —— **已完成,见下节**;③ 「默认开启」(DB migration v6 + 批量铺 + spawn 自动装)④ 官方目录导入路径补信任入口(档案 66 待办 1)。
**未 commit**(红线)。
## 执行(方案第二步):「功能管理」UI 按规范重做 + **发现一个平台 bug**(11:55-12:20)
### UI 重做(v0.2.4,档案 67)
`poc/business-plugins/lib/client.js` + `package.json`(0.2.3 → **0.2.4**;同名同版本改内容必须升版本号,否则 pnpm 缓存复用旧包):
- **信息层次反转**:主视觉改为**用途说明 14px**,包名 + 版本退为副行 12px(与门户 plugins 页同一决策 —— 「一眼知道这插件干什么」)
- 字号对齐规范 §2.2(section 语境取 14/13/12,不照搬页面级 16px 基准);徽章 `2px 8px`/r10/12px;按钮 `.btn-sm` 量级;行距与内边距按 §2.4
- 空态与加载态改「标题 15px/500 + 说明 13px」层次(§4.9),不再是一行灰字
- **新增注入式 CSS** 提供行 hover(§4.3)与按钮 hover(§5)—— 内联样式表达不了 `:hover`,用 `ctx.effect` 注入 `<style>` 并在 disposer 移除
- 新增 locale key `loadingDesc` / `emptyTitle` / `noMatch`(zh/en 同步);搜索占位符改「搜索插件名或用途」
- **保留**:`--dsw-*` design token(跟随 dsh 主题,不回退规范硬编码色)、i18n、探活刷新、rejected 行内提示
**交付**:`npm pack` → 改名 `business-plugins-0.2.4.tgz`(8951 B)→ `/opt/dsh/artifacts/` → `ensure-biz-plugins.cjs --all --restart`。
### 部署结果
| 对象 | 结果 |
|---|---|
| **admin**(uid 114801) | ✅ 0.2.3 → 0.2.4 成功,bundles=6 含该包 |
| **guest**(uid 100002) | ❌ `EACCES: permission denied, open '.../node_modules/.bin/modlens'` |
### ⚠️ 执行中发现的平台 bug(重要,待修)
**guest 的 profile 下有 561 个 root 属主项**(`.pnpm` 555 / `@liustack` 2 / `.bin` 2 / `dsh-univer-office` 1 / 一个 `.bak`);admin 5 个(全在 `.pnpm`)。
**根因**:`src/web/routes/business-plugins.ts` 的 `install()` / `uninstall()` **没有 `setpriv` 降权** —— 以平台进程身份(root)跑 `pnpm add/remove` → 装出来的文件属主是 root → 该用户此后**任何** `pnpm add/remove` 必然 EACCES。
**证据吻合**:`audit_log` 显示 guest 01:58 通过候选池启用 `@liustack/modlens` + `dsh-univer-office`,正是这批文件的来源。
**与档案 43 的关系**:那次的属主自愈**没覆盖候选池这条路径**(平台脚本如 `ensure-biz-plugins.cjs` 用的是 setpriv 正姿,路线不同所以一直没暴露)。
**待确认(R7:561 > 10,须先出清单 + 确认)**:① 短期 chown 561 项解阻塞 ② 长期修 `install/uninstall` 改用 setpriv(并考虑给 apply 路径补一次属主自愈)。
### 附:规范强制技能缺失
`06-工作台UI规范.md` 第 7 行强制加载 `impeccable` + `taste-skill`。**我上一条说"两者在本机不存在"是错的** —— 它们不在 WorkBuddy 的技能目录,而在 **`C:\Users\Administrator\.dsh\skills`**(dsh 侧,用户 12:58 指出)。Skill 工具加载不到,但**可直接读文件使用**;已记进用户级记忆。
**未 commit**(红线)。
## 根治:候选池启停的 root 属主污染(档案 68,12:58-13:35)
**用户指令**:① **始终选择长期最佳策略,避免短期东补西补**(已写进**用户级**记忆)② UI 技能在 `C:\Users\Administrator\.dsh\skills` ③ 其余按决策策略自主进行。
**背景**:上一轮投放 0.2.4 时 guest 失败(EACCES,profile 下 561 个 root 属主项)。
**按「长期最佳」做三层,而不是只 chown 回去**:
1. **修根因**:`business-plugins.ts` 的 `install/uninstall` 改为 `runPnpmAs()`(setpriv 降权)。新增 `wsDir()` / `uidOf()`(取 `<root>/home` 属主)/ `runPnpmAs()` / `healOwnership()`;原来以 root 跑 pnpm 的 `pnpmEnv()` 成死代码,已删(其 HOME 说明移到 `wsDir` 上)。
2. **防复发**:每次改插件**最前面**跑 `healOwnership()`(`find <profile> -user root -exec chown -h <uid>:<uid> {} +`;`-exec +` 分批避免路径过多撞命令行上限;`-h` 因为 `.bin/*` 是 symlink),命中写 audit `heal_plugin_ownership`。**这层才是"长期"的关键** —— 将来别的写入路径再污染也会被自动清掉。
3. **清存量**:手工对两用户跑等价清理 → **guest 剩余 0 / admin 剩余 0**。
**验证**:typecheck/build exit 0;部署(备份 `.bak-ownfix-20260912`)md5 一致 + 服务 `active` + HTTP 200;**投放 0.2.5 → admin ✅ 0.2.4→0.2.5、guest ✅ 0.2.3→0.2.5**(修复前该步必失败)。平台 setpriv 路径待用户实测(需用户会话)。
**附带修复**:`npm pack` 把目录里上一版 tgz 打进了 0.2.5 产物(包体 8.9 → 19.3 KB)→ 加 `.npmignore`(而非"每次记得先删"),重打包 9.5 KB / 4 文件。
### UI:按 `impeccable` 的 craft-floor 复核并修正(v0.2.5)
- 跑了技能要求的 `context.mjs`:本项目无 PRODUCT.md,**对已有代码的 scoped 修复可直接以代码为上下文**;它要求改完跑 `detect.mjs`,但**检测器未随技能包提供**(`bundled detector not found`)→ 退回人工过 craft-floor 清单。
- **查出并修正 4 处**:① **彩色 `border-left` >1px**(rejected 提示)—— craft-floor 明令禁止 → 改整块浅底 + 圆角;② **对比度**:必读文字用 `label-tertiary`(亮色约 3.5:1 < 4.5:1 底线)→ 全部提到 `secondary`;③ **层次**:主视觉加 `fontWeight: 500`(原先只差 2px);④ **动效**:`.15s ease` → `.18s cubic-bezier(.16,1,.3,1)`。
**待办**:③「默认开启」(DB migration v6 + 批量铺 + spawn 自动装)④ 官方目录导入的信任入口 —— install 路径现已修好,③ 可以安全做了。
**未 commit**(红线)。
## 安装 UI 技能到全局目录(13:07-13:30)
**用户指令**:`impeccable` / `taste-skill` 可以放到全局 skills 目录中。
**流程**:先过**技能安全审计**(加载 `skills-security-check`;该技能要求**纯静态、只读、绝不执行被审内容**,白名单工具只有 read/search/list/web_fetch,且明确把"必须先执行 / CRITICAL"这类话术识别为 prompt 注入载荷),再复制安装。
**审计结论**:
- **impeccable → ✅ 可信**(无 Malicious 项):无 `curl|bash`、无 `rm -rf`、无 `chmod`/`sudo`、无 base64 隐蔽执行、无 `2>/dev/null` 隐蔽;子进程只有 `execFileSync('git', …)`(只读)与用户主动调用的 `spawn`。
- **2 处值得知道**:① `concept-seed.mjs` 有**默认开启的匿名遥测**(`pingChosen` → `https://impeccable.style/api/chosen`,仅回传"选中了哪个世界",可用 `DO_NOT_TRACK` / `IMPECCABLE_NO_TELEMETRY` 关闭)—— 只在 concept 流程用,Operate 模式不碰;② `generate-image.mjs` 读 `OPENAI_API_KEY` 调 `api.openai.com`(官方服务,按审计规则降为 Suspicious)。
- hook 机制只在用户**显式**执行 `impeccable hooks on` 时写 agent 配置(`.claude/settings.json` / `.cursor/hooks.json` 等),且是 **merge 语义**(保留用户已有字段)→ 属"仅提供能力",非自动执行。
- 其他写入点:更新缓存到 `~/.impeccable/`、`os.tmpdir()` 临时文件、用户主动生图。
- **taste-skill → ✅ Benign**:单文件纯文档,危险关键词命中全是官方设计系统链接(Fluent / Material / Atlassian / Shopify / Carbon / Primer / GOV.UK),`npm install` 均为教学示例。
- **顺手发现(重要)**:taste-skill 的 frontmatter name 是 **`design-taste-frontend`**,且**自述不覆盖 dashboards / data tables / 多步产品 UI** → **本平台的设置面板/管理页不在它的射程内**,容易误用。
**安装**:复制到 `E:\ProgramData\.workbuddy\skills\`(impeccable **107** 文件 / taste-skill 1 文件);把 md 里指向 `~/.dsh/skills` 的命令示例适配成本机实际路径(1 个文件)。**新技能可能需要新会话才出现在技能列表**。
**未 commit**。
## 修订 UI 规范 + 文档库维护(13:20-13:45)
**用户指令**:认可「两个技能场景不同」的发现,要求**调整要求描述**。
**改动 `06-工作台UI规范.md` 的「UI skill」条款** —— 由"必须**同时**使用 impeccable + taste-skill"改为**按场景分流**:
- `impeccable` → 本工作区绝大多数页面(工具后台 / 设置面板 / 管理页 / 表单 / 空态),用 **Operate** 模式;并注明它**自带的机械检测器未随包提供** → 改为人工逐条过 `reference/craft-floor.md`。
- `taste-skill` → **仅**落地页 / 作品集 / 整站重设计;显式标注它**自述不覆盖 dashboards / data tables / 多步产品 UI**。
- 附两者在 `E:\ProgramData\.workbuddy\skills\` 的实际位置。
- 按规范自身要求追加了同步时间戳(2026-09-12 一行)。
**顺手修掉一个失效指针**:规范头部写的「权威源」路径(`D:\AI技能\mcn-short-video\project\...\工作台UI规范.md`)**已不存在**(实测 glob 无结果;`mcn-short-video` 目录仍在、其下已无该文件)→ 标注「本副本当前即唯一有效版本」,避免后续再去找一个不存在的源。**因此本次未执行"回写源侧 + 回拷"**(源侧不可达)。
**文档库维护**:
- **对账**:`docs-sync-check.sh` 的逻辑用 PowerShell 复现(**本机 Bash 工具不可用** —— 它的 shim 在设 PATH 前就调用 `dirname` 而崩)。结果 **112 一致 / 2 不一致 / 6 仅本地 / 1 仅服务器**:
- 我改的 `06-工作台UI规范.md` **双端一致** ✅
- 5 个「仅本地」= 本次会话新档案 **64-68** → 已 scp 推送 ✅
- 2 个「不一致」= `scripts/handoff-guard.sh`、`交接单/README.md` → **并行会话**的改动,**未动**(只推自己的)
- 1 个「仅服务器」= `INDEX.md.bak-20260911111003.` → 服务器残留备份
- **`docs-audit.py` 抓到我**引入的一处悬空引用**:档案 64 写了"档案 63",而 **63 号并不存在**(只记在当日工作日志的「事故 63」里)→ 改为准确表述后复跑:**`✓ 无悬空档案号引用`** ✅
- **待办**:5 个新档案尚未登记进 `INDEX.md`(本轮未做,本轮已过长)。
**可复用技巧(长期痛点已解)**:PowerShell 里 `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`(配合 `$OutputEncoding`)**能彻底解决 ssh 输出中文乱码** —— 此前所有中文输出一律乱码,误判过一次数据。
**未 commit**。
---
## 14:50 文档库「待处理任务」全量盘点(只读,未改任何文件)
**触发**:用户「检查 dsh 服务文档,还有哪些任务待处理」。**结论快照**(14:50 实测,文档库正处于并行会话活跃期):
**双端对账**(`scripts/docs-sync-check.sh`):**一致 120 / 内容不一致 0 / 仅服务器 0 / 仅本地 1** = `交接单/.doing-T03/OWNER`(本地占用锁标记,非内容差异)。→ **文档内容已全量同步到 `/opt/dsh/docs`**,退出码 1 是锁标记造成的**工具噪音**(建议把 `.doing-*` 加入对账忽略,否则占用期间对账恒判"❌")。
**三层待办(单一来源)**:
- **已规划待执行**(`交接单/README.md §一`):T01 ⏳ 待认领(依赖用户拍板决策点 1)|T03 🔄 执行中(`.doing-T03` 占用,10:14 起 exec-session-B;剩 上传候选池 → guest 启用 → 启动实例 → §八验收 → 归档)|T04 ⏳ 待执行(① 剩 13 项未提交 ② `/opt/dsh/state/.op-lock` 实测不存在 ③ 交接单目录 755/644 未统一 700/600)。**T02 已归档。**
- **未规划**(`03-路线图与待办.md §二`):P2 平台技能投放现状对齐|P3 门户「浏览文件」补下载入口|暂缓 Cookie 域收窄|触发式 dsh 升级回归 + 会话 GC。**注意该文件停在 09-11 23:05,未登记档案 62–68。**
- **档案内挂起**:**档案 65 待部署(需 R8 窗口,会中断在线用户)** ← 当前最卡的一项|档案 64 §8.3(admin 端到端确认 → 铺普通用户)|档案 68 setpriv 路径待用户会话自然验证|档案 66 三项(官方目录批量导入未接信任入口 / 信任状态未持久化,建议并入 migration v6 / `ensure-anysearch-admin.cjs` 手工覆写段待退役)|档案 59 `wake.html` 本机 4708B vs 服务器 4591B 待核对同步|档案 57 四项、档案 42 三项、档案 38b 三项|档案 32 一条未成档发现(`dsh-market` 可绕过第三层管控,风险中)。
**发现 6 处文档记账滞后(未改,留待用户决定)**:
1. `README.md:86` 写「当前下一号 = **53**」(实际已到 **68**,滞后 15 号)
2. `INDEX.md:165` 写「下一号 = **63**」;且 **63 为空号**(跳号,64 起有档案)
3. `INDEX.md §二` 状态摘要「档案 **62** 份」(实际 **68** 份)
4. `BRIEF.md §2` 隔离行仍写 **`512M`**(档案 58 已改 **384 MiB**)—— T03 验收项 M 声称"BRIEF 已更正为 384M",**实际未改**
5. `03-路线图与待办.md` 未登记档案 62–68;`BRIEF.md §3/§4` 停在档案 56/53
6. 档案 64 §9.4「不建议走候选池」已被档案 65 决策(走候选池 + 平台托管段)推翻 → 待回写;档案 16 §7.1-5「禁用=留包 + `disabled:true`」与实现(`pnpm remove` 真卸载)不符(64 §9.2 已登记,未改)
**文档审计** `docs-audit.py`:**退出码 0**(无 P0)。余项 = 2 个 `.bak` 残留(`INDEX.md.bak-20260911111003`、`skills/dsh-change-workflow/SKILL.md.bak-20260911-2055`)、15 个档案缺「状态」段、旧术语/旧域名漂移(历史档案,正常)。
**未 commit、未改任何文档**(用户只要求"检查")。
---