Files
dsh_shenxian/dsh-server-docs/04-调整方案/57-设置面板用户管理入口与全员安装.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

11 KiB
Raw Blame History

57 · 设置面板「用户管理」入口:改名、移位、全员安装

  • 日期:2026-09-11
  • 状态:✅ 已实施并验证(服务器已生效;本地待提交)
  • 触发:用户要求「将设置中的平台管理改为用户管理,放在功能插件下面;普通用户也需要有用户管理入口,里面只有退出按钮」
  • 关联:档案 16(插件模型)、36/37(功能插件分区)、26 §C4(自建 client bundle 契约)、28(pnpm store 污染修复)、39(可见面收窄)、15(角色 patch:普通用户被隐藏的官方分区)

一、需求拆解与现状

# 需求 现状(改动前) 本次处置
1 「平台管理」→「用户管理」 portal-entry 注册 section:id: platform、label: 平台管理、order: 100 id: user-management、label: 用户管理
2 排到「功能插件」下面 order 100 < 功能插件 101 → 在上面 order 102(官方分区:models=10 / plugins=15,故 102 排最后)
3 普通用户也要有该入口 @dsh-local/portal-entry 只装在 admin 的 profile(dsh.profile.bundles);guest 的 bundles 里没有它 → 普通用户设置面板里根本没有这个分区 新增安装器 ensure-portal-entry.cjs,全员铺设
4 普通用户进去只有退出按钮 原实现固定渲染「平台门户地址」+「打开管理台」+「退出登录」 按角色分支:role === 'admin' 才渲染门户地址与「打开管理台」;其余只渲染「退出登录」

关键认识:需求 3 不是"改个显隐条件"就能满足的 —— 分区是否出现在普通用户的设置面板,取决于该用户的 profile 有没有加载这个 bundle。所以必须同时解决"插件分发"问题。

「功能插件」名称无需改动:business-plugins 的 section.label 自 v0.2.0 起就是「功能插件」(官方 README 亦用此译名),代码与 UI 里都不存在「系统插件」字样。


二、实现

① 插件本体 @dsh-local/portal-entry → v0.5.1

改动集中在 client 面(lib/client.js),host 面(lib/index.js)未动。

项 改动
section id platform → user-management
section label 平台管理 → 用户管理
section order 100 → 102
内容 新增 React.useState/useEffect 角色探测;管理员渲染「平台门户:」+「打开管理台」(蓝)+「退出登录」(红);非管理员只渲染「退出登录」
角色来源 GET <门户>/api/auth/me(requireAuth),credentials: "include" —— 与 business-plugins 调 /api/plugins/mine 同一套跨子域机制
失败降级 fail-closed:探测失败 / 非 admin / 未登录 → 按非管理员渲染,绝不展示查看者可能打不开的管理台入口
保持不变 只导出 apply + inject(禁 exports.default,红线 R3);配色继续硬编码(v0.4.3 结论:设置面板是暗色主题,--dsw-alias-label-primary 与 --dsw-alias-button-elevated-fill 同为 #f9fafb,var() fallback 永不生效)

② 安装器 scripts/ensure-portal-entry.cjs(新增)

幂等:按「node_modules 已装版本 + dep spec 是否指向本次产物 + 是否在 bundles」三条件判定。

安装姿势沿用 ensure-workspace-picker.cjs 的 2026-09-11 修复版:

  1. 产物从 /opt/dsh/artifacts/portal-entry-*.tgz 取版本号最大者(升级只需丢新包,脚本不用改);
  2. 暂存到 <userRoot>/home/.dsh-stage/(/opt/dsh 是 drwx------ root,用户 uid 读不到其中文件)+ chmod 444 + chown <uid>;
  3. setpriv --reuid <uid> env HOME=<home> pnpm add ... file:<staged>,profile 是 pnpm workspace 根时加 -w;
  4. store 位置自适应(见 §三.1);
  5. reconcileBundles():把 dependencies 里声明了 dsh.bundle.patch 的包并入 dsh.profile.bundles;
  6. --restart 时停该 uid 名下全部 dsh-<uid>-*.scope(下次访问自动拉起才加载新 bundle)。

用法:node scripts/ensure-portal-entry.cjs [用户名…] [--all] [--user-id <uuid>] [--tgz <path>] [--dry-run] [--restart]

③ 新用户自动化

位置 改动
/usr/local/bin/provision-new-users.sh 末尾追加 [portal-entry] 段(与既有 [picker] 段同一时机、同一 best-effort 策略);本次一并入库 scripts/provision-new-users.sh(此前只存在于服务器,仓库里没有)
/etc/cron.d/dsh-maintenance 新增 15 4 * * * 每日巡检兜底,日志 /var/log/dsh-ensure-portal-entry.log(与既有 04:30 ensure-biz-plugins 同一模式)

未改动 TS 代码 —— 因此不需要 build、不需要重启 dshs(即时生效,只在实例重启后加载新 bundle)。


三、踩到的坑(都值得记)

1. ERR_PNPM_UNEXPECTED_STORE —— 存量安装不能换 store 位置

首次执行安装器时两个用户都秒失败:

ERR_PNPM_UNEXPECTED_STORE  Unexpected store location
The dependencies at ".../profiles/web/node_modules" are currently linked from the store at
  "<userRoot>/ws/.local/share/pnpm/store/v3"
pnpm now wants to use the store at "<userRoot>/home/.pnpm-store/v3"

根因:存量 profile 的 node_modules 全部诞生于「HOME=<ws> pnpm add」时代,node_modules/.modules.yaml 里 storeDir 记的是 ws 内路径;档案 28 把脚本改成了"store 放 <home>/.pnpm-store",但存量从未重装过(版本没变 → 幂等跳过),于是脚本的新姿势与磁盘上的旧安装互相矛盾。pnpm 要求换 store 必须 pnpm install 重建全量依赖(联网重拉整棵 dsh 依赖树)。

处置:安装器改为读 node_modules/.modules.yaml 的 storeDir 并沿用;只有全新 profile(无 node_modules)才用 <home>/.pnpm-store。cache 同理(沿用 <userRoot>/ws/.cache/pnpm 可免重拉 metadata)。

⚠️ 同一个坑仍在 ensure-workspace-picker.cjs 里(它无条件用 <home>/.pnpm-store)→ 一旦 picker 需要升级(版本号变化),会同样撞 UNEXPECTED_STORE。列为待办 §五.1。

2. 上传构建包时漏了一个文件 → 全实例崩溃循环

第一版 portal-entry-0.5.0.tgz 只有 lib/client.js + package.json + cordis.patch.yml,漏了 host 面 lib/index.js(组装构建目录时手工逐个 cp 漏项)。后果是所有实例启动即崩:

failed to import loader entry portal-entry (@dsh-local/portal-entry):
  Cannot find module '.../node_modules/@dsh-local/portal-entry/lib/index.js'
→ 崩溃循环(1→2→4→8→16s)→ 熔断 crash-loop-circuit-open

处置:改为 cp -r <源目录>/. <构建目录>/ 整目录复制;打包后强制校验包内清单与源清单一致:

diff <(cd src && find . -type f | sed 's|^\./|package/|' | sort) <(tar tzf out.tgz | sort)

3. pnpm 同版本缓存

同名同版本的 tgz 即使内容变了,pnpm 也会复用缓存里的旧包 → 修复必须升版本号(0.5.0 → 0.5.1),不能原地覆盖。坏包已隔离到 /opt/dsh/backups/portal-entry-0.5.0-BAD-missing-index.tgz。

4. grep -c $'\r' 在 MSYS 下会假阳性

用它检查行尾时报告本机文件"36 / 239 行含 CR",而两端 md5 完全相同 → 该测法不可信。可靠做法是 od -c 看首行字节(实测两端均为 b a s h \n,即 LF,与项目惯例一致)或 file(无 "with CRLF line terminators")。


四、验证记录(服务器实测)

项 结果
安装 admin: ✓ v0.5.1(bundles=5,含本插件=true)、guest: ✓ v0.5.1(-w,bundles=5,含本插件=true)
profile 配置 两个用户 dependencies["@dsh-local/portal-entry"] → file:<home>/.dsh-stage/portal-entry-0.5.1.tgz(持久位置,不会进 ws 或被清理);dsh.profile.bundles 均含该包
实例启动 停 scope 后访问即拉起,dsh web: http://127.0.0.1:<port>/?token=… 正常打印,无加载错误
bundle 加载 实例首页(200 / 24.8KB)的 client bundle 清单含 @dsh-local/portal-entry/client.js,URL 带新 rev=89891954539546d8-44
服务端内容 实例内 client.js 含 user-management ×3、order: 102、/api/auth/me、角色分支
跨子域 CORS GET https://alotbuy.com/api/auth/me,Origin: https://admin.alotbuy.com → 200 + access-control-allow-origin: https://admin.alotbuy.com + allow-credentials: true,响应体 {"user":{…,"role":"admin"}}
角色取值 DB:admin → role=admin(会看到「打开管理台」)、guest → role=active(只看到「退出登录」)
新用户链路 执行 provision-new-users.sh → [portal-entry] admin: skip(已装 v0.5.1 且在 bundles)、guest: skip,幂等生效
仓库状态 /opt/dshs 仅 ?? scripts/ensure-portal-entry.cjs(新文件),工作树干净

未做的验证:浏览器内的实际渲染(本机无 agent-browser/Chromium,安装成本 ~500MB 且需联网下载)。机制链路已全部实测(bundle 加载 + CORS + 角色数据 + 注册代码),渲染机制沿用 v0.4.x 已生产验证的实现。请管理员硬刷新会话页 → 设置面板底部的「用户管理」确认位置与按钮。


五、待办与风险

  1. P2|ensure-workspace-picker.cjs 的 store 位置隐患(§三.1):它无条件使用 <home>/.pnpm-store,与存量 node_modules 的 storeDir 不一致 → 下次 picker 升级会失败。修法同本档案:读 .modules.yaml 沿用旧 store。
  2. P2|存量 dep spec 指向 ws:business-plugins / workspace-scoped-picker 的 dependencies 仍写 file:<userRoot>/ws/*.tgz,而 ws 会被 ws-cleanup 定期清理 → 将来任何 pnpm install 都会因源文件缺失而失败(当前不影响运行:node_modules 已就位)。新装的 portal-entry 已改用 <home>/.dsh-stage/,其余两个包建议照此迁移。
  3. P3|portal-entry 的 host 面是失效的 PoC:lib/index.js 仍注册 portal_ping 工具,其实现 fetch(http://127.0.0.1:3080) 已被档案 39 的 loopback 封锁 → agent 会看到一个永远失败的工具。建议移除或改为不依赖 loopback 的探针(档案 55 已记)。
  4. P3|插件源码位置不统一:portal-entry 源码在文档库 04-调整方案/poc/portal-entry/,business-plugins、workspace-scoped-picker 在代码库 poc/ 下。同一类资产两处存放,容易漂移。建议统一到代码库 poc/。

六、回滚

# 1. 回退插件到 0.4.3(admin 有旧包;guest 直接卸载即可恢复"没有该入口")
node /opt/dshs/scripts/ensure-portal-entry.cjs --tgz /opt/dsh/artifacts/portal-entry-0.4.3.tgz --all --restart
# 2. 或从 bundles 摘除(普通用户即不再显示该分区)
#    编辑 <userRoot>/home/profiles/web/package.json 的 dsh.profile.bundles,去掉 @dsh-local/portal-entry
# 3. cron 兜底行:注释 /etc/cron.d/dsh-maintenance 里 04:15 那行
# 4. provision 挂载:注释 /usr/local/bin/provision-new-users.sh 末尾的 [portal-entry] 段

回滚后必须重启实例才生效(bundle 在实例启动时加载)。