# 工作日志 · 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: ""` 条目(并回退删相邻的 `- 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/anysearch-dsh@0.1.4`(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 暂存 `/.dsh-stage/`(chmod 444 + chown uid)→ `setpriv --reuid --regid --clear-groups env HOME= pnpm add --store-dir <沿用旧> --cache-dir <...> file:` → `reconcileBundles`(deps 里带 `dsh.bundle.patch` 的进 `pkg.dsh.profile.bundles`)→ 停 `dsh--*.scope`。产物目录 `/opt/dsh/artifacts/`;候选池 tgz 在 `/var/lib/dshs/business-plugins/`。 7. **凭据文件格式**(`/.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` 写入 `/.credentials.yaml`,**600 + uid 114801** | | patch | `/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` 标记块(备份 `/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 ` + `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 ` vs 本机 `git rev-parse HEAD:`)判断"服务器上的工作区文件是否就是我的改前基线"——**比 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` 注入 `