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 一律写「远程服务器」。
500 lines
44 KiB
Markdown
500 lines
44 KiB
Markdown
# 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(关键 —— 这条路不碰官方代码)**:
|
||
|
||
```js
|
||
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 文案的逻辑包) |
|
||
|
||
复现命令(逐包两条计数):
|
||
|
||
```sh
|
||
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 页都词条化后再开,否则会误拦未迁移的页)。
|
||
|