Files
dsh_shenxian/dsh-server-docs/04-调整方案/81-目标架构与命名规范-重构总纲.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

44 KiB
Raw Blame History

81 · 目标架构与命名规范(重构总纲)

  • 日期:2026-09-13
  • 状态:🚧 执行中(R0 已完成;R1–R4 分期见 §四)
  • 触发:用户「按照方案改进,要求改造后是一个新的更好的架构包括命名方式等细节」(承接档案 80 的 9 项优化)
  • 原则:保留全部现有功能;只做收口、显式化、命名统一;不做推倒重来

TL;DR|① 定下一套命名:对外叫「工作台」、内部统一前缀 dshs(替换 dshs); ② 定下六层架构:静态层 / 注入层 / 控制面 / 数据面 / 管理面 / 可观测; ③ 分 R0–R4 五期落地,每期自带验收与回滚;④ 内部重命名走「双名期 → 切换期」,绝不硬切(hooks / 自动化 / 脚本里写死了绝对路径)。


一、命名规范(新)

1.1 对外(用户可见)—— 统一为「工作台」

位置 现在 目标 状态
工作台首页 / 工作台 工作台 ✅ 已改
登录 / 注册 登录 / 注册 同 ✅ 已改
管理门户 管理门户(副标题曾含平台名) 管理门户,副标题「平台管理」 ✅ 已改
管理台 管理台 同 ✅ 已改
启动过渡页 正在启动工作区 同 ✅ 已改
英文短名(对外文档/品牌) — DSH Workspace 待用

⛔ 由 scripts/verify-static.mjs 守住:任何 web/**.{html,css,js} 出现 dshs 即校验失败(已接入 npm run verify)。

1.2 内部(工程标识)—— 统一前缀 dshs

资产 现在 目标 迁移方式
systemd 单元 dshs.service dshs.service 双名期:单元做 symlink + Alias=
源码目录 /opt/dshs /opt/dshs 双名期:/opt/dshs → symlink
数据目录 /var/lib/dshs /var/lib/dshs 双名期:symlink(先只加链接,不搬数据)
DB 文件 dshs.db dshs.db 切换期:停服 → mv → 起服(原子+回滚)
env 前缀 DSHS_* DSHS_* 同时读两个名(新名优先),旧名保留一版
npm 包名 dshs、@dsh-local/* dshs、@dshs/* 切换期批量改(>10 文件,先出清单)
注入脚本 SESSION_*_JS 内联在 proxy.ts assets/inject/{recovery,assist}.js R1(见 §四)
注入全局标记 __dshRecover / __dshAssist __dshsRecover / __dshsAssist R1,保留旧名别名一版
注入 CSS 类 .__dsh-*、.wk-* .dshs-*(浮层与过渡页统一) R1/R2
管理面路由 portal.html + desktop/plugins/skills.html 三个桩页 /admin 路由族(桩页删除) R2
业务技能前缀 mcn-* / douyin-* 不动(业务域命名,与平台无关) —

兼容策略(硬要求):内部改名一律两期 —— 双名期(新名为主 + 旧名仍可用)→ 切换期(删旧名)。 理由:~/.workbuddy/settings.json 的 hooks、/etc/cron.d/*、平台脚本、备份脚本、文档、自动化任务里都写死了绝对路径;今天已因路径失配被"全机写操作被拒"上了一课。


二、目标架构(六层)

① 静态/呈现层   web/*.html(9 → 6 页,删 3 个跳转桩)+ design.css(统一 .dshs-* 前缀)
② 注入层        assets/inject/{recovery,assist}.js —— 独立文件、构建期硬校验、运行时可读
③ 控制面        supervisor(实例生命周期)· orchestrator(spawn/reap/配额/熔断)· proxy(子域/鉴权/注入)
④ 数据面        SQLite + /var/lib/dshs/{users,state,artifacts,business-plugins}
⑤ 管理面        /admin 路由族(原 portal.html 拆分)+ admin API(**权限仍全在平台域**)
⑥ 可观测        /opt/dshs/state/*.json + GET /api/dsh/status(汇聚:实例/内存/熔断/插件)

相对现状的 4 处关键改动(都来自今天的事故):

# 改动 消灭的问题
1 注入脚本出模板字面量,落成独立文件 模板转义把整段脚本写崩(今天 2 次:.join('\n') → SyntaxError → 浮层/自愈/助手全废)
2 自愈链路收口成一份状态机(探活 → 判据 → 恢复 → 冷却 → 告警) 档案 50→51→72→77→78 5 代补丁叠加,改一处要读五处
3 实例配额跟随插件集合(按已启用插件的内存预估定 MemoryMax,V8 堆跟随) 「启用某插件 = 必然被 OOM 杀」(今天:384MB 上限 < univer gateway 390MB)
4 有界恢复(连续 N 次失败停在明确失败态 + 手动重试) 「恢复→起来→又被杀」无限循环(今天 guest 每 ~35s 一次)

三、目标 vs 现状(逐项对照)

维度 现状 目标
用户可见命名 6 个页面挂内部平台名 全清,统一「工作台」;CI 守住
注入脚本 2 段内联 TS 模板字面量(~27KB) 2 个独立 .js 文件 + verify-inject 强校验
自愈 proxy 注入脚本 + orchestrator 各自为政 一份状态机(前端只负责"探针 + 呈现")
配额 硬编码 384MB / V8 160MB 按插件集合自适应
恢复 无限重试 有界(N 次后停)
校验 单测 + 临时补的 verify-inject npm run verify 一条命令(build+单测+注入+静态页)
管理面 整页跳 portal.html(+3 桩页) /admin 路由族 + 实例内弹窗 iframe(R5 评估后)
可观测 stderr / 采样文件 / 新加的 breaker 统一 state/*.json + 一个 status 接口
内部标识 dshs 遍布 统一 dshs(双名期过渡)

四、分期落地(R0–R4)

期 内容 重启 验收 回滚
R0 ✅ 本轮已完成 ① 静态页去痕迹(7 文件,已上线)② npm run verify 统一校验 + verify-static.mjs 静态不变量(含"去痕迹"防回归) 否 线上 <title> 全绿;npm run verify rc=0 git checkout -- web/… scripts/… package.json(未 commit)
R1 ① 注入脚本外置(assets/inject/*.js + 运行时可读 + 缺文件 fail-fast)② 自愈收口状态机 ③ 有界恢复 ④ 配额自适应 一次重启 真 Chrome 三态(切回/断流/无操作自愈)+ 内存采样不再贴顶 + verify 全绿 备份 lib/ + 回滚源文件 + 重启
R2 管理面就地化:/admin 路由族 + 实例内弹窗 iframe;删 3 个跳转桩页 一次重启 admin 在实例内完成全部分区操作;普通用户看不到入口 恢复 portal.html 跳转
R3 内部标识迁移 dshs → dshs:双名期(symlink + 新旧 env 双读)→ 切换期(改单元名/路径/DB) 两次窗口 systemctl status dshs 正常;hooks/自动化/脚本路径全部改完并验证 保留旧名 symlink 与 DB 备份,随时切回
R4 文档与代码同仓(或明确单向导出),去掉"对账/推送/幽灵文件"仪式 否 文档改动随代码 PR 一起走 —
R5 多语言(i18n):平台 6 页 + 注入脚本文案(词条表 + data-i18n + 极小运行时);不动官方 dsh UI 与业务插件 否 见 §十 §10.4 —

R1 是价值最高的一期(今天 4 类事故里的 3 类都在它刀下);R3 风险最高(写死的绝对路径),故排在最后且必须原子化。


五、与红线的边界(先讲清)

线 与本次的关系
R2 不改官方 dsh 全程只动平台侧(supervisor/proxy/web/scripts),@deepseek-ai/dsh 零改动
R3 client bundle 禁 exports.default 业务插件改动需守(含 R1 若动插件侧)
R5 权限只准收窄 R2 的"管理面就地化"属扩大可见面 ⇒ 先出「权限影响评估」+ 用户确认;采用 iframe 内嵌(代码/数据仍在平台域、agent 读不到)而非能力下沉
R7 >10 文件先出清单 R3 的包名/路径批量改先出受影响清单再动
R8 中断在线用户 R1/R2/R3 各需窗口,全部提前知会;能合并的合并(R1+R2 建议同一窗口)

六、待用户

  1. R1 是否现在做(一次重启窗口,断在线用户 2–5 秒)?我建议 R1 + R2 同一窗口,一次断线办完。
  2. R3 的"双名期"是否赞成(先加 symlink/双读,稳定后再删旧名)?这是把"命名彻底换掉"的风险压到最低的走法。
  3. 是否需要我把 R1 落成 8 段式交接单交执行会话(遵循"规划与执行分离")。

七、进度追加(2026-09-13 11:0x)

7.1 R0 追加:静态校验再加一条「内联脚本必须能解析」

scripts/verify-static.mjs 现在除了「去痕迹 + title + wake.html 五个 id」之外,还会把每个页面里的内联 <script> 抽出来跑 node --check。 理由:今天两次事故都是脚本语法错误 = 静默失效(.join('\n') 那次),而当时所有"验证"都验不出这一点。现 10 段内联脚本全部通过。

7.2 R2 提前做掉一项:登录 / 注册页的提交忙碌态(用户报障)

报障:「点击登录后停几秒,按钮不能交互、也没有加载动画」。 根因:web/login.html 提交处理里没有任何忙碌态 —— /api/auth/login 走 bcrypt 要数百毫秒~数秒,/api/dsh/enter 还要拉起实例(可达 20 s),期间按钮可点、无反馈。 改法(对齐 06-工作台UI规范 的按钮/圆角/主色 Token,只加不删):

  • 新增页面局部样式:.btn[disabled](透明度 .72 + not-allowed)、.btn.is-busy(inline-flex + gap)、.btn .sp(14px 环形转圈,currentColor 系)、prefers-reduced-motion 关闭动画;
  • 新增通用 setBusy(busy, label):进入忙碌态时记住原 innerHTML、禁用按钮、插入 spinner + 文案;退出时原样恢复;
  • 两段式文案:登录中… → 登录成功后切 正在进入工作台…(这一句覆盖"停几秒"的那段,最容易被误认为卡死);
  • 失败路径(用户名/密码错、进入工作台失败、网络错误)统一 setBusy(false);register.html 同构改造,成功后保持禁用避免重复注册。 验证:线上取回页面含 setBusy / is-busy / .sp / 两段文案;真 Chrome 实测 —— 点击后 220 ms 按钮已 disabled=true、class=btn primary is-busy、内容为「spinner + 登录中…」;失败后按钮恢复「登 录」并显示「用户名或密码错误」。截图 _patch77/shots/login-busy.png。 部署:静态页 + 校验脚本 scp(免重启)。回滚:git checkout -- web/login.html web/register.html。

八、R1 完成记录(2026-09-13 11:0x–11:2x)

8.1 四项全部落地

# 内容 落点
① 注入脚本外置 新增 assets/inject/recovery.js + assist.js(纯 JS,可 node --check);proxy.ts 改为 loadInject('recovery.js') 运行时读取(fail-fast:读不到即启动失败,宁可不发页面也不静默失效)。原 27 KB 内联模板字面量删除 ⇒ 「转义写崩」这一类事故从根上消失
② 自愈收口成单一状态机 在 recovery.js 头部写明唯一策略表(5 类触发面 × 判定 × 动作):切回页面 / 请求 401·not_running / 可见时 25 s 心跳 / EventSource·WebSocket 断开 / 有界保护。改这一个文件即可,不再需要在 proxy.ts 与注入脚本间来回找
③ 有界恢复 窗口 10 min 内恢复 > 3 次 ⇒ 停止自动恢复,停在明确失败态 + 「重试」按钮(手动点击重置预算)。治「恢复→起来→又被杀」的无限循环
④ 配额随插件集合推导 orchestrator.ts 新增 PLUGIN_MEM_MB 预估表 + instanceMemMb() + heapMbFor() + withHeap():MemoryMax = 160 + Σ(插件预估)(下限 384 / 上限 1024);V8 堆由配额独占(配额-96,封顶 256)——原先写死的 MemoryMax=384M 与 env 里的 --max-old-space-size=160 都作废

校验:npm run verify 全绿(构建 + 单测 + 注入脚本 node --check + 内联回退检查 + 静态页不变量 + 10 段内联脚本语法)。

8.2 部署与验收(一次重启窗口)

项 实测
服务 active,PID 424413 → 435201,门户 200
guest 实例配额 dsh-100002-*.scope → MemoryMax = 544 MB(160 基座 + 384 univer)✅
guest 实例 V8 堆 进程 env NODE_OPTIONS=--max-old-space-size=256 ✅
注入脚本 guest 实例页含 overRecoverBudget / __dshRetryBtn / 「状态机(单一来源」/ HEARTBEAT_MS / 新视觉 conic-gradient ✅
备份 /opt/dsh/backups/pre-r1-20260913-110923(lib-pre-r1 + proxy/orchestrator 源文件)

8.3 ⚠️ 部署中踩的两个坑(如实记录)

  1. __dirname 在 ESM 里不存在 ⇒ 平台启动即失败(ReferenceError: __dirname is not defined in ES module scope),systemd 自动重启 3 次(约 1 分钟不可用)。本项目 package.json 是 "type": "module",必须用 dirname(fileURLToPath(import.meta.url))。已修 + 重新部署恢复。教训:本地 npm run build 通过 ≠ 能跑起来 —— ESM/CJS 的运行时差异编译期查不出,所以这类改动必须"部署后立刻 curl 自证"。
  2. DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160 改不动:它既不在 systemd unit、也不在 manager env(systemctl unset-environment 后进程 env 里仍在)⇒ 靠改配置够不到。故改为代码侧独占堆上限(withHeap():把既有 NODE_OPTIONS 里的 --max-old-space-size 摘掉再按配额补回),env 只保留承载其它选项的能力;如需强行指定堆,用 DSHS_HEAP_MB_OVERRIDE。

8.4 仍未做(R2–R4)

期 状态
R2 管理面就地化(/admin 路由族 + 实例内弹窗 iframe;删 3 个跳转桩页) ⏸ 待 R5 权限影响评估(扩大可见面)→ 评估后再动;且它落在 @dsh-local/business-plugins 插件侧,改完需重建插件 + 实例重启
R3 内部标识迁移(dshs → dshs) ⏸ 未动(须「双名期 → 切换期」,两次窗口)
R4 文档/代码同仓 ⏸ 未动(工程仪式向)

九、R3 双名期(第一步已做)+ R2 的 R5 权限影响评估(待确认)

9.1 R3 · 双名期第一步:加 symlink,不动运行中的服务(2026-09-13 11:4x)

目标名 形式 实测
/opt/dshs → /opt/dshs ✓ /opt/dshs/lib/supervisor/proxy.js 可达
/var/lib/dshs → /var/lib/dshs ✓ /var/lib/dshs/users 可达
dshs.service → dshs.service + daemon-reload ✓ systemctl is-active dshs = active,两名字同 PID(435201)
  • 零中断:只加符号链接 + daemon-reload,没有重启任何服务。
  • 可回滚(一条口令):rm -f /opt/dshs /var/lib/dshs /etc/systemd/system/dshs.service && systemctl daemon-reload
  • 意义:「双名期」成立 —— 新旧两个名字同时可用,后续把引用的绝对路径从旧名改成新名时不会出现"改到一半全断"。

R3 剩余步骤(未做):① 代码侧 env 双读(DSHS_* 优先、DSHS_* 兜底);② 生效脚本 / hooks / 自动化 / 云备份任务里的绝对路径逐条改新名;③ 切换期:改 systemd 单元真名、停服迁 DB(dshs.db → dshs.db)、删旧符号链接。两次窗口,且必须原子化。

9.2 R2 · R5 权限影响评估(按 R5 要求先出评估,再等用户确认)

R2 = 「实例内『设置 → 平台管理』弹窗内嵌 portal.html,不再整页跳门户」。

维度 结论
扩大点(R5 关注) 实例页里出现可操作平台管理的入口(原设计是从实例整页跳到平台域)。仅对 admin 可见;普通用户看不到
技术形态 <iframe src="https://alotbuy.com/portal.html?embed=1">:代码与数据全部仍在平台域;实例域(<user>.alotbuy.com)与平台域跨子域 → 同源策略隔离 ⇒ 实例内的 agent 读不到 iframe 内容(这与档案 05 PoC-2 ① 当年否掉的「把 admin 能力装进实例」有本质区别)
实测可行性 curl -I https://alotbuy.com/portal.html → 无 X-Frame-Options、无 CSP frame-ancestors ⇒ 同站子域 iframe 可直接嵌,无需改安全头
风险 ① 管理动作在实例内完成 ⇒ 误操作面。缓解:先"只读优先"(iframe 内仅展示,写操作仍回跳平台域完成)
风险 ② agent 被诱导去点管理面?跨域读不到内容 ⇒ 实际不可行;再加"仅 admin 可见"兜底
风险 ③ 删掉 desktop/plugins/skills.html 三个跳转桩页会影响既有书签 ⇒ 保留 301 到 /admin 更稳
我的建议 批准 R2 的"只读优先"形态(iframe 内嵌 + 仅 admin + 写操作回跳平台域)

✅ 用户已拍板(2026-09-13,见 §9.2b):批准"只读优先",但形态改为「原生弹窗」而非 iframe。

R2 工程量提示:它落在 @dsh-local/business-plugins 插件侧(设置面板的 section 由该插件注册)⇒ 改完要重建插件 tgz + 投放候选池 + 实例启用,比 R1 长。

9.2b 修正(2026-09-13)· R2 形态改为原生弹窗(用户拍板)

用户原话:「为什么要 iframe,原生弹窗体验更好」——质疑成立,原方案是为了省工程量牺牲了体验。

iframe 当初为什么被选(如实记录,不是辩护):弹窗里要展示的是平台自己的管理面(portal.html,跑在 alotbuy.com)。用 iframe 只是把它整页嵌进来 ⇒ 零重写;代价是:外观与实例内 dsh UI 不一致(两套字体/间距/配色)、门户是带自己导航的 SPA 塞进小窗里、跨域也不方便共享状态。原生弹窗要把这块 UI 在插件里重新渲染,所以当初没选。

改定后的形态:

维度 原(iframe) 改定(原生弹窗)
渲染 <iframe src="portal.html?embed=1"> 插件内原生渲染(复用 06-工作台UI规范.md 的 Token)
数据来源 门户页面自己取 插件直接调平台 API(共享会话 Cookie:/api/dsh/status、/api/admin/runtime、/api/plugins/business …)
覆盖范围 门户全量管理面(免费白拿) 只读优先的子集(状态 / 实例 / 候选池列表),写操作仍回跳平台域 —— 与"只读优先"的批准口径一致
体验 两套 UI 拼接感 与实例内 UI 同源;无跨域嵌套
代价 0 重写 需要把只读子集重写一遍;门户新增区块时要同步(双源风险,但仅限 UI 层,API 仍是单一来源)

⚠️ 不能两头都要:若日后要求"弹窗里能完成全部管理动作",那就等于把整个门户重写进插件(双源维护 + 与档案 39「收窄执行面」方向相反)⇒ 届时应回头把门户收敛成一个 API 客户端,而不是继续复制 UI。

9.3 R4(文档/代码同仓)· 仍待用户定 —— 先讲清它在解决什么

⚠️ 用户 2026-09-13 反馈「没看懂 R4 是要做什么」⇒ 原描述直接跳到选项,缺了动机。补在这里。

现在是什么样(事实):改造文档与平台代码分在两个 Git 仓库——

代码 文档
本机 D:\github\dsh_shenxian E:\ProgramData\AI技能\aliyun-dsh-server\dsh-server-docs
远端 dsh_shenxian.git dsh_shenxian_doc.git
生产 /opt/dshs(scp + build + 重启) /opt/dsh/docs(无 .git,纯 scp 镜像)

这带来三个真实摩擦(都是本项目实际踩过的,不是假想):

  1. 同一个改动要分两次走:改功能往往同时要改档案/UI 规范 ⇒ 一次交付要 2 个 commit + 2 次 push + 1 次 scp 对账;漏一边就出现"代码说 A、文档说 B"。
  2. 版本对不上:文档里写的"现状"绑的是某一个代码 commit,但两库各走各的 ⇒ 复现历史时无法用一次 checkout 同时拿到"当时的码 + 当时的文档"(开源导出正因此需要单独冻结版本)。
  3. scp 镜像靠仪式维持:/opt/dsh/docs 没有版本控制 ⇒ 只能靠 docs-sync-check.sh 对账 + 人肉纪律;今天的"5 处内容不一致"就是这么暴露出来的。

两条路(选一个即可):

  • (a) 文档并入代码仓:一个仓库装 src/ + docs/。彻底去掉对账仪式,PR 里码与文档一起评审;代价 = 要动 Gitea 仓库结构、迁移历史、改 hooks 与所有脚本里的路径假设。
  • (b) 保持双库,但明确"单向导出":承认两库并存,把流向写死成一个方向(如"文档是权威源 → 随代码 PR 单向导出到代码仓的 docs/ 供开源用"),取消反向同步;代价 = 仍是两套 commit,只是不再有"谁覆盖谁"的歧义。

为什么它排在 R2/R3 之后:纯仓库结构决策,不影响运行时;不选也能继续开发,只是每次交付多两道手续。


十、R5 · 多语言(i18n)· 排最后处理(用户 2026-09-13 新增需求)

用户原话:「加个需求 相关页面能否 支持多语言,可以最后处理」

10.1 范围(先划清,避免越界)

层 是否做 说明
平台静态页(/、login、register、portal、admin、wake) ✅ 做 文案全在 web/*.html 里,改动可控
注入脚本用户可见文案(assets/inject/*.js:浮层「工作区正在恢复…」、「重试」、提示条) ✅ 做 只改文案字符串,逻辑不动
平台 API 的错误文案({error:'pending_review'} → 中文句子) ✅ 做(改为错误码 + 前端映射) 现在前端自己把 error 码翻成中文,属"半成品 i18n";统一到一张表
实例内·官方 UI 文案(侧栏「新会话/工作区/设置」、对话区「描述你想要构建的内容…」、模型名、标题 DeepSeek Harness) ⚠️ 平台/插件都改不了它本身(只能「覆盖」,见 §10.5 路径 A/B) 实地查证(2026-09-13):这些文案硬编码在官方包里 —— /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-ui-*/lib/client.js(sidebar / settings-plugins / agent-preset …)。官方没有 UI i18n 机制:唯一的 README.i18n.yaml 是文档翻译、不是界面语言包;全树无 locale / 语言切换 API。⇒ 要换语言只能「覆盖」,不是「写个插件就能干净做到」
业务插件/技能(dsh-plugin-mcn-suite 的界面与 36 个 SKILL.md) ✅ 能做(是我们的代码,只是要另排一期) 上一版写「不做」是界线划错了:它的界面与技能都是我们自研插件的内容,改它不动官方包。唯一成本在发布链路:改 → 重打包 tgz → 投放候选池 → 用户重新启用(见 §10.5 路径 C)

10.2 方案(不引框架,最小实现)

  1. 词条表:assets/i18n/zh-CN.json + en.json(扁平 key,如 login.submit、wake.starting)。
  2. 极小运行时:assets/i18n.js(约 60 行,放 web/ 供静态页用;注入脚本用同一份词条):
    • 选语言优先级:?lang= → cookie dshs_lang → navigator.language → 兜底 zh-CN;
    • 页面用 data-i18n="key"(文本)/ data-i18n-attr="placeholder:key"(属性)标注,运行时就地替换;
    • 缺 key 时回退到中文原文并 warn(不出现空文案)。
  3. 切换入口:登录页与门户页页脚一个语言下拉(?lang=en + 写 cookie),不新增页面。
  4. 抽文案纪律:所有用户可见中文必须是词条 key;verify-static.mjs 加一条防回归判据(静态页里不得出现"裸中文文案",白名单放 data-i18n 与注释)。

10.3 为什么排最后(三条硬理由)

  1. 它会横穿几乎所有页面 —— 在 R2(门户管理就地化)落地前抽文案,R2 一改就等于白抽一遍;
  2. R3 的内部改名还没走完 —— 词条文件的目录(assets/i18n/)与将来 dshs 命名体系要对齐,先改完再放词条更省事;
  3. 它是纯体验增强、不修任何事故 —— 而今天已确认的 4 类事故(脚本写崩 / OOM / 无限恢复 / 假绿校验)已全部闭环,优先级自然排在后面。

10.4 验收口径(先写下来,免得漏)

  • 6 个平台页 + 注入脚本文案在 zh-CN / en 下无空文案、无 key 泄漏(页面不得出现 login.submit 这种裸 key);
  • 语言切换刷新后保持(cookie);既有中文行为零回归(默认仍是 zh-CN);
  • npm run verify 通过(含新增的"裸中文"防回归判据);
  • 不动 @deepseek-ai/dsh 与业务插件(R2 红线)。

10.5 两个"可选纳入项"(要不要做,你一句话定)

选项 内容 代价 / 风险 我的建议
A · 注入层覆盖官方 UI 文案(让实例内也能换语言) 在注入脚本里按语言字典替换 dsh 官方界面上的可见文案(侧栏/菜单/输入框 placeholder 等) ① 官方升级或改版即失效(脆弱,需长期跟)② 属"改别人 UI",可能出现文案与官方功能不匹配 ③ 只能覆盖 DOM 可见文本,覆盖不全(提示/报错/动态内容) ⚠️ 折中可行但不推荐;若只为"给外部客户看",建议只覆盖 3–5 个高频入口(如「新会话 / 设置 / 发送」),不做全量
B · 插件自带 i18n(mcn-suite 界面与技能文案) 词条表打进插件 tgz;插件 UI 与 SKILL.md 按语言选词 改建 → 重打包 → 投放候选池 → 用户重新启用(链路长);技能文案还牵动 agent 的提示词 📋 可另立一期(等 R5 主体做完再评估)
C · 只做平台页 + 注入脚本文案(原方案) 6 个平台页(登录/注册/门户/管理台/过渡页/首页)+ 我们注入的浮层与提示条 最小、零回归 ✅ 默认按 C 做:平台自己写的界面 100% 覆盖;实例内官方 UI 保持原样

一句话结论:C 是必做项(平台自己的界面),A / B 是可选扩展;你说「加 A」「加 B」或「只做 C」,我照办。

📌 补充(2026-09-13,见 §10.7):查证发现官方自带 locale 扩展点后,多出第 4 个选项 D · 走官方 locale 体系(自研插件与浮层文案跟实例内语言开关联动,不触 R2)—— 它可视为 C 的加强版(同一批词条,换一套消费方式)。选型时把 D 一并考虑。

✅ 用户已拍板(2026-09-13):R5 = 走官方方案(用户原话:「R5 肯定是匹配 dsh 官方方案,什么会有疑问呢」)⇒ 按 D 实施:平台页 / 注入层 / 自研插件一律走官方 ctx.locale.addLanguage() + ctx.locale.register(ns, lang, {...}) 扩展点,官方代码零改动。 ⚠️ 唯一的边界(不是疑问,是事实,需在验收口径里写明):官方自己只把 2 个 UI 包迁到了 locale,仍有 20+ 个包硬编码中文(trajectory/chat/settings-models/workspace/settings-plugins/agent-preset …,清单见 §10.7 ⑤)。⇒ 切英文后,那些官方包仍显示中文。这属官方未完成项,不在我们可改范围(改它们 = 触 R2)。若日后确有需要,只能另开一期走「注入层改 DOM」(脆弱、覆盖不全)。 ⚠️ 2026-09-15 更正:本条只适用于 0.1.2-rc.1。0.1.5-rc.1 实测:官方客户端 55 个包里已有 36 个走官方 locale API ⇒「切英文后仍大面积中文」已基本不成立,详见文末 §10.8。 因此 A/B/C 三项不再作为主方案:A 降级为"若官方长期不迁则考虑"的备选;B 被 D 取代(用官方体系即可覆盖自研插件,不必自造词条运行时);C 被 D 完全包含(D 的做法同样覆盖平台页,只是消费端换成官方 t 座位)。

10.6 ⚠️ 界线更正(用户 2026-09-13 质疑后实地查证)

用户原话:「现在做的不都是插件吗,怎么会影响 dsh 原项目呢」——质疑成立,我上一版把界线划错了。

真正的分界不是「是不是插件」,而是「这段文案的代码是谁的」:

代码归属 例子 i18n 能不能做
我们写的·平台静态页 登录 / 注册 / 门户 / 管理台 / 过渡页 / 首页 ✅ 直接做
我们写的·平台注入层 恢复浮层「工作区正在恢复…」、顶部提示条、📁 我的文件 / 🧭 能力 面板 ✅ 直接做(改 assets/inject/*.js)
我们写的·自研插件 @dsh-local/business-plugins(功能管理分区)、portal-entry(平台管理卡片)、workspace-scoped-picker、dsh-plugin-mcn-suite(MCN 工作台)、dsh-univer-office ✅ 能做(改插件 → 重打包 → 投放 → 启用)
官方写的·@deepseek-ai/dsh* 内的 UI 侧栏「新会话」、对话区 placeholder、设置面板骨架、模型名 ⚠️ 改不了它本身(R2 红线:官方包零改动)。要分两种:① 已迁到官方 locale 的包 → 用户切语言即跟随(零改动,白拿);② 仍硬编码中文的包(约 20+ 个,见 §10.7)→ 只能覆盖(§10.5 路径 A / 改 DOM)

为什么插件不能「改」官方那部分:dsh 的插件机制是扩展(extend) —— 插件注册自己的 UI / 能力;它不提供「替换官方内置文案」的扩展点。想动官方已硬编码的那部分只有两条路:注入层改 DOM(可行但脆弱)或 替换官方同名包(= 改官方产物,触 R2,不做)。

⚠️ 本节曾被写错、已于同日更正:原文写「官方 UI 包…无语言包、无 locale API」是错的 —— 官方有 @deepseek-ai/dsh-client-locale 与完整的语言扩展 API。查证证据与结论见 §10.7。

10.7 修正(2026-09-13)· 官方 i18n 能力实地查证

触发:用户追问「有哪里是需要 替换官方包 / 改官方代码的」。核查时发现 §10.6 的结论建立在一次不完整的抽查上(当时只看到 locale 包里有一个 README.i18n.yaml,就当成"只是文档翻译")——这个判断是错的,本节省略推测、只留实测。

实测对象(服务器 bt-server,只读):

/usr/local/bin/dsh -> /usr/local/lib/node_modules/@deepseek-ai/dsh/lib/bin.js
/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-locale/
    package.json  README.md  README.zh.md  README.i18n.yaml  LICENSE
    lib/index.js  (1.3 KB, host 侧)   lib/client.js (55.5 KB, 含内置词典)

① 官方确实有一套 i18n,而且是有文档的扩展点(package.json description 原文):

"Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries"

② host 侧只做一件事 —— 在 settings 里注册语言偏好命名空间(lib/index.js 全文关键常量):

常量 值 含义
LOCALE_SETTINGS_NAMESPACE "locale" 设置命名空间
LOCALE_PREFERENCE_FIELD "preference" 存 $DSH_HOME/settings.yaml 的字段
LOCALE_IDS ["zh", "en"] 随包发布的语言(仅两种)

③ client 侧导出:LocaleRuntime · COMMON_NS · FALLBACK_LOCALE · SETTINGS_NS · apply · inject。内置 common 词典是通用词("ok":"确定" / "cancel":"取消" / "close":"关闭" / "copied":"复制成功" / "retry" / "loading" …)。

④ 官方文档给出的扩展 API(关键 —— 这条路不碰官方代码):

export const inject = ['locale']
export function apply(ctx) {
  // 加一门语言(外部 id = BCP 47 风格;fallback 必须已注册且链最终终止于 en)
  ctx.effect(() => ctx.locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }), 'my-locale: language')
  // 注册某命名空间的该语言词典
  ctx.effect(() => ctx.locale.register('common', 'ja', { cancel: 'キャンセル', close: '閉じる' }), 'my-locale: common dictionary')
}
  • 消费方式:ctx.locale.bind(ns) 或框架注入的 t 座位;
  • 设置入口:Settings → General(用户自己选语言);
  • 官方原话:"The package ships zh and en, while external client plugins can add languages and their namespace dictionaries."

⚠️ 本条已被更正(2026-09-15 实测 dsh 0.1.5-rc.1,服务器 bt-server 只读)

下面这段结论只适用于 0.1.2-rc.1。当前版本实测:客户端包 55 个中, 36 个已走官方 locale API(locale.register / locale.bind / addLanguage / LOCALE_IDS), 其中 35 个同时含中文 —— 那是它们自带的 zh 词条,不是硬编码; 不用 API 却仍含中文的只剩 1 个(dsh-client-connection)。

⇒ 官方已大面积补齐客户端 i18n,「切英文后大量官方包仍显示中文」这个担心在当前版本已基本不成立。 ⇒ 但这不改变 R5 的结论:平台自己的 6 个静态页 + 注入浮层文案仍然要做(那段代码是我们的)。

复现命令(对 <dsh 包根>/node_modules/@deepseek-ai/*/lib/client.js 逐包): grep -cE "locale\.(register|bind)|addLanguage|LOCALE_IDS" 与 grep -c '[一-龥]'。

⑤(0.1.2-rc.1 时期)但官方自己没铺开 —— 实际引用了 locale 的官方 UI 包只有 2 个(dsh-client-ui-conversation、dsh-client-ui-directory-picker-browse),其余大量包把中文硬编码在 lib/client.js(按含中文行数,前 15):

包 中文行数 包 中文行数
dsh-client-ui-trajectory 167 dsh-client-ui-cordis 46
dsh-client-ui-conversation 138 dsh-client-ui-settings-plugin-inventory 36
dsh-client-ui-chat 99 dsh-client-ui-subagent 34
dsh-client-ui-settings-models 98 dsh-client-ui-permission-presets 20
dsh-client-ui-workspace 64 dsh-client-ui-schedule 18
dsh-client-ui-settings-plugins 50 dsh-client-ui-workflow-run 17
dsh-client-ui-agent-preset 50 dsh-client-ui-model-selection 17

⑥ 结论 —— 回答「有哪里需要替换官方包 / 改官方代码」:

范围 要不要替换官方包 / 改官方代码 用什么
平台 6 个静态页 + 过渡页 ❌ 不需要 自己的 HTML,按 §10.4 抽词条
平台注入层(浮层 / 提示条 / 文件·能力面板) ❌ 不需要 改 assets/inject/*.js
我们自研 / 投放的插件(business-plugins、portal-entry、mcn-suite…) ❌ 不需要 官方扩展点 ctx.locale.register() / addLanguage()(第 ④ 条)
官方 UI · 已迁到 locale 的包(2 个) ❌ 不需要 用户切语言即自动跟随(零改动白拿)
官方 UI · 仍硬编码中文的包(20+) ⚠️ 官方自己没迁完。不改官方代码就只能注入层改 DOM(脆弱、覆盖不全,§10.5 路径 A);替换官方同名包 = 改官方产物 ⇒ 触 R2 ⇒ 不做 —

⇒ 对 R5(多语言)的净影响:可选面变大了。 原先只有 C(平台页+注入层) / A(覆盖官方 DOM) / B(插件自带 i18n) 三项;现在多出一项: D · 走官方 locale 体系 —— 用 addLanguage + register 让我们自研插件与浮层文案跟着实例内的语言开关走,与官方 zh/en 同一套机制、用户在一个下拉里切换。它不触 R2,是 §10.4 方案 C 的加强版(同一批词条,换一套消费方式)。 ⚠️ 代价:register(ns) 要求同时提供 zh 与 en(编译期校验),且新增语言(如 ja)的 fallback 链必须终止于 en。

⑦ 遗留待查(尚未查,不写结论):addLanguage 是否要求目标语言的语言定义先行注册(README 说"definitions 与 dictionaries 可任意顺序",但"fallback 必须已注册")——真做 D 方案时需在实例里跑一次验证。

⚠️ 教训:上一轮的错在**用「一个文件名」代替「读一读那个包」**就下了"官方没有能力"的结论。凡结论会影响方案取舍(尤其"做不了"这类),必须落到实测证据,不能靠抽样直觉。


10.8 修正(2026-09-15)· 官方 locale 覆盖率已大幅补齐(实测 dsh 0.1.5-rc.1)

触发:用户问「项目支持多语言吗(英文/中文)」。顺手复核 §10.7 ⑤,发现它已过时。

实测(服务器 bt-server,只读;对象 <dsh 包根>/node_modules/@deepseek-ai/*/lib/client.js):

指标 实测值
客户端包总数 55
已走官方 locale API(locale.register / locale.bind / addLanguage / LOCALE_IDS) 36 (其中 35 个同时含中文 —— 那是它们自带的 zh 词条,不是硬编码)
不用 API 却仍含中文 1(dsh-client-connection)
既不用 API 也不含中文 18(多为没有 UI 文案的逻辑包)

复现命令(逐包两条计数):

f=<pkg>/lib/client.js
grep -cE "locale\.(register|bind)|addLanguage|LOCALE_IDS" "$f"
grep -c '[一-龥]' "$f"

结论更正

  • §10.7 ⑤ 的「官方只迁了 2 个包、20+ 个包硬编码中文」只适用于 0.1.2-rc.1;在 0.1.5-rc.1 上官方已大面积补齐 ⇒ §10.5 那句「切英文后那些官方包仍显示中文」在当前版本已基本不成立。
  • ✅ R5 的结论不变:平台自己的 6 个静态页(web/*.html,现全为 <html lang="zh-CN"> + 硬编码中文)与注入浮层文案仍是待做项 —— 那段代码是我们的,与官方迁不迁无关。
  • 📌 选型不变:走官方 locale 体系(D:addLanguage + register),零官方改动。
  • ⚠️ 另一处仍未做(与「官方有没有迁 locale」无关):平台静态页目前没有任何 i18n 运行时 —— 无词条表、无 ?lang=/cookie 选择、无界面切换入口;API 错误文案仍是「前端把 error 码翻成中文」的半成品(R5 计划统一到一张表)。

10.9 追加口径(2026-09-15 · 用户定)· 默认语言 = 英语;插件一律沿用官方 locale

用户原话:「开发的插件需要沿用 dsh 官方项目的多语言方案,这个项目是全球化的项目,默认选中英语」

① 自研插件:现状已合规(无需改造)

检查项 实测(源仓 poc/business-plugins/lib/client.js)
走官方扩展点 ✅ locale.register(NS, { zh, en }) + locale.bind(NS)
声明依赖 ✅ inject = ["slots", "locale"]
双语完整 ✅ zh / en 两套同时提供(register 的编译期要求)

⇒ 纪律(对后续所有自研插件生效):⛔ 不自造词条运行时;一律 ctx.locale.register(ns, lang, {...}) + ctx.locale.bind(ns),且 inject 必须含 "locale"。

② 「默认英语」在官方侧本来成立(实测 dsh 0.1.5-rc.1)

事实 值
FALLBACK_LOCALE "en"
LOCALE_IDS ["zh", "en"]
偏好存放 settings.yaml 的 locale.preference(LOCALE_SETTINGS_NAMESPACE="locale" / LOCALE_PREFERENCE_FIELD="preference" / SETTINGS_NS="settings.locale")
解析顺序 宿主偏好 → … → navigator.language(排在后面)→ 兜底 FALLBACK_LOCALE

⇒ 平台不需要为「默认英语」改官方代码或打补丁(官方兜底即英语)。 ⚠️ 唯一例外:navigator.language 仍参与解析 ⇒ 中文浏览器且无偏好时可能自动落到 zh。若要「一律默认英语、忽略浏览器」,就在铺实例时把 locale.preference: en 写进该用户的 settings.yaml —— 这是平台侧配置(不触官方代码);用户之后仍可在 Settings → General 自选,且选择会保持。

③ 平台静态页(R5 范围):兜底语言 zh-CN → en(覆盖 §10.2 第 2 条)

新的选择优先级:?lang= → cookie(dshs_lang)→ navigator.language(仅命中支持列表才用)→ 兜底 en。

📌 保留 navigator 一步是全球化产品的常规做法(浏览器是中文就用中文);若你要的是「忽略浏览器、首次一律英文」,去掉这一步即可。 ⚠️ 这一步的取舍是我的取值(可推翻) —— 你的口径是「默认英语」,我按「兜底英语 + 浏览器命中则跟随」实现。

验收口径增补:6 个平台页 + 注入文案在 en 下无空文案、无裸 key;无 cookie 首次访问应为英文;切到 zh 后刷新保持;既有中文行为零回归。

④ 尚未落地(现状差距)

项 现状 归属
平台 6 个静态页 + 注入浮层文案 仍是纯中文(<html lang="zh-CN"> + 硬编码文案,无 i18n 运行时) R5 待做(排最后,见 §10.3)
实例内官方 UI 已默认英语(FALLBACK_LOCALE=en) 官方已给 ✅
自研插件(business-plugins 等) 已双语(跟随实例语言) ✅ 已完成(档案 37b / 60 / 67)
开源仓库文档 英文为主 + 中文副本 ✅ 已完成(R-O13)

⑤ 落地进展(2026-09-15 · 用户要求「优化插件代码 + 登录窗口与设置中加语言切换」)

项 状态
平台 i18n 运行时 ✅ 新增 web/i18n.js:词条表(en / zh 两套,扁平 key)+ 极小运行时(data-i18n / data-i18n-attr 就地替换)+ 语言下拉组件(容器标 data-i18n-switch)。默认 en;语言 id 与官方 LOCALE_IDS 对齐(en / zh,便于日后透传给实例)
登录窗口语言切换 ✅ web/login.html 全量词条化 + 卡内语言下拉;缺 key 回退英文并 warn(不出现空文案/裸 key)
设置中用户设置栏语言切换 ✅ 插件 @dsh-local/business-plugins 0.3.20 新增用户可见 section「偏好设置」(id preferences,order 99,排在模型设置之前),内含「界面语言」下拉,走官方 ctx.locale.getLocale() / ctx.locale.setLocale(id)(零官方改动)。已铺发开发服务器:admin bundles=7 / guest bundles=6,均含该插件(未被静默摘除)。pnpm/npm pack 产物 md5 本地=服务器
断言(防回归) ✅ scripts/verify-platform-admin-section.mjs:section 数量断言 3 → 4、"非 admin 2 → 3 个分区",并新增 preferences 的渲染断言(断言输出实测含「界面语言 | English | 中文」)。npm run verify 全绿

⚠️ 与 §10.2 的一处有意偏差:cookie 名由 dshs_lang 改为 dsh_lang —— scripts/verify-static.mjs 有去痕迹约束 BANNED(用户可见面不得出现平台内部名),前端资产含该串会被判不合格(本次已实测被拦一次)。

⏳ 仍未做:其余 5 个平台页(index / register / portal / admin / wake)与注入浮层文案的词条化(同一套运行时,按页推进即可);verify-static.mjs 的「静态页不得出现裸中文文案」防回归判据(待 6 页都词条化后再开,否则会误拦未迁移的页)。