Files
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

462 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作日志 · 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、未改任何文档**(用户只要求"检查")。
---