**背景**:这些改动原属本工作区另外几个会话(T01/T02 等),**那些会话已被用户删除** ⇒
工作树里的成果处于"无主"状态,一次错误 checkout / 覆盖即**永久丢失** ⇒ 代入库保全。
口径遵循本项目**先例**(`39a1f2e` / `b617cdb`:**别人的活,代入库并在提交信息里注明**)。
**内容**:档案 101「能力管理」改名 + 页内 tab 分页|档案 102 语言切换搬入「用户设置」|
`07-实例UI分区登记表.md`|`scripts/find-ui*.mjs`(UI 元素定位工具)|`poc/portal-entry/`(0.5.3)|
`src/web/locale-pref.ts` + `home-files.ts`(语言偏好持久化)|`test/locale-pref.test.mjs`|
`package.json`|`BRIEF.md` / `INDEX.md` / `docs-manifest.json` / `03-路线图与待办.md` / 档案 100 增量。
**已做最小健全性检查**(⚠️ **未跑完整构建 / 单测** —— 那是原会话的验收职责,本次只求"不丢"):
- JSON 合法:`package.json` / `poc/business-plugins/package.json` / `docs-manifest.json` ✓
- 4 个 TS 文件 `{}`/`()` 配平 ✓;新增文件均非空 ✓
- 规模:13 文件改动 +340/−210,新增 12 条
**未 push**(按 §4 提交边界:用户说"提交",未说"推送")。
11 KiB
102 · 语言切换从「偏好设置」搬进「用户设置」,并撤除偏好设置分区
- 日期:2026-09-15
- 触发:用户「把偏好设置中的 语言切换功能放到用户设置中,去掉偏好设置这个栏目,并检查语言切换功能是否正常」
- 对象:
poc/business-plugins(0.3.22 → 0.3.23)+poc/portal-entry(0.5.2 → 0.5.3) - 状态:✅ 已实施并投放(两实例磁盘层已核)
TL;DR|结论:语言切换搬进
@dsh-local/portal-entry的「用户设置」分区(iduser-settings,order 103),business-plugins的「偏好设置」分区(idpreferences,order 99)整块撤除。 关键:语言是用户级偏好 —— 与「账号 / 退出登录」同区即可,单开一个分区就是重复入口。 不做:不动官方包(R2);语言切换实现仍是官方扩展点(ctx.locale),零官方改动。
一、先澄清一件事(这轮排查的最大结论)
「用户设置」是我们自己的 bundle 提供的,不是官方分区:
| 项 | 值 |
|---|---|
| section id | user-settings |
| order | 103(排在 能力管理 101 / 系统管理 102 之后) |
| label | "\u7528\u6237\u8bbe\u7f6e" ← 转义 unicode 存的 |
| 提供者 | @dsh-local/portal-entry(client 面 lib/client.js) |
| 源码 | 仓内 poc/portal-entry/(← 本轮补入,之前不在仓里) |
⚠️ 因为它是转义形态,我按 UTF-8 字面量 grep 用户设置 在全域(官方树 / 平台代码 / 文档库 / profile)
搜了十几轮全部落空,最后是"在活着的 profile 里 grep -l settings.section @dsh-local/*/lib/client.js"才定位到。
⇒ 已把这条写进 07-实例UI分区登记表.md §二(含"同时搜 UTF-8 与 \uXXXX 两种形态")。
二、改动
2.1 poc/portal-entry 0.5.2 → 0.5.3:语言行搬入「用户设置」
- 位置:
UserManagementSection里,插在「账号/角色」(仅 admin 显示)与「退出登录」之间。 - 实现(官方扩展点,零官方改动):
inject:["slots"]→["slots", "locale"];- 读
ctx.locale.getLocale(),切ctx.locale.setLocale(id),语言项en/zh(= 官方LOCALE_IDS;en是官方 FALLBACK,即默认英语); - 本行自己的双语文案走
ctx.locale.register(PE_NS, {zh,en})+ctx.locale.bind(PE_NS)(与本仓business-plugins同一套姿势); - 切换后
setTick()强制重渲染本行(官方 locale 的通知不会打到本组件); - 语言 id 读不到时退回
en,ctx.locale缺失时只告警、不整区崩。
- ⚠️ 颜色沿用本文件硬编码(
#f9fafb/#43464d/#c9cdd4)—— 本区渲染在 DARK 主题, v0.4.2/v0.4.3 的结论是这里不能依赖--dsw-*(其 fallback 会让文字与底色同色而看不见)。 - 源码入仓:仓内此前没有
poc/portal-entry/(只有服务器上的 tgz)⇒ 本轮把部署中的 0.5.2 源码 取出来放进poc/portal-entry/,再在其上改。约定:自研 bundle 的产物只能从仓内源码构建。
2.2 poc/business-plugins 0.3.22 → 0.3.23:撤除「偏好设置」
撤掉三样东西(只改用户可见面,不留残迹):
PreferencesSection组件(整段,含其 doc 注释);ctx.slots.inject("settings.section")里 idpreferencesorder 99 的注册;- zh/en 各 3 条
pref.*词条(pref.label/pref.lang/pref.hint)。
⇒ 本 bundle 现在只注册 2 个分区:model-settings(100) + business-plugins(101)(admin 另加 platform-admin 102)。
语言切换一个字都没丢 —— 原实现就是 ctx.locale.getLocale()/setLocale(),搬过去后是同一套 API。
2.3 断言同步(防回退)
| 脚本 | 改动 |
|---|---|
scripts/verify-platform-admin-section.mjs |
「非 admin 注册 3 个分区」→ 2 个;「admin 注册四个」→ 三个;新增 「preferences 分区已撤除(不再注册)」 断言;原「偏好设置」功能断言整块移除 |
scripts/verify-portal-entry.mjs |
新增(22 条):源码在仓内 / host 半边 lib/index.js 在(v0.5.1 事故防线)/ id·order·label(按转义形态断言)/ inject 含 locale / get·setLocale 调用 / 双语词典 / en·zh 两项 / 语言行确实 push 进 rows / 颜色硬编码 / R3:无 exports.default 赋值 |
仓根 package.json |
npm run verify 链尾接上 verify-portal-entry.mjs |
三、验收
| # | 口径 | 结果 |
|---|---|---|
| A | npm run verify(build + 单测 + 10 个 verify 脚本) |
✅ 全绿(含两个新/改脚本) |
| B | 产物 | ✅ business-plugins-0.3.23.tgz(72,037 B)· portal-entry-0.5.3.tgz(8,201 B,4 文件=两个半边都在)均已投放,与本地同名 |
| C | 投放 | ✅ ensure-biz-plugins.cjs --all --restart + ensure-portal-entry.cjs --all --restart ⇒ admin/guest bundles 7 / 6 完好(未被 pruneBrokenFileDeps 摘依赖) |
| D | 磁盘层(06 §7.3 ①) | ✅ 两实例:business-plugins 0.3.23 且 id: "preferences" 命中 0、能力管理/tab 仍在;portal-entry 0.5.3 且 key: "lang" 命中 1、inject 含 locale |
| E | 实例侧 / 浏览器实测 | ⚠️ 未做,原因见 §四 |
四、未完成:实例侧与浏览器实测(原因 + 回头条件)
阻塞点(客观):平台已切集群 ⇒ 新用户按容量落 Worker w-106,而 47 → 106 无 SSH 路由
(106 经反向隧道连到 47)⇒ 本机脚本走不通"建临时用户 → 进实例 → 抓壳页 → 点 UI"这条链;
R4 又不允许用真实账号(guest/admin)登录做实测。
已做到的:磁盘层(§三 D)确认两个实例 profile 里就是新代码,而实例壳页的 combo 是对
profile/node_modules/**/client.js 的拼接 ⇒ "服务端会返回新内容"由 D 强推,但未经端到端实测。
回头解决的条件:拿到「新用户落哪台 Worker + 到那台机的通道」后,用档案 100 §8.4 那套
(poc-ui-user.cjs + agent-browser)补齐三段式与视觉验收(重点看:语言行是否在「用户设置」里、
切到 English 后本行文案与官方 UI 是否立即变、以及 settings.yaml 的持久化行为见下)。
⚠️ 一条要如实告知用户的行为边界
官方 dsh-client-locale README 原文:语言选择立即生效;loopback 页面会持久化到
$DSH_HOME/settings.yaml,非 loopback 页面只为当前进程保留。平台是"浏览器经域名访问远程服务器"
⇒ 属非 loopback ⇒ 切换在新开页面/重启后不保证保持。这不是本次改动引入的(原「偏好设置」用的是同一套 API),
但用户会说"我切了怎么又变回去" ⇒ 已在 §五 记为待观察项。
五、待观察 / 技术债
- 持久化:见 §四末条。若要"记住选择",得走平台侧写
settings.yaml(属新需求,不是本单)。 - ⚠️
scripts/ensure-portal-entry.cjs/ensure-biz-plugins.cjs仍用better-sqlite3读/var/lib/dshs/dshs.db枚举用户 —— 而集群模式下权威库是 PG(DSHS_DB_URL)。 本轮两脚本对新用户/存量用户都工作正常(说明过渡期 SQLite 仍在被维护),但这是一条隐患, 属集群化的收尾项(不是本单 lane)⇒ 只报告,未动手。 .pnpm里的历史副本(@[email protected]/0.3.4/0.3.8/0.3.11等)与文档库04-调整方案/poc/portal-entry(0.5.1 快照)都属"同名旧副本",排查时别当现行(已写进07 §二)。
六、语言偏好持久化(2026-09-15 追加,用户:「A B 都按最优方案处理」)
问题(A)与需求(B)其实不冲突 —— 最优解是替官方把它的设置写进它自己的文件:
6.1 做法(零官方改动、零新增平台状态)
| 层 | 改动 |
|---|---|
平台(新)src/web/locale-pref.ts |
reconcileLocalePreference(text, 'en'|'zh') —— 按行对账,只在 settings.yaml 顶层 locale: → preference: 那两行上动手(不整份 YAML 解析,注释/顺序/别人的块一律不动);幂等(值相同 ⇒ changed:false) |
平台(新)src/web/home-files.ts |
把原先内联在 server.ts 闭包里的 readTextOrEmpty / writeHomeFile 抽出来共用(writeHomeFile = 备份到平台目录 + 写 + chown 给 home 属主,漏最后一步实例读不了 —— R10 同族)。server.ts 改为 import(行为不变) |
平台(新路由)POST /api/me/locale |
requireAuth;{locale} → 写 <home>/settings.yaml;非法值 400 invalid_locale |
客户端 portal-entry 0.5.3 → 0.5.5 |
pickLocale() 在 setLocale() 之后额外调 POST /api/me/locale(credentials:'include',跨子域);持久化失败不影响本次切换(就地已生效) |
官方键名出处:dsh-client-locale 的 host 半边 settings schema 用的就是 locale / preference
(⛔ 不要凭猜 —— 首版我按 locale.preference 写是核对过的)。
6.2 为什么这是"最优"而不是"另造一套"
- A(守官方语义):写的正是官方设置文件的官方键 ⇒ 实例启动时官方运行时读自己的文件即生效;
- B(记住选择):用户的选择跨页面 / 跨重启保留;
- 单一来源:平台不新增"语言状态"(没有新表 / 新列),唯一事实仍是
settings.yaml; - 失败可降级:平台路由挂了 / CORS 拦了 ⇒ 只是"记不住",不影响本次切换(catch 兜底)。
6.3 验收
| # | 口径 | 结果 |
|---|---|---|
| A | 单测(新增 test/locale-pref.test.mjs,9 例) |
✅ 全过:建块 / 插行 / 换值 / 幂等 / 注释与其它块不动 / CRLF 保持 / 行数不增 |
| B | npm run verify |
✅ 全绿(test 45 例;含新增打包防线与持久化断言) |
| C | 平台路由上线 | ✅ 送 4 个编译产物(lib/web/{home-files,locale-pref,server}.js、lib/web/routes/auth.js)+ restart dshs;冒烟 POST /api/me/locale → 401(有鉴权 = 路由存在) |
| D | 投放 | ✅ portal-entry-0.5.5.tgz 两实例 bundles 7/6 完好;磁盘层 0.5.5、api/me/locale 命中 1、包内无残留 tgz |
| E | 浏览器实测 | ⚠️ 仍未做(同 §四:新用户落 w-106、47→106 无路由) |
6.4 ⚠️ 部署方式的一个实测坑(重要,别踩)
打算按常规 cd /opt/dshs && npm run build && systemctl restart dshs 部署时发现:
服务器 /opt/dshs/src 是"集群化之前"的旧源码(server.ts 里还是 K8sSpawner,git 停在 8390eb3 初始提交),
而线上 lib/ 却是集群版编译产物 —— 源码与产物不一致。
⇒ 在那边直接 build 会把线上编译回旧版(危险)。本次改为只送编译产物(4 个文件),
并已核对"线上 lib/ 与我本地编译版只差本次改动"再动手。
⚠️ 这条属集群化收尾项(另一 lane):/opt/dshs 的 src 需要与 git 基线对齐,否则任何一次服务器侧 build 都是事故。