回收 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)、记忆修复前备份。
47 KiB
工作日志 · 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 次重试后熔断)。
过程中踩的两个坑:
- 服务器与本机镜像不同步(服务器有 06:48 未提交的 TS 改动)→ 不能整文件覆盖 → 改为锚点精确插入(先核对锚点文本 +
existsSync/readFileSync/writeFileSync、join、userRoot均已 import 才动手)。 - 首次插入漏删锚点里的原行 →
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 —— 否则接手者会误以为要从头做。所以先把状态写成"可接手",再释放:
交接单/README.md §一的 T03 行:🔄 执行中 → ⏳ 待续做(明确"步骤 1–9 已完成 / 14:55 释放锁 / 接手者直接续做、勿从头来"),占用者改—(已释放);T03-*.md头部状态行:改为「⏳ 待续做 —— 步骤 1–9 已完成(建包 / host 面 / client 合并 / P0 适配 / 打包 / 扫描 / smoke 全过),剩:上传候选池 →guest启用 → 启动实例 → §八验收 → 归档;已释放占用锁」;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_20250305server 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_search2 次、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(idweb-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. 四个必须先解决的坑(未动手,仅记录)
- P0 provider 歧义:装上后
ctx.web有 2 个可用 search provider → dsh 选择规则是「无显式searchProvider且有多个可用 →WEB_PROVIDER_AMBIGUOUS报错」(不会自动选一个)→ 必须同时显式设web.searchProvider,否则连原有搜索一起挂。 - P0 凭据归属:普通用户看不到插件配置页 → 无法自助填 key,须平台侧注入(
baseEnv加ANYSEARCH_API_KEY或写实例.credentials.yaml)。全租户共用一把 key = 配额/账单耦合 + 外泄面(与档案 23 已记的DEEPSEEK_API_KEY同类问题)。匿名模式看似零成本,但所有实例同 IP 出网 → 1000 次/天配额是全体共享,要先想清楚。 - P0 fetch provider 一并被替换:anysearch 插件同时接管 fetch。dsh 现有
dsh-web-fetch-http是本地实现且带 SSRF 防护(拒非公网地址、逐跳校验重定向)→ 换成 AnySearch Extract = 抓取外发第三方、防护逻辑改变。须明确只换 search、保留本地 fetch。 - P1 内存:每实例
MemoryMax384 MiB(档案 58),多一个常驻 provider 的增量需实测(用scripts/probe-instance-mem.cjs)。
未做:未装、未改、未 scp、未重启、未 commit(纯调研,符合规划会话边界)。
调研(续):B 方案细化 + 前置验证全绿(10:16-11:05,只读取证)
用户指令:「B 方案,能否优先使用这个 [AnySearch API Key]」→ 进入 B 方案落地准备。
新增事实(本地 dsh 源码 + 插件包 + 服务器只读 ssh):
- 插件包已下载分析:
@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-httpid 是http,带 SSRF 防护,会被替换)。 available()不校验 key(endpoint(baseURL,'/v1/search') !== undefined)→ 装上后 search registry = {deepseek-official, anysearch} 两个可用,必须靠显式 patch 消歧(插件自带的那段)。- 安全审查 P2 通过:
lib/无child_process/eval/new Function/require()/process.env/ 文件写 /chmod,唯一外联https://api.anysearch.com。 - 前置验证 4 项全绿:① key 本机有效(HTTP 200
code:01.4s)② 服务器网络可达(拿到 request_id;DNS 走 CloudFront)③ 服务器+key 联合可用(HTTP 200code:02.6s)④ 包安全。 - 踩坑记录:本机
Invoke-RestMethod打该端点必超时(两次),换curl正常;PowerShell 传参给 ssh 会破坏内层双引号 → JSON body 用$(printf '{\042query\042:...}')八进制引号绕过;base64 -d | bash会被安全策略拦。 - 平台铺功能插件的官方姿势(
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/。 - 凭据文件格式(
<home>/.credentials.yaml,600,用户属主):version: 1+records: {client-connection/browser-session: {kind: grant, payload: {version, secret}}}—— 写入须合并,不能覆盖。 - 风险点: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 patchdisabled: 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.htmlL483-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.tsL55 类型定义可证),门户列表页显示的正是中文;但 import 入库时写的是staged.description(从 tgz 的 package.json 读到的英文),而不是entry.description.zh(whitelist.tsL271-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 回去:
- 修根因:
business-plugins.ts的install/uninstall改为runPnpmAs()(setpriv 降权)。新增wsDir()/uidOf()(取<root>/home属主)/runPnpmAs()/healOwnership();原来以 root 跑 pnpm 的pnpmEnv()成死代码,已删(其 HOME 说明移到wsDir上)。 - 防复发:每次改插件最前面跑
healOwnership()(find <profile> -user root -exec chown -h <uid>:<uid> {} +;-exec +分批避免路径过多撞命令行上限;-h因为.bin/*是 symlink),命中写 auditheal_plugin_ownership。这层才是"长期"的关键 —— 将来别的写入路径再污染也会被自动清掉。 - 清存量:手工对两用户跑等价清理 → 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()临时文件、用户主动生图。
- 2 处值得知道:①
- 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手工覆写段待退役)|档案 59wake.html本机 4708B vs 服务器 4591B 待核对同步|档案 57 四项、档案 42 三项、档案 38b 三项|档案 32 一条未成档发现(dsh-market可绕过第三层管控,风险中)。
发现 6 处文档记账滞后(未改,留待用户决定):
README.md:86写「当前下一号 = 53」(实际已到 68,滞后 15 号)INDEX.md:165写「下一号 = 63」;且 63 为空号(跳号,64 起有档案)INDEX.md §二状态摘要「档案 62 份」(实际 68 份)BRIEF.md §2隔离行仍写512M(档案 58 已改 384 MiB)—— T03 验收项 M 声称"BRIEF 已更正为 384M",实际未改03-路线图与待办.md未登记档案 62–68;BRIEF.md §3/§4停在档案 56/53- 档案 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、未改任何文档(用户只要求"检查")。