Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下7.md
T
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

47 KiB
Raw Blame 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 导入入库那一行 ——

- 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、未改任何文档(用户只要求"检查")。