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 一律写「远程服务器」。
44 KiB
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 建议同一窗口) |
六、待用户
- R1 是否现在做(一次重启窗口,断在线用户 2–5 秒)?我建议 R1 + R2 同一窗口,一次断线办完。
- R3 的"双名期"是否赞成(先加 symlink/双读,稳定后再删旧名)?这是把"命名彻底换掉"的风险压到最低的走法。
- 是否需要我把 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 ⚠️ 部署中踩的两个坑(如实记录)
__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 自证"。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 更稳 |
| 我的建议 |
✅ 用户已拍板(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 镜像) |
这带来三个真实摩擦(都是本项目实际踩过的,不是假想):
- 同一个改动要分两次走:改功能往往同时要改档案/UI 规范 ⇒ 一次交付要 2 个 commit + 2 次 push + 1 次 scp 对账;漏一边就出现"代码说 A、文档说 B"。
- 版本对不上:文档里写的"现状"绑的是某一个代码 commit,但两库各走各的 ⇒ 复现历史时无法用一次 checkout 同时拿到"当时的码 + 当时的文档"(开源导出正因此需要单独冻结版本)。
- 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 方案(不引框架,最小实现)
- 词条表:
assets/i18n/zh-CN.json+en.json(扁平 key,如login.submit、wake.starting)。 - 极小运行时:
assets/i18n.js(约 60 行,放web/供静态页用;注入脚本用同一份词条):- 选语言优先级:
?lang=→ cookiedshs_lang→navigator.language→ 兜底zh-CN; - 页面用
data-i18n="key"(文本)/data-i18n-attr="placeholder:key"(属性)标注,运行时就地替换; - 缺 key 时回退到中文原文并 warn(不出现空文案)。
- 选语言优先级:
- 切换入口:登录页与门户页页脚一个语言下拉(
?lang=en+ 写 cookie),不新增页面。 - 抽文案纪律:所有用户可见中文必须是词条 key;
verify-static.mjs加一条防回归判据(静态页里不得出现"裸中文文案",白名单放data-i18n与注释)。
10.3 为什么排最后(三条硬理由)
- 它会横穿几乎所有页面 —— 在 R2(门户管理就地化)落地前抽文案,R2 一改就等于白抽一遍;
- R3 的内部改名还没走完 —— 词条文件的目录(
assets/i18n/)与将来dshs命名体系要对齐,先改完再放词条更省事; - 它是纯体验增强、不修任何事故 —— 而今天已确认的 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
zhanden, 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 页都词条化后再开,否则会误拦未迁移的页)。