Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下17.md
T
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

377 lines
46 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作日志 · 2026-09(第 18 片)
> ⚠️ **本目录日志已按【月】分片**(2026-09-15 用户定,单文件 ≤50 KB):同月续片 `2026-09.md` / `2026-09-下.md` / `2026-09-下2.md` / `2026-09-下3.md` / `2026-09-下4.md` / `2026-09-下5.md` / `2026-09-下6.md` / `2026-09-下7.md` / `2026-09-下8.md` / `2026-09-下9.md` / `2026-09-下10.md` / `2026-09-下11.md` / `2026-09-下12.md` / `2026-09-下13.md` / `2026-09-下14.md` / `2026-09-下15.md` / `2026-09-下16.md` / `2026-09-下17.md` / `2026-09-下18.md` / `2026-09-下19.md` / `2026-09-下20.md` / `2026-09-下21.md` / `2026-09-下22.md` / `2026-09-下23.md`
> **写入约定**:一律 append 到**当月最后一片**;该片超 50 KB ⇒ 新建 `2026-09-下N.md`;⛔ **不要再按日新建 `2026-09-DD.md`**。
> **覆盖来源**:2026-09-13.md
> **上一片**:`2026-09-下16.md` **下一片**:`2026-09-下18.md`
---
## 20:35–20:5x · R2 视觉层第 2 轮:「系统管理」改按门户源码照抄(business-plugins 0.3.3)
**触发**:用户第 2 次反馈「点击卡片后的弹窗样式还是没变呢,说了要用之前门户的里面的对应样式」。
### 查明两个真因(都不是猜)
1. **上一步改动没出厂**:`poc/business-plugins/lib/client.js` mtime `20:33` > 最新产物 `business-plugins-0.3.2.tgz` mtime `20:17`。
2. **更要命**:上一版只有「用户管理」1 个弹窗是门户表格;另外 4 个(候选池/运行时/存储/当前实例)**仍是裸 `div` + 内联样式** ⇒ 点开即「毛坯房」。
3. 方法论错误:上一轮抄的是 `06-工作台UI规范` 的**抽象条目**(§4.5/§4.7),用户要的是 `web/portal.html` 里**已跑着的实际组件**。
### 改法(读门户源码,逐值对应)
| 元素 | 门户来源 | 落地类名 |
|---|---|---|
| 卡片网格/卡片 | `.nav-grid` / `.nav-card`(r10, 18px 16px, hover 上浮-2px + 蓝边 + `0 4px 12px rgba(47,111,237,.12)`) | `.pa-grid` / `.pa-card` |
| 计数胶囊 | `.pg-tab .pg-cnt` | `.pa-cnt` |
| **弹窗表格(5 个全改)** | `.table-wrap` + `table.tbl`(th 8px10px/600, td 7px10px, 行 hover 浅蓝) | `.pa-wrap` + `.pa-tbl` |
| 徽章 / 按钮 | `.badge` 四色 / `.btn-sm`(+`.danger`) | `.pa-badge` / `.pa-sm` |
| 弹窗头 / 标题 / 副标题 | `.card-h` / `.page-title` / `.page-sub` | `.pa-card-h` / `.pa-hd` / `.pa-sub` |
| 弹窗外壳 | 门户无弹窗 ⇒ `.card`(r10/18px) + `design.css --shadow` | `.pa-overlay` + `.pa-modal` |
| 表格空态 | 门户 `emptyRow()` | `.pa-tbl td.pa-empty-cell` |
仅颜色走 dsh token `--dsw-*`(须随 dsh 主题);字号/间距/圆角/动效与门户**逐值一致**。新增 zh/en 词典键:`pa.colName/colVer/colDesc/colItem/colValue/colUsed/detail/close`。
### 三层验收(这次不只看单测)
1. **单测**:`scripts/verify-platform-admin-section.mjs` 新增「结构层」8 项断言(含「**5 个弹窗全部** = 门户表格」)→ 全绿。
2. **实装**:`md5 5800ba4f77e1c786063aed8a91f9f4b4` 三处一致(本地产物 / admin profile / guest profile),旧标记 `pa-go`/`.pa-n{` = 0。
3. **端到端**:`mksess.cjs` 临时会话 → 实例首页 200 → 模块 URL `rev=fd084d6aa33a` → 取回 bundle 4.26 MB:`pa-empty-cell` 3 / `pa-card-h` 4 / `pa-overlay` 2 / `pa-cnt` 3 / `pa-grid` 3 / `pa-tbl` 10 处,旧标记 0;**临时会话已删(1 行)**。
> 缓存不是原因:`src/supervisor/proxy.ts:383` 已对 `/plugins/`、`/assets/` 下发 `Cache-Control: no-cache`,且模块 URL 自带内容 `rev=<sha1>`。
### 顺手清掉一个自作的地雷(如实记录)
打包脚本第一次因断言锚点写错(`description` 不以 `v0.2.8` 开头)**在写文件前抛错**,但我已先跑了 `npm pack` ⇒ 生成了一个**内容是新版、版本号仍写 0.3.2** 的本地 tgz。**已删除该文件**,改用正确锚点重新升到 **0.3.3**(服务器侧 `business-plugins-0.3.2.tgz` 是真正旧版,未受影响)。
### 交付
- 产物 `business-plugins-0.3.3.tgz`;铺发 `node /opt/dshs/scripts/ensure-biz-plugins.cjs --all --restart` ⇒ `admin bundles=6` / `guest bundles=7`。
- 代码仓提交 **`b9540a8`** → 已 push `origin master`;文档库提交 **`f1a0da1`**(档案 82 §10.7)→ 已 push `origin main`;档案 82 已 tar 管到 `/opt/dsh/docs`,双端 md5 `456a2a55c3648ab760359c15d803cbfa` 一致。
- 顺带修**校验脚本自身缺陷**:`text()` 树遍历 `depth > 12` 会把 `表格>tbody>tr>td>文本` 截断(表头有、行文本丢)→ 放宽到 20。
### ⚠️ 收尾状态(给下一次接手)
- **并行会话在跑**:技能归档副本同步时抢锁失败 —— 占用者 **`univer-e2e-confirm`**(20:48 起)。按 R9 **已停手**。
- **未完成(因抢不到锁)**:`skills/dsh-change-workflow/SKILL.md` 的**归档副本 + 服务器镜像**未同步(**工作副本已改好**,见下)。
- 文档库 `docs-sync-check.sh` 当前 141 一致 / **2 不一致**,两个都属并行会话在途:`docs-manifest.json`、`skills/dsh-opensource-release/SKILL.md`(另有档案 76 md 本地已改但服务器已同步 ⇒ 已一致)。**刻意不碰**。
### 技能更新(工作副本已改,待补归档副本)
`dsh-change-workflow` 改了 4 处:
1. **工具口径纠偏**:技能原写「agent-browser 是死路,别再试」与用户 2026-09-13 明令(「只允许 `agent-browser`(默认) / `browser-harness`;Playwright 全面禁止」)冲突 ⇒ 已改为「用户口径优先,旧实测降为背景」。
2. 阶段 4 新增第 7 条:**交付前必问「这份改动出厂了吗」**(源文件 mtime vs 最新 tgz mtime 的一秒自检 + 假产物陷阱)。
3. 阶段 4 新增第 8 条:**视觉验收三层**(单测 class / 实装 md5 / 端到端取 bundle)。
4. 阶段 4 新增第 9 条:**「参考门户样式」= 抄门户源码实际取值,不抄规范抽象条目**(`⛔ docs-status-sync.sh 已不存在` 等阶段 5 过期内容一并修正)。
### 20:5x · 档案 76 §六(端到端确认)已补 + 一次自己的操作失误(已修)
- 第四轮后再补一节「**§六 端到端确认**」:dock `active:false → true`(浏览器侧闭环)、turnTail 的 `-10` 占位者即本插件卡片、旧回合不回溯、以及「硬刷新会取消在途 client-platform 查询」这条平台级现象。四件套 rc=0。
- ⚠️ **我的一次失误(必记)**:`scp <两个文件> bt-server:/opt/dsh/docs/04-调整方案/` —— **多文件 + 单目标目录 = 两个都落进那个子目录**,于是 `docs-manifest.json` 被误放进 `04-调整方案/`,对账立刻报出「**仅服务器(待拉取)**」。⇒ **本库有子目录结构,scp 必须逐个文件、各自带正确相对目录**(文档库 CODEBUDDY 的同步链路一节已写明,是我没照做)。已 rm 误放副本 + 复对账(只剩另一会话的 `skills/dsh-opensource-release/SKILL.md` 待它自己同步)。
### 21:0x · 收尾:沉淀新技能 + 清掉三把技能的归档漂移
- **新增第 7 把技能 `dsh-plugin-diagnose`**(工作副本 `~/.workbuddy/skills/` + 归档副本 + 镜像 `/opt/dsh/docs/skills/`,三方 md5 `61bd08b7…` 一致,目录 700 / 文件 600)。内容 = 插件三层归属(host / client / 网关·原生绑定)+ 三把尺子:**inject 差集**(客户端半边静默挂死)、**glibc 直测**(原生绑定两类对策)、**产物插探针**(打包顺序废掉垫片);附 8 条实测踩坑与平台侧事实。
- **清掉三把技能的归档漂移**(记忆里挂着的历史遗留):`dsh-change-workflow`(995 vs 958 行)/ `dsh-decision-method`(491 vs 435)/ `dsh-feature-first`(284 vs 239)—— 工作副本为准同步到归档与镜像,**三方 md5 一致**。
方向安全性先验证过:归档独有行只有 3–5 行且**全是过时残留**(引用已删的 `docs-status-sync.sh`、「文档只写服务器不落本地」、已被推翻的「agent-browser 是死路」)⇒ 不是「归档更新」。
- 对账回收:只剩 **1 项不是我的**(`skills/dsh-opensource-release/SKILL.md`,另一会话改了未同步)。
---
## 21:00–21:2x · MCN 插件摘除「计划任务」+ 查明 `mcn-task` 会话分组真因
**触发**:用户问「为什么 admin 会话中一会儿就多出一个 `mcn-task` 会话分组 哪里来的」→ 接指令「把那个插件的计划任务删除」。
### 真因(源码级证据,不是猜)
`mcn-task` **不是平台的东西**,是 **`dsh-plugin-mcn-suite` 自己建的工作区分组**(名字是 09-12 用户定的统一名)。两个创建点:
| # | 位置 | 机制 |
|---|---|---|
| **A** | `lib/host/schedule/index.js` `ensureTaskWorkspace()` | 挂在 `apply(ctx)` 上 ⇒ **每次实例冷启动都跑**(进程内缓存随进程消失)⇒ 用户删掉分组后**下次启动又回来**。有按 title 查重,不会重复建,但会「复活」 |
| **B** | `lib/host/mcn/index.js` **291/405/511/613**(视频解析/改写/审稿/人设) | `mkdirSync` 后**直接 `reg.create(dir,"mcn-task")`,没先查重**(只有进程内缓存兜底)+ 建完 `setTitle("mcn-task")` 强制同名 ⇒ 只要 `dir` 变了就再建一个**同名**分组 |
**`dir` 为什么会变**:`config.js` 的 `resolveCreativeCwd()` **每次调用重读 `workspace.json`、取「路径最浅」的工作区当根** ⇒ 用户动工作区 ⇒ 根漂移 ⇒ dir 变。
现场(admin):`ws/mcnworkspace/mcn-task`(1 会话) + `ws/mcntimo/mcn-task`(0 会话) = **同名 2 个、路径不同** —— 正是「一会儿多一个」的形态。
### 本轮改动(0.3.9 → 0.3.10,只做用户要的那一项)
`lib/index.js`(HOST_PARTS 去掉 mcn-schedule + import;顺带修 `VERSION` 常量长期写 0.3.0 的笔误)|
`lib/client.js`(不再注册 `mcn-schedule` 入口 ⇒ 工具栏按钮靠 `scheduleEntry &&` 自动消失;删「更多功能」里未守卫的「自动任务」;**整块删 248 行**子包客户端段,`SchedulePage`/`mcnSched_*` 清零)|
`lib/host/schedule/` **整目录删除**|`tests/smoke.mjs` 删用例+重编号|`README.md` 功能页 5→4|`cordis.patch.yml` 注释|`package.json`。
### 验证(三层,全通过)
1. **实装**:admin/guest 均 0.3.10;`lib/index.js` md5 `919efdd9…`、`lib/client.js` md5 `f1a3cf7c…` **与本地产物完全一致**;`lib/host` 无 schedule;`HOST_PARTS=[mcn, voxemw-cloud]`;客户端子包 5、入口注册 0。
2. **端到端**:冷启动日志 `[mcn-suite] loaded v0.3.10(host 面 2 个:mcn, voxemw-cloud)`、**无任何 mcn-schedule 行**;`/mcn/schedule/list → 404`;**mcn-task 分组数 冷启动前 2 → 后 2(不再自动新增)**;下发 bundle(rev `9136d5916c17`)`dsh-plugin-mcn-schedule` 0 处 / `mcnSched_` 0 处。
3. **基线校验**(本次新增纪律,已写进技能):把线上 0.3.9 tgz 拉下解包与本地源码全量比对 ⇒ 除本轮 4 文件外**逐字节一致** ⇒ 「要改的 == 正在跑的」。⚠️ 我当时是**先改后验**,顺序错了(结论好但风险窗口白开)。
### ⚠️ 未解决(已写进 `dsh-plugin-dev/notes/2026-09-13-mcn-suite摘除计划任务与mcn-task分组真因.md`)
1. **B 处仍会新增同名 `mcn-task`** —— 三种走法各有取舍(合并进一个会打乱各功能独立工作目录;各给独立标题违背 09-12「统一名」;只加按 title 查重仍有目录语义问题)⇒ **需产品决策,未擅自改**。
2. **既有缺陷(非本轮引入)**:`/mcn/api/accounts` → **500 `unable to open database file`**。日志该句最早 **09-12 23:37:52**,今天 21:10(部署前)已累计 **81 次**。线索:`DB_PATH=join(homedir(),".dsh","mcn-plugin.db")`,而磁盘上实存的是 `<home>/mcn-plugin.db`(旧路径少一层 `.dsh`)。
3. `lib/client.js` 线上是**混用行尾**(CR=5222/LF=5547),本地纯 LF ⇒ 整文件 md5 必变(无语义影响)。
### 其它
- 回滚件:`/var/lib/dshs/business-plugins/.bak/dsh-plugin-mcn-suite-0.3.9.tgz`;升级脚本留档 `E://ProgramData//AI技能//aliyun-dsh-server//.workbuddy//tmp//upgrade-mcn-suite.cjs`。
- ⚠️ **该插件目录未被 git 跟踪**(上层仓 `D://dshworkspace` 里是 `?? ./`)⇒ 改它**没有 git 兜底**。
- 技能归档副本仍未同步:`dsh-change-workflow` 现在共 **2 处未同步**(20:5x 那 4 条 + 21:2x 的基线校验条),因锁被 `r2-portal-align` 占用。
---
## 21:0x–21:3x · 会话 `r2-portal-align`:「系统管理」弹窗按门户 6 功能页重做(business-plugins 0.3.3 → **0.3.4**)
**触发**(用户原话):「怎么设置系统管理里面还是老样子呢,和之前门户那个**功能名称**、**页面样式**完全不一样呢,你要不要把之前那个 portal 页面打开看看,**我要点开弹窗里面展示的内容是和 portal 点击功能后那种详细的表格和功能**」+「弹窗页面需要多大多大,不用做的太小」。
**先核事实(不是没出厂)**:实例 profile 解析 `@dsh-local/business-plugins` → `.dsh-stage/business-plugins-0.3.3.tgz`,`version = 0.3.3` ⇒ **第 2 轮(视觉)确实已生效**。「还是老样子」的真因 = 第 2 轮**只改了视觉层,没动信息架构**:管理项仍是自造的 5 项只读摘要(用户管理/候选池/运行时/存储/当前实例),而门户是 6 个功能页 ⇒ **名字与表格都对不上**。
**改造**(`poc/business-plugins/lib/client.js`,1624 → 2218 行):
- 新增 `PA_PAGES`(**6 页**,每页 `{icon,title,desc,sub,html(),init($,root)}`,`html` ≡ 门户 `renderXxx()`、`init` ≡ 门户 `initXxx()`)+ `PA_GROUPS`(← 门户 `renderHome()` 的 sec1 服务 / sec2 管理)+ `PortalPage`(`dangerouslySetInnerHTML` 注门户原文 + `useEffect` 跑门户 init)。
- **三处机械改写**:`document.getElementById` → 局部 `$`(弹窗子树内查,避免与 dsh 面板同名 id 撞车);`fetch` → `paReq`(跨子域 + `credentials:include`);`location.hash` 路由 → 弹窗内页签状态。
- 词典 `pa.*` 91 → **225 条**(zh/en 一一对应,新增断言把关),弹窗内 **182 条**文案逐字取自门户。
- CSS:门户页面级组件逐条译为 `.pa-*`(`.card→.pa-box`、`.btn*→.pa-btn*`、`.page-head→.pa-phead`、`.pg-tabs/.pg-tab/.pg-cnt→.pa-tabs/.pa-tab/.pa-tcnt`、`.dsh-bar/.dot→.pa-dshbar/.pa-dot`、`.pathbar/.crumb→.pa-pathbar/.pa-crumb`、`.upload-row/.hint→.pa-row/.pa-hint`、`.key-row→.pa-keyrow`、`.wl-*→.pa-wl*`、`.empty→.pa-empty`、`.toast→.pa-toast`)。
- 弹窗尺寸:`min(1440px,96vw)` × `min(92vh,1040px)`(对齐门户功能页 1440),头固定 = 门户 `.page-head`(← 返回 + 标题 + 副标题)+ 关闭。
- ⚠️ **口径变更**:推翻档案 82 §一 的「只读子集 + 写操作回跳门户」⇒ 现为**就地全量管理**(CORS 白名单已含 GET/POST/DELETE/OPTIONS)。
**顺带发现(未动手,只报告)**:
1. 🐛 门户 `web/portal.html` 的 `readAsBase64()` **缺 `await`**(`String(new Promise(…))` 恒得 `"[object Promise]"`)⇒ 门户**三个上传入口**(文件 / 技能 zip / 插件 tgz)**实际传的是空数据**。弹窗侧(0.3.4)已改为真 Promise,**门户侧未动**。
2. ⚠️ **档案 82 自身严重重复**:`# 82 · R2 管理面就地化` 标题 6 次、§一–§八 重复 6 份(1937 行里大半是重复)⇒ 待单独立项去重。
3. 门户文件夹行**没有任何可点提示**(无手型/无颜色)⇒ 弹窗补 `.pa-dir`(否则文件树没法导航)。
**出厂与铺发**:
- `dsh-local-business-plugins-0.3.4.tgz`(39,098 B;包内 4 条目,无 `.tgz`/无 `.bak`)。⚠️ 打包前删掉 `lib/client.js.bak-*` —— `.npmignore` **不排除** `.bak`。
- `scp → /opt/dsh/artifacts/business-plugins-0.3.4.tgz` → `ssh bt-server 'cd /opt/dshs && node scripts/ensure-biz-plugins.cjs --all --restart'` ⇒ admin/guest 均 0.3.4,`bundles=6`;已停 0 个 scope(本来就没跑,下次访问自动拉起)。
- **sha256 三方一致**:本机 = artifacts = 两实例 `.dsh-stage` = `15cbec22e342…`;实例内 `lib/client.js` 含 `PA_PAGES.files/plugins/runtime`、`function PortalPage`、`pa-box`×9 / `pa-tabs`×3 / `pa-dshbar`×4。
**验收**:`node scripts/verify-platform-admin-section.mjs` **全绿**(含新增的「zh/en 键集一一对应」「占位符键一致」「6 功能页同名 + 6 弹窗外壳 + 各页表头/按钮文本」);`node --check lib/client.js` 通过;`docs-audit.py` rc=0。
⚠️ **本轮踩到的验收盲区**:功能页正文走 `dangerouslySetInnerHTML`,class 只存在于**字符串**里 ⇒ 只看 React 节点的 `classes()` 扫不到 ⇒ 新增 `clsAll()`(同时扫节点与内联 HTML)。写这类断言时别再踩。
**档案**:`04-调整方案/82-…md` 追加 §十一(含口径变更表、落地清单、两处有意偏离、未动手的三项)。**注意该文件本身重复严重(见上)**。
### 21:2x · 同会话续做:官方插件列表拉高(0.3.4 → **0.3.5**)
用户:「系统设置 插件管理里面官方插件列表 **高度可以增加,距离底部 100PX 就行**」。
- 改前:`.pa-wrap` + 内联 `style="max-height:420px"`(我在 §十一 里为规避弹窗高度硬套的固定值)。
- 改后:新增 `.pa-plist{max-height:max(280px, min(calc(92vh - 470px), 580px))}`。依据写进 CSS 注释:弹窗高 `min(92vh,1040px)`,表上方固定消耗 ≈470px,表下方还有按钮行 + 结果区 + 内边距 ⇒ 反推使**列表底部距弹窗底部约 100px**;280px 下限防小屏压扁、580px 上限对应弹窗触顶。
- ⚠️ 门户原值 `calc(100vh - 520px)` 是整页布局取值,**弹窗内不适用**,别照抄。
- 出厂 `business-plugins-0.3.5.tgz`(39,544 B)→ scp artifacts → `ensure-biz-plugins.cjs --all --restart` ⇒ admin/guest 均 0.3.5;`sha256 1d29683770b4…` **本机 = artifacts = 两实例 `.dsh-stage` 三方一致**;实例内 `lib/client.js:654` 含 `.pa-plist{…}`。
- 验收:`verify-platform-admin-section.mjs` 全绿(CSS 断言已扩到 `.pa-plist`);`node --check` 通过;**防假产物自检**:源 21:21:41 < 产物 21:21:58 ✅。
- 档案 82 追加 **§11.7**。
---
## 21:40–22:0x · 会话 `user-byok-keys`:**模型密钥开放给用户自助配置(两层密钥)**(档案 85)
**需求**(用户原话):「能否把配置模型密钥的功能开放给用户自己配置,参考原本 dsh 模型设置功能,**用户自己配置模型自己用**」+补充「**admin 设置的 用户也可以用,和用户设置的区分开就行**」。
**先取证(关键:数据层本来就是 per-user,卡住的是路由与注入)**:
- `src/web/server.ts:66-78` 改前 = 「统一 KEY 模式」,`resolveApiKey = async (_userId) =>`(**下划线参数名 = 故意忽略**),永远注入 `admins[0]` 那把;注释原文「用户不可自配」。
- `src/web/routes/auth.ts` 的 4 个 `/api/me/keys*` 路由**全部 `requireAdmin`**。
- 但 `credential_vault(user_id, key_name, secret_ref, enabled)` + `getEnabledCredentialKeyRef(userId)` 天生 per-user ⇒ **不用新建体系,放开 + 加回落即可**。
- 线上现状(db 实测):只有 `admin/deepseek-main/enabled=1`;guest **一行都没有** ⇒ guest 一直靠回落 admin 在跑。
**改动**:
- `server.ts`:`resolveApiKey(userId)` **两级** —— ① 用户自己的启用 key → ② 回落 admin 的「平台共享密钥」(抽 `decryptRef()` 复用解密 + 损坏兜底)。
- `auth.ts`:4 路由 `requireAdmin` → **`requireAuth`**;新增 `keySourceOf(userId)` / `sharedKeyInfo(userId)`(含 `ownerIsMe`)/ `refreshAfterKeyChange(userId, role)`(**admin ⇒ `restartAllMains()`;其他人 ⇒ 只 `restartMain(自己)`**)。
- `client.js`:新增 **`MyKeysSection`** + `settings.section` **`id: my-keys`, order 103,无条件注册(所有用户可见)**;词典 +20 个 `mk.*` 键;复用门户同款 `.pa-*` 样式。
- `web/portal.html`:keys 页语义改名「平台共享密钥(未自配密钥的用户默认使用;仅管理员可改)」+ 空态改写。
- `verify-platform-admin-section.mjs`:注册数 2→3(admin)/ 非 admin 2(含 my-keys);新增 mk 分区断言。
**部署**:平台 `tsc` → 传 `src/web/{server.ts,routes/auth.ts}`(转 LF)→ 服务器 `npm run build` → `systemctl restart dshs`(两次,均 active);`web/portal.html` 直传;插件 **0.3.7** 铺发(sha256 `16bb37dbcc13…` 本机=artifacts=两实例三方一致)。回滚件 `/opt/dsh/backups/{server,auth}.ts.bak-20260913-byok`。
**验收**:`npm run verify` 全绿;**真环境端到端**(R4 合规:临时 session 直插、用完即删)——guest `GET /api/me/keys` **200** + `{"keys":[],"effective":"shared","shared":{name:"deepseek-main",owner:"admin",ownerIsMe:false}}`(**改前应是 403**);未登录 401;`POST` 假 key ⇒ `effective` 变 **own**;`DELETE` ⇒ 回落 **shared** ✅。清理已确认:临时 session 0 条、vault 只剩 admin 那行。
**🔴 本轮踩到的并行冲突(已写进档案 85 §八 + MEMORY.md §七)**:
我 21:40 抢到锁后开工,21:50 pack 时才发现**另一个会话已把版本改成 0.3.6**(「内存条读真实配额」,档案 84)**并已铺发**。后果:① `client.js` 里两方改动混在一起;② 我 pack 的 0.3.6 是**我的内容**,而 `ensure-biz-plugins.cjs` **按文件名判版本** ⇒ 输出「已是 0.3.6 → 跳过」,实例里留的其实是旧内容 ⇒ **同号不同内容**。
处置:拒绝同号 ⇒ **提升为 0.3.7** 重铺(0.3.6→0.3.7 实测生效);出厂前跑**全量** `npm run verify`(含它挂的 `verify-mem-model.mjs`)把两方改动一起把关,全绿才发。
教训:**抢到锁 ≠ 工作区干净**;升版本号前先确认该号没被用过;铺发后必须回读实例内版本 + sha256 三方比对。
**文档**:新建 `04-调整方案/85-模型密钥开放给用户自助配置-两层密钥.md`(原子占号 85;**84 已被并行会话占用**);档案 82 追加 §11.7(列表高度)。四件套全过(audit rc=0 / manifest 含 85 / consistency ✓);镜像已推我改的 3 个文件(`82-…`、`85-…`、`docs-manifest.json`),**未碰** `skills/dsh-opensource-release/SKILL.md`(别人的在途,对账仍报它 1 处不一致)。
---
## 22:0x–22:2x · 会话 `official-models-open`:**改用官方「模型」页(不仿制)** —— 85 §九 修正
**用户追加要求**:「参考 **dsh 原始的配置密钥页面格式** 去开发页面,**那里面是可以选模型厂家的**」+「**界面交互都要和官方的一摸一样**」。
**判断**:「一模一样」只有**用本体**才能满足 ⇒ 方案从"仿制一个多厂家页"改为**直接放开官方 `ui-settings-models`**。
**调研(读官方包,只读不改)**:
- 包 = `@deepseek-ai/dsh-client-ui-settings-models`(在官方 dsh 的 node_modules 里);实体是 `ProviderEditor`(**一家一张卡**:write-only API key + 折叠的「自定义设置」= **baseURL** + DeepSeek 模型目录)+ `CustomProviderCard`(**声明官方目录外的厂家**:OpenAI 兼容网关,必填 endpoint/protocol/≥1 模型)。
- 数据走 `remote.llm/settings/credentials`(**dsh 内部通道,非外网**)⇒ 补丁注释里"provider 目录在此环境不可用"的旧判断**已不成立**。
- 🔴 **两处硬约束(决定性)**:① `dsh-credentials-local` 头注释写明 `inherited process environment (read-only, wins) > $DSH_HOME/.credentials.yaml > …` ⇒ **env 永远赢**;② 它的 `write()` 有 **`assertUnshadowed()`**:env 里存在同名 ref 时,用户在模型页保存**直接抛错**("supplied read-only by the launching environment")⇒ **只要平台注入 env,用户就被锁死不能自配 DeepSeek key**。
**方案落地**:
1. `ensure-role-profile-patch.cjs`:**不再禁用** `ui-settings-models`(保留 plugins/inventory/cordis 的禁用),并加"旧块含 models 禁用 ⇒ `--force` 时升级"的判据。
2. `src/web/server.ts`:`resolveApiKey(userId)` 语义**整体换掉** —— 不再注入 env(**恒返回 null**),改为 spawn 时把「平台共享密钥」**预置进** `$DSH_HOME/.credentials.yaml` 的 `refs:` 段(新增 `ensureRefInCredentials()`:只在无该 ref 时写 / 只在 `version: 1` 上插入 / 写前备份 / 写完 **chown 给 home 属主** / 认不出的布局宁可不动 / 失败退回 env 保底)。
3. **撤掉「我的密钥」分区**(插件 **0.3.8**,删组件+注册+20×2 条 `mk.*` 词典,脚本精确删四处并修相邻逗号)。
**🔴🔴 本轮真事故(如实记录)**:第一版把备份写成 `$DSH_HOME/.credentials.yaml.bak-platform`(root 属主 600)⇒ **dsh 用 chokidar watch 整个 home** ⇒ `EACCES: permission denied, watch …` ⇒ **实例直接退出 + 崩溃循环(attempt 1→5)**,guest 约 1 分钟不可用。**修复**:① 立即删该文件止血(实例自愈恢复);② 备份改落 `/opt/dsh/backups/`;③ 顺手扫了 home 里 root 属主的文件。教训已写进 MEMORY.md §七。
**验证(逐条实测)**:patch 里 `ui-settings-models` **0 处**;用户操作 `/api/session/modelCatalog` **200**;凭据文件被**正确预置** `refs.DEEPSEEK_API_KEY`(原 `records:` 完整保留、属主=实例 uid);实例进程 env 里 `DEEPSEEK_API_KEY` **0 条**(改前 1 条);实例稳定 0 崩溃;备份落 `/opt/dsh/backups/`;**抹掉 refs 再 spawn ⇒ 又被正确重新预置**(写入分支闭环)。临时 session 残留 0、临时脚本已清。
**制品**:插件 **0.3.8** 已铺发 admin/guest;后端 `src/web/server.ts` 已部署 + 服务器 `npm run build` + 重启;角色补丁已应用。回滚件 `/opt/dsh/backups/{server,auth}.ts.bak-20260913-byok`。
### 22:2x · 用户要求「提交项目最新文件」⇒ 已提交并推送
- **代码仓** `eb50ca2`(`f6d4ad9..eb50ca2 master`,**已 push**):7 个文件**全是我的** —— `ensure-role-profile-patch.cjs`、`src/web/{server.ts,routes/auth.ts}`、`web/portal.html`、`poc/business-plugins/{package.json,lib/client.js}`、`scripts/verify-platform-admin-section.mjs`。(并行会话的内存改动已由它自己提交为 `f6d4ad9`,**工作区里没有别人的残留**。)
- **文档库** `bc3a176`(`0606f28..bc3a176 main`,**已 push**):只加我改的两个 —— `04-调整方案/82-…md`(§十一 + §11.7)、`04-调整方案/85-…md`(新建,含 §九修正)。
- ⚠️ **文档库仍留着「别人的」未提交在途,未动**:`04-调整方案/76-…md`、`docs-manifest.json`、`skills/{dsh-change-workflow,dsh-decision-method,dsh-feature-first,dsh-opensource-release}/SKILL.md`、`skills/dsh-plugin-diagnose/`(新增目录)。依据:`git show --stat 0606f28` 显示别人上次提交**只带自己那 1 个文件** ⇒ 惯例 = 只提交自己那份。
- 步骤:先 `git status` 分区归属 → `git add <逐个文件>`(不用 `-A`)→ `git diff --cached --name-only` 复核 → commit → push。锁 `commit-final` 内完成,完工已释放。
---
## 22:30–22:5x · 会话 `admin-multi-user-svc`:**admin 跨用户实例管理 + 两处改名**(档案 86,插件 0.3.10)
**用户三条要求(原话)**:①「admin 的 系统管理 中的**服务管理只管得了自己的,看不到用户的服务,需要都能管理才行**」②「这个功能名称 改为**实例管理**」③「**密钥管理** 改为**模型管理**」。
**取证**:平台**所有**服务/文件 API 都是 `request.user.id`(`desktop.ts` 头注释原文 "one user can never address another user's files")⇒ admin 只能管自己。但**底层 `UserFs` / `Spawner` 每个方法都以 `userId` 为首参** ⇒ 卡点**只在路由层**。
**做法**:
- **新开一组** `/api/admin/users/:id/{fs/tree,fs/create,fs/upload,dsh/status,dsh/launch,dsh/stop}`(新文件 `src/web/routes/admin-user-ops.ts`,全 `requireAdmin` + `targetOr404`)。**没给既有路由加 `userId` 参数** —— 那会把"越界判断"混进普通用户每天走的路径,一处写漏就是任意读他人文件;隔离式新增风险面为 0。
- **抽共用函数并导出**(`dsh.ts`):`alive` / `dshUrl` / `launchForUser` / `statusForUser` / `sendBreakerOpen`,原 `/api/dsh/launch`、`/api/dsh/status` 改为调它们(行为不变)—— 避免"两处副本必然漂"。
- 插件「实例管理」页加**「用户」下拉**(`/api/admin/users` 填充,默认选自己),切换后文件树/启停/打开全跟着走。
- 改名同步:插件词典(zh/en)+ 卡片说明 + 副标题(files 副标题改为「按用户查看工作区、启动 / 停止其 DSH 实例」)+ 门户 `CRUMB_TITLES` 与首页卡片。
**🔴 部署踩坑(已写进 MEMORY.md §六)**:第一次服务器构建报 `TS2339: Property 'quotaInfo' does not exist on type 'Spawner'`。真因 = **`quotaInfo` 是档案 84 加进 `Spawner` 的接口成员(已提交进 git),但当初部署档案 84 时漏传了 `src/supervisor/spawner.ts`**(diff 仅那 7 行)⇒ 服务器源码落后于 git。补传后干净通过。
> 教训:**「服务器能跑」≠「服务器源码 == git」**;浅克隆 + 逐文件 scp 会静默落后,**改动依赖别人已提交的符号时,先 `grep` 服务器上有没有它**。
**验证(真环境)**:admin 读**他人**文件树 → **200** + 真实 entries;读他人实例状态 → **200** `{running:false,…,url:"https://guest.alotbuy.com/"}`;未登录 → **401**。插件 admin/guest 均升 **0.3.10**,实例内 `client.js` 含「实例管理/模型管理」3 处、`svcUser` 切换器 2 处。`verify-platform-admin-section.mjs` 全绿(新增「实例管理页含用户切换器」断言)。探针 session 残留 0。
**编号修正**:代码注释里原写「档案 87」,但 86 才是空号 ⇒ 批量改回 86(5 处),并因此**升版本 0.3.9 → 0.3.10**(同号不同内容是禁忌,见 §七)。
**文档**:新建 `04-调整方案/86-admin跨用户实例管理-两处改名.md`(原子占号 86);四件套全过;镜像已推 `86-…md` + `docs-manifest.json`。
**未提交**(§4:用户未要求)—— 在途:`src/web/routes/{dsh.ts,admin-user-ops.ts,server.ts}`、`web/portal.html`、`poc/business-plugins/{package.json,lib/client.js}`、`scripts/verify-platform-admin-section.mjs`、`04-调整方案/86-…md`。
---
## 21:2x · 查明「功能管理里显示 388 多」= 前端自估模型与平台配额**两套表从未对齐**
**用户问**:「实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多」
### 事实(实测)
| 项 | 平台侧(决定真实 `MemoryMax`) | 插件前端(「功能管理」显示的那个数) |
|---|---|---|
| 基线 | `BASE_MEM_MB = 160` | `MEM_BASE_MIB = 285` |
| `dsh-univer-office` | **512** | **64** |
| `dsh-plugin-mcn-suite` | **128** | **39** |
| 合成规则 | `clamp(160 + Σ, 384, 1024)` | `285 + Σ`,且把 **384 当硬上限**判超限 |
| 上限 | 384 只是**下限**,真实可到 1024 | `MEM_LIMIT_MIB = 384`(写死,档案 58 时代的旧值) |
**实测 `MemoryMax`(字节 → MiB)**:guest(uid 100002,装 univer) `704643072` = **672**(=160+512 ✓)|admin(uid 114801,装 mcn-suite) `402653184` = **384**(=clamp(160+128)=384,踩下限 ✓)。
`heapMbFor()`:两组都= min(256, 配额−96) = **256**。
**388 怎么来的**:池里恰好 2 个插件(实调 `/api/plugins/business` 确认),前端表都有值 ⇒ 全勾 = `285+64+39 = 388`。而 `MIC` 侧同一情形 = `160+512+128 = 800`。
⇒ 那条「⚠ 将超出上限」是**误报**:384 在平台侧早已只是下限。
### 根因(一句话)
**「单实例上限」这个事实被复制到了两处**(编排器 `instanceMemMb()` / 插件 `MEM_LIMIT_MIB`),R1 把它从写死 384 改成动态 384–1024 时**只改了编排器**;插件那张成本表(2026-09-13 另一路实测口径)也一直没同步。
### 附带确认
`/api/dsh/status` **不返回任何内存字段**(只有 running / instance{id,port,status,restarts} / watchdog / breaker / url)⇒ 插件**拿不到真实配额**,只能自估,这才是它自建表的根本原因。
### 拟定修法(**未执行:平台仓被 `r2-portal-align` 占锁,按 R9 停手**)
- **主修**:`/api/dsh/status` 增补 `memMb`(= `instanceMemMb(profileDir)`)与 `heapMb` —— 编排器已算好,只是透出来,**零新增逻辑**;前端以真实配额为 100% 基准,读不到才退回估算并明确标注「预估值」。
- **兜底**:即使不读 status,也把前端表/规则对齐平台(base 160 / univer 512 / mcn 128 / clamp 384–1024)。因为前端拿到的 `enabled` 就是本实例 bundles ⇒ 能算出**与平台完全相等**的值(不再是估计)。
- 现状 `MEM_BASE_MIB=285` 的口径(含 148 个官方插件)与平台 `BASE_MEM_MB=160`(只算 dsh 基座)**是两种不同量法**,不能简单取一个覆盖另一个 —— 这也是为什么首选「读真实值」而不是「把表抄过去」。
## 21:2x–21:4x 会话 `48a9c14c`:检查最新方案状态(**全程只读**)
**锁**:全局锁被 `mem-model-fix`(09-13 **21:23** 起)持有 → 在「统一内存配额口径(`/api/dsh/status` 暴露真实值 + 前端读真值)」→ 按 §6/R9 **未抢锁、未改任何文件**。
### ★一次我自己的误判与当场纠正(值得记的教训)
- 我实测发现 **guest `bundles` 不含 `dsh-plugin-mcn-suite`**(且 `MemoryMax` 仍是 672 而非装入后的 800),而 `03-路线图 §二` 记着「T03 …**guest 启用已成功**(任务 `c20739a6dbe8975c`:`success`/`restarted=true`;配额自动 672→800)」⇒ 我据此判定为「**账实不符**」,并去追查 21:22 那次 `ensure-biz-plugins.cjs --all` 重写 profile 是否把它冲掉了。
- **用户当场澄清:「那是我手动禁用的」** ⇒ **既不是回归、也不是账实不符**,是**用户在产品面的主动操作**(实例「功能管理」启用/禁用本就是用户自助面)。
- **教训(写给自己)**:遇到「文档说 A、实测说 B」,可能原因有**三层** —— ① 文档滞后 ② 会话事故/回归 ③ **用户主动操作**(启用/禁用、候选池上下架、删文件…)。**先想到第③层**,再下"账实/回归"的结论;否则会把人家的正常操作报成系统故障。平台有大量**用户自助面**,这类"看起来不该变的状态"本就随时可被用户改。
### 线上实测快照(2026-09-13 21:3x,只读)
| 项 | admin(uid 114801 / `cce6d1cd…`) | guest(uid 100002 / `4092b965…`) |
|---|---|---|
| `bundles`(6 个) | dsh-base · dsh-web-app · portal-entry · business-plugins · workspace-scoped-picker · **dsh-plugin-mcn-suite** | dsh-base · dsh-web-app · business-plugins · portal-entry · workspace-scoped-picker · **dsh-univer-office** |
| `business-plugins` | **0.3.5** | **0.3.5** |
| `portal-entry` | 0.5.2 | 0.5.2 |
| 其它 | `dsh-plugin-mcn-suite` = **0.3.10**(21:10 池内版) | `dsh-univer-office` = **0.2.27** |
| **`MemoryMax`** | 402,653,184 = **384 MiB**(=clamp(160+128)) | 704,643,072 = **672 MiB**(=160+512) |
- **R3 标识迁移已实际落地**:`/opt/dshs`、`/var/lib/dshs`、`/etc/dshs.env`、`dshs.service` 全部存在且为现行(实例数据根 = `/var/lib/dshs/users/<uuid>/`;**不是** `/opt/dsh-server-login` 与 `/var/lib/dsh-server-login`,那两个已不存在)。
- 候选池 `/var/lib/dshs/business-plugins/`:`dsh-plugin-mcn-suite.tgz`(0.3.10,1,944,704 B @21:10)、`dsh-univer-office.tgz`(0.2.27,42,430,328 B @19:44)。
- `.dsh-stage/`:两实例都已铺 `dsh-plugin-mcn-suite.tgz`(0.3.10 @21:10)与 `business-plugins-0.3.5.tgz`(@21:22)⇒ **包铺到位 ≠ 已启用**,启用态看 `bundles`。
- 服务器 op-lock 空闲;`dshs.service` active;两实例 scope 均 running。
### 待执行清单现状
- **交接单 §一**:只剩 **`T01`**(⏳ 待执行,决策点 1 已定 = A 扩展 `business-plugins`,无阻塞)。T02–T05 全部归档。
- **`03-路线图 §二`**:档案 81 重构总纲的 **R2/R4/R5**(R0/R1/R3 已完成;R2 代码已完、待收尾)|档案 **78 遗留 L1**(需维护窗口)|档案 **27** 暂缓(3 处 P0)|档案内挂起一批(57 / 59 / 66 / 68 / 38b / 15 / 32)|P3 门户下载入口|触发式(dsh 升级回归 / 会话 GC)。
- **本机 git**:末次提交 `f1a0da1`(档案 82 §10.7);未提交 **8 项** = `04-76`、`04-82`、`docs-manifest.json`、4 个技能 `SKILL.md` + 未跟踪 `skills/dsh-plugin-diagnose/`。
---
## 21:2x–21:4x · 已修:「388 MiB」内存误报(档案 84)
**用户授权执行我的方案**。锁当时被 `r2-plist-height` 占着 ⇒ 等它释放后抢到(未抢锁、只等)。
### 改了什么
**平台侧**(把已算好的真值透出来,零新增计算):
`src/supervisor/spawner.ts` 加可选 `quotaInfo?`(照 `breakerInfo` 模式)|
`src/supervisor/orchestrator.ts` 实现 `quotaInfo()`(走**同一个** `instanceMemMb()`/`heapMbFor()`)|
`src/web/routes/dsh.ts` 的 `/api/dsh/status` 返回 `quota:{memMb,heapMb}`。
**插件侧**(0.3.5 → **0.3.6**):常量/规则逐项对齐编排器(160/384/1024/96 + univer 512 + mcn 128),新增 `rawMiB()/quotaOf()/heapMiBFor()` 同构;`load()` 并行读 status 并作为**事实行**显示「实际配额 · V8 堆」;**超限判据改「原始需求 > 硬顶 1024」**(旧版拿 clamp 后的值判 ⇒ 既误报又永不触发);未进成本表的插件不再显示 `≈0 MiB` 徽章;文案 `上限`→`硬顶`。
**钉死**(本次关键):新增 `scripts/verify-mem-model.mjs` —— 解析两侧常量/成本表/clamp 算式/接线点逐项交叉断言,接进 `npm run verify`;**反向验证过**(把插件基线改回 285 ⇒ 报错 exit 1,还原即全绿)。根因是「同一事实两处副本」,所以修法必须包含「让它不能再漂」。
### 验证
build 零报错 | verify 全绿(含新增 15 项断言)| 服务端 `/opt/dshs/lib/...` 两文件 md5 与本机编译产物一致(差异已核:orchestrator 正好我的 19 行;dsh.js 我的 3 行 + 1 行他人旧注释)| **`GET /api/dsh/status` → `quota:{"memMb":384,"heapMb":256}`** 与实测 `MemoryMax=402653184` 吻合 | 两 profile 实装 0.3.6、client.js md5 `86bc8a46…` 与本地产物一致、旧符号 `MEM_LIMIT_MIB` **0 处** | 下发 bundle(`rev=a8eb3ecf1edb`)含新模型。
修复后界面应为:`当前 384 MiB → 勾选后 800 MiB` + `实际配额 384 MiB · V8 堆 256 MiB`,状态**余量充足**,无红色误报。
### 🔴 并发撞车(本次最该记住的)
`poc/business-plugins/lib/client.js` 在我作业期间被并行会话**连续改了两轮**(0.3.4「系统管理改为门户 6 功能页」、0.3.5「插件列表高度」,**0.3.5 已部署**)。因每次 `Edit` 前都**重读文件**,我的改动**叠在其上、未覆盖**;但我以为版本是 0.3.3、实际已是 **0.3.5** ⇒ 第一次 `npm pack` 打出「内容是别人 0.3.5 + 我的修改、版本号仍写 0.3.5」的**同名假产物**(已删),改判 **0.3.6**。
⇒ **新纪律:动手前先 `git status` + 查最新 tgz 版本号,版本取「当前值 +1」,不要用记忆里的值。**
### 提交与收尾
代码仓 **`f6d4ad9`** → push master(**只提交我的 5 个文件**:3 个 src + 新脚本 + 根 package.json);档案 **`84-实例内存配额口径统一-消灭388误报.md`**,文档库 **`0606f28`** → push main,已 tar 管到 `/opt/dsh/docs` 且双端 md5 `093346e1…` 一致;四件套 audit rc=0 / consistency ✓;**锁已释放**。
⚠️ **`poc/business-plugins/lib/client.js` 与 `poc/business-plugins/package.json` 刻意未提交** —— 里面有并行会话已上线但未 commit 的 0.3.4/0.3.5,留给他们的批次一起提交;**同时这也是一处风险:这 3000+ 行改动目前只在工作树里,没有 git 兜底**。
---
## 22:2x · 普通用户权限审计(用户要求确认) + 修回一次「整包覆盖」事故
### 一、权限审计结论:**没有被放开**(四层证据)
1. **静态**:我 21:4x 的提交 `f6d4ad9` 共 5 文件 / +128 −1,新增行里**没有任何鉴权判断**;唯一路由改动是加 `quota` 一个只读字段,且取的是 `quotaInfo(request.user!.id)` —— **按调用者自己**,没有可传的 userId 参数。
2. **动态实测**(临时会话,用完即删):
| 身份 | `/api/dsh/status` | `/api/plugins/business` | `/api/admin/{users,storage,domains,runtime}` |
|---|---|---|---|
| 无 cookie | **401** | 401 | 401 |
| guest(role=active) | 200(**只有自己的** quota) | **403** | **403**(全家族) |
| admin | 200 | 200 | 200 |
3. **quota 隔离**:admin `{memMb:384}` / guest `{memMb:672}` —— 各是自己实例的值,**无越权读取**。
4. **鉴权中间件未被改动**:那次 22:16 的整包覆盖虽然也覆盖了 `lib/web/middleware/authn.js` 等,但与本机 HEAD 构建 **md5 逐字节一致**(authn / fs-guard / rate-limit / security-scan 四个都是),即**覆盖的是同一份已审鉴权代码**。
5. **实例内 UI 门禁仍在**(并行会话重写过「系统管理」分区,特意复查):`verify-platform-admin-section.mjs` 的 `✓ 非 admin 视角下只注册 1 个分区(功能管理;不含系统管理)` 仍通过;guest 实装的 client.js 里 `role !== "admin"` 门禁 3 处、`/api/auth/me` 判角色 3 处均在。
### 二、🔴 事故:我的内存修复被「整包 lib/ 覆盖」抹掉(已修回)
**现象**:22:2x 复验时 `/api/dsh/status` **不再有 `quota` 字段**(keys 退回 5 个)。
**诊断**:服务器 `/opt/dshs/lib/supervisor/orchestrator.js` 与 `lib/web/routes/dsh.js` 的 md5 **恰好等于我 21:33 备份的旧版 md5**,mtime = **22:16**,`lib/**` 有一整批文件同时在 22:16 被改、服务 **22:16:32** 重启
⇒ **某个并行会话做了一次「整个 lib/ 全量覆盖」,用的是不含我 21:4x 提交的陈旧构建。**
**处置**:锁空闲 ⇒ 抢锁 → 备份 22:16 版到 `/opt/dshs/.bak-20260913-2216/` → 重传 2 文件(md5 一致)→ 重启 → 复验 `admin 384 / guest 672` 回来 → **释放锁**。
**残余影响很小**:插件 0.3.6+ 有**兜底** —— 读不到 `quota` 时用 `quotaOf(rawNow)` 本地算(表已对齐且被 `verify-mem-model.mjs` 钉死),数值仍正确,只是事实行文案降级成「估算值」。
### 三、新增纪律(已写进 `dsh-change-workflow`)
> **全量部署 `lib/` 之前必须先在 HEAD 上 `npm run build`** —— 否则会把别人的已提交改动**整体回退**(本次:整批文件统一 mtime + 服务重启时间就是指纹)。
> 判据:`grep -c <你上次加的符号> /opt/dshs/lib/...` 或直接 md5 对账;**部署后立刻复验上次刚验收过的东西**(本次正是靠这个发现的)。
> 另:**「已部署」≠「已提交」**(并行会话 0.3.5 已上线却未 commit),所以**不能拿服务器状态当版本事实源**。
### 四、附带观察
并行会话的插件已推进到 **0.3.8**(0.3.5→0.3.6→0.3.7→0.3.8),两 profile 均 0.3.8;**我的内存修复仍在其中**(`MEM_BASE_MIB=160` / `MEM_MAX_MIB=1024` / 成本表 512+128 / 读 status 6 处 / 旧符号 0 处)⇒ 插件侧没有被他们改掉。
---
## 23:20 「模型设置」lane 交接单 —— 递单受阻(**规划会话**)
**任务**:为用户 09-13 22:5x 那轮「平台自建模型设置页」写交接单(把后端已改部分登记,防新会话重复劳动)。
**做了什么(全部只读 + 非受保护路径写入)**:
1. **清掉「官方页为何不可用」的落盘错位**:用户以为该结论在**档案 86**,实际 86 是「admin 跨用户实例管理 + 两处改名」,**全文没有该内容**;真结论只存在于 **`ensure-role-profile-patch.cjs` 的工作树注释**(`DISABLE_MODELS_BLOCK` / 新增 `ADMIN_MODELS_BLOCK`):官方模型页要 Host settings 镜像,`isLoopback = transport.ownsHost || pageLocation===undefined || isLoopbackHostname(page)`,平台是远程域名访问 ⇒ 三条皆不成立 ⇒ persistence 降 `memory` ⇒ 页面必报「加载提供方目录失败」。⇒ **代码注释里的「档案 86 §九」是笔误,应为 87**。
2. **摸清后端缺口**(`git diff` + grep):`adapter.ts` 新增/改了 **5 项**接口(`listEnabledCredentialKeys` / `toggleCredentialKey` / `getSharedModelEnabled` / `setSharedModelEnabled` / `setCredentialKey` 签名 +baseUrl,models);`repo.ts` 已实现前两项 + 签名,**缺** `getSharedModelEnabled`/`setSharedModelEnabled`(grep = 0);`sqlite.ts`/`pg.ts` **一项未动**(mtime 停在 15:39)⇒ tsc 必报「类未实现接口」。
3. **挖出一处新口径引入的正确性缺陷(本单最重要)**:`getEnabledCredentialKeyRef` 是 `SELECT secret_ref … WHERE user_id=? AND enabled=1`,**无 `ORDER BY`**;老逻辑靠「写入时 `SET enabled=0` 全关」保证唯一,而本轮已**删掉互斥**(`repo.ts:254-256` 注释「不再互斥」)⇒ 多条目启用后**任取一条**,`keySourceOf()`(`auth.ts:144,147`)与 `resolveApiKey()`(`server.ts:145`)会飘。必须重定义为「已启用的内置 DeepSeek 条目」。
4. **产出交接单草案**:`E:\ProgramData\AI技能\aliyun-dsh-server\.workbuddy\draft\T06-模型设置页-多厂家条目可开关.md`(8 段齐全,含 4 个待补项 A/B/C 与唯一未取证项 = 自定义厂家的 `route`/`protocol`/`models` 命名规则)。
5. **压缩本库记忆**:`MEMORY.md` 16,494 B → 10,201 B(已超注入上限被截断,按「只记会变的状态」重写)。
**为什么没落盘(关键)**:`--claim-exec` **抢锁失败** —— 占用者 **`rebuild-my-keys`**,开始 **09-13 22:57**,未声明单号;而 `src/db/{adapter,repo,schema}.ts` 的 mtime = **23:11**(抢锁**之后**仍在写)⇒ **同一 lane 的活会话**。按 **R9** + `交接单/README.md §一`:抢不到 = 停手 + 报告用户,⛔ 不得删锁/接管 ⇒ 草案只落非受保护路径(`.workbuddy/` 不在两处受保护根内 ⇒ 无需锁)。
**待用户处置**:① 让原会话自己收尾(上下文最全,推荐)或 ② 释放锁后由新会话落盘 `交接单/T06-…md` + 回填 `§一` + 跑四件套。
---