Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下17.md
T

376 lines
46 KiB
Markdown
Raw Normal View History

# 工作日志 · 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//AIProject//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\AIProject\ai1net-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` + 回填 `§一` + 跑四件套。
---