Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下18.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

228 lines
30 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(第 19 片)
> ⚠️ **本目录日志已按【月】分片**(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-14.md
> **上一片**:`2026-09-下17.md` **下一片**:`2026-09-下19.md`
---
## 23:2x–23:5x 用户授权解锁 ⇒ **接手上床,T06 全流程闭环上线**(`craft-session-T06`)
**用户两句指令**:「另一个会话已经删除了,继续执行完成这个功能开发」+「**允许解锁操作**」。按 R9 我**没有**自己删锁,而是用项目自带机制 `handoff-guard.sh --release-exec` 释放残留锁、再以 `craft-session-T06` 重新持锁(单 `T06`)。
**做完的事(全部有实机证据)**:
1. **取证官方 schema**(本轮唯一未取证项,救回一个字段名):读服务器 `[email protected]` ⇒ 分区名 `llm-pi-ai`、凭据字段 **`apiKeyEnv`**、endpoint `baseURL`、协议字段是 **`api`**(**不是 `protocol`**)、ref 名须匹配 `^[A-Za-z_][A-Za-z0-9_]*$`、`provider`/`maxRetries` 会直接抛错;并确认该插件在 `dsh-base` 的 bundle 列表里(**真的会被加载**)。
2. **又挖到一条权威出处**:官方 `dsh-client-ui-settings/README.md:97`「Non-loopback pages get no durable settings」⇒ 坐实「平台页必然报错」;并**辨清与 `03-路线图` 决策 3 的冲突**:那是 host 侧 trust fence(proxy 伪装 Host ⇒ 放行),这是**浏览器侧**持久化(真实域名 ⇒ unavailable)——**两层都成立**,85 §九 只验证了前者。
3. **DB 层**:迁移 **V6**(`credential_vault.route/base_url/api/models` + `users.shared_model_enabled`)+ 各后端补 5 项;**重定义 `getEnabledCredentialKeyRef`**(老实现无 `ORDER BY`,互斥一删就**任取一条** ⇒ `keySourceOf`/`resolveApiKey` 会飘)。
4. **新落地层 `src/web/model-landing.ts`**(纯文本、可测)+ **13 组回归**;回归当场抓出**真 bug**:`providers:` 被写重复(第一版按"只看顶层行"找链,而 `providers:` 本就是缩进 2 的子键)。含**一次性交接**(认领老实现写下的 `DEEPSEEK_API_KEY`,否则"关共享即生效"不成立)。
5. **接口** 4 条 `/api/me/*` + **前端「设置 → 模型设置」**(插件 **0.3.11**,`settings.section` id=`model-settings` order 100 全角色)。
6. **出厂**:代码 tar 管道(LF 化)→ 服务器 build 干净 → 重启;V6 实测落库;插件 0.3.11 铺发两实例(`bundles=6`);角色补丁 `--force --restart`(**先留回滚点** `/opt/dsh/backups/patch-20260913-234511/`,跑完 diff 验证)。
7. **验收四件事全绿**:① 两账号 patch 均有 models 禁用行;② HTTP 实测新增/开关/删除 + 4 类非法入参被拒(400/409) + 前端 harness 全绿;③ 关共享 → 实例 `.credentials.yaml` 里 `DEEPSEEK_API_KEY` **消失**、进程 env 恒 **0 条**,开回来又出现且与自定义厂家**并存**;④ `settings.yaml` 落地官方 schema 原文、实例启动成功无报错、删除后**整块消失**。
8. **顺手修两颗雷**:① `ensure-role-profile-patch.cjs` 的 `--force` **整文件覆盖**会抹掉 admin 的 `disable-hmr` + `workspace-scoped-picker` 两个平台块 ⇒ 新增 `stripManagedBlock()` 只替换自己那段(实机 diff 证明另两块完整保留);② `verify-mem-model.mjs` 因档案 86 重构而长期失败的**陈旧断言**。
9. **收口**:档案 **87** + 单 `T06` 归档 + `交接单/README.md §一` + `INDEX.md`(加 87 行 + §四) + 四件套(audit rc=0 / 一致性 ✓ / 对账 148 一致,仅剩**别人的** 1 个在途)+ 文档镜像按 SOP scp(只推自己的)。
**暴露的流程问题(已上报,未代改)**:① **档案 82–86 尚未登记进 `INDEX.md §二`**;② `04-调整方案/.lock-78/85/86/87` 空占号目录堆积(87 已清);③ ⚠️ **别拿会话自述当线上事实** —— 上一会话称"未构建未部署",实际**角色补丁已被它刷到线上**。这次"先看服务器再动手"正是靠这一点救回两颗雷。
---
## 23:59 用户指令「同步项目到仓库」⇒ **两仓已提交并推送**
- **代码仓** `eb50ca2` → **`918f1d3`**(`master`,已 push;`ls-remote` 核对远端 = 本地)。
20 个文件。⚠️ **不可避免地把档案 86 的代码一并带上**(`admin-user-ops.ts` / `dsh.ts` / `server.ts` 的 register / `client.js` 的改名段 / `portal.html`)—— 它已上线并端到端验证,且与我的改动**同处一个文件、按文件粒度无法拆分**;不带它仓库 `tsc` 直接失败(缺模块)。已在提交说明里明确披露。
- **文档库** `bc3a176` → **`bbccf35`**(`main`,已 push;同样 `ls-remote` 核对)。只 add 自己那 5 个(`04/87`、`archive/T06`、`INDEX.md`、`交接单/README.md`、`docs-manifest.json`)**外加 `04/86`** —— 因为 87 与 INDEX 都引用 86,不带它会让仓库出现**悬空引用**(该档案另一 lane 已完成)。
- **未纳入**(属别人在途,一个没动):`04/76`、`skills/{dsh-change-workflow,dsh-decision-method,dsh-feature-first,dsh-opensource-release}/SKILL.md`、`skills/dsh-plugin-diagnose/`。
- 📌 判据经验:`git log origin/<branch>..HEAD` 在本机**不可靠**(无 remote-tracking ref)⇒ 用 **`git ls-remote origin refs/heads/<branch>`** 与本地 `rev-parse` 对比才是权威判据。
---
## 09-14 00:0x–00:3x 用户实测反馈驱动**补做**:接官方厂家目录 + 页面按官方结构重做
**用户原话**:「新增模型条目怎么看不懂呢,**是按照 dsh 自带的模型设置交互开发的吗**,而且**模型厂商选择怎么这么少、国内的一家都没有**」→「页面就按照 dsh 模型设置的页面(使用项目 ui 规范),**增加 admin 设置的模型管理**就行」。
**根因(我认下的设计做窄)**:第一版只支持「内置 DeepSeek + 手填一个 OpenAI 兼容网关」,把"选厂家"这件事整个漏了 —— 而官方 `pi-ai` 包里**自带 38 个厂家**(国内 11 家:蚂蚁 / 通义千问×3 / 小米×2 / 月之暗面×2 / 智谱×2 / MiniMax)。
**官方依据(决定性)**:`dsh-llm-pi-ai` 的 `config.d.ts` 原文 —— *"A route key is not required to name an installed pi-ai provider. When it does, that provider's endpoint, protocol, display name, and model catalog are the profile's defaults"* ⇒ **目录厂家只需 `apiKeyEnv`**,端点/协议/模型全免填。
**做了四层**:① 新文件 `src/web/model-catalog.ts`(只读官方 `data/*.json`,10 分钟缓存,**不导入 pi-ai 运行时**;中文名走显示表,选取范围以目录为准);② `GET /api/me/model-providers`(38 家 + `cn` 分组 + modelCount;**排除 `deepseek`** —— 平台已有内置入口);③ `POST /api/me/keys` 新增 `provider`(**只收 apiKey**);④ 前端按官方页结构重写(提供方行 + 新增先选厂家 + 中国大陆/国际分组 + 保留强化 admin 的「平台共享模型」)。
**两个判据坑(都写进代码注释了)**:
1. `refForEntry` 必须用「**没有 route**」判内置 —— **目录厂家也没有 `baseUrl`**,沿用第一版的「baseUrl 为空」会把目录厂家的 key 写进 `DEEPSEEK_API_KEY`。
2. 验收桩只收 `children` + `placeholder`,**`<optgroup label>` 是 prop** ⇒ 「中国大陆 / 国际」断言会假红;已扩桩(`optgroup` 的 label 也算用户可见文案)。
**真环境验收全绿**:目录端点 38 家 / 国内 11 家;加 `moonshotai-cn` **只给 key** → `settings.yaml` 只写 `apiKeyEnv`(无 baseURL/api/models,正是官方语义)、`.credentials.yaml` 出现 `MOONSHOTAI_CN_API_KEY` 且与 `DEEPSEEK_API_KEY` 并存;非法厂家 400;删除后整块消失;本地 `verify-platform-admin-section`(+8 条新断言)与 `verify-model-landing` 全绿;zh/en 词典 **282/282**。
**出厂与归档**:插件 **0.3.12**(两实例 bundles=6)|后端 build + restart|档案 87 追加 **§九 补做**|代码仓 `918f1d3`→**`022b1f7`**、文档库 `bbccf35`→**`730249a`**(均已 push,远端哈希已核)|临时候话 / 测试条目 / 临时脚本全部清理(0 残留)。
---
## 09-14 00:20 用户「都推上去」⇒ 把**别人的 lane 在途**也代为提交,文档镜像**首次双端全绿**
**背景**:上一轮我报告"我的改动已全同步、只剩 6 项别人的在途",并问要不要一并提交 → 用户答「**都推上去**」。
**做法(保住可归因性)**:**按 lane 分两笔**,提交说明里**明确写"另一 lane 的会话沉淀,由用户 2026-09-14 拍板代为提交(原 lane 未提交)"** —— 这样历史里能看出是谁写的、为什么由别人提交。
- `69ce162` `docs(76)`:univer 档追加第二轮(gateway「写入假错误」→ 0.2.25)与第三轮(AI 生成 Office 文件做成实例内能力 → 0.2.26)。
- `d1d1f11` `docs(skills)`:4 份技能副本(decision-method 扩到 U1–U22 / feature-first 新增 §5.1 结论骨架 / change-workflow + opensource-release 增补)+ **新技能 `dsh-plugin-diagnose` v1.0.0**(业务插件静默失败诊断:三层归属 + inject 差集 / glibc 直测 / 产物插探针三把尺子,09-13 univer 四轮排查沉淀)+ manifest 刷新。
**顺手把镜像也补齐**(这是"双端全绿"的关键):`/opt/dsh/docs` 一直挂着 1 个"内容不一致"(`skills/dsh-opensource-release/SKILL.md`)—— 把 6 个文件 + manifest 一并 scp 过去、`mkdir -p` 新技能目录、按既有约定 `chmod 600`。
⇒ `docs-sync-check.sh`:**149 一致 / 0 不一致 / 0 仅本地 / 0 仅服务器 → 双端一致 ✅**(本库首次);`docs-consistency.py` ✓;两仓工作树**均已清空**,远端哈希与本地一致。
**口径记录**:这次代提交是**用户一次性授权**,**不是常设规则** —— 下次仍按红线默认不动别人的在途;要动先问。
**仍留的尾巴**:技能是**三处**(本机 `~/.workbuddy/skills` ← 文档库 ← 镜像),本次只对齐了后两处,**本机工作副本未核**;`INDEX.md §二` 里 `skills/dsh-plugin-diagnose` 这条**未登记**(同理 **档案 82–86 也未登记**)。
# 2026-09-14 工作日志
## 04:2x 复核「开源导出会话」发来的 P1 交接单:**模型目录 / 平台包目录安装路径写死**
来源:`E:\ProgramData\AIProject\dsh-laijing-github\_交原仓会话_模型目录安装路径缺陷_20260914.md`
(提出方 = 开源导出会话;受理方标注为本会话;它**没动源仓、没动锁**,按 R9 停手)
### 复核方法(**不照抄结论**,全部自己取证)
1. **先查全仓有几处** —— ⚠️ 第一次我 `grep -v node_modules` 把**要查的行本身也滤掉了**(那些行就含 `node_modules` 字样),得出"0 处"的错误结论;改用 ripgrep(尊重 .gitignore)后真相浮现。
2. **用平台自己的编译产物在 `/usr/lib` 布局的测试服(`test106` = 106.54.21.172,OpenCloudOS+宝塔)上真跑**,而不是只读代码或采信对方贴的输出。
3. 补扫 `lib/node_modules` 类写死,确认**没有第四处**。
### 结论:问题**确实存在**,而且**比单子描述的多一处**
| # | 位置 | 单子 | 我的复核 |
|---|---|---|---|
| 1 | `src/web/model-catalog.ts:30` | 有(L30) | ✅ 有。`catalogDir()` 指向 `/usr/local/lib/node_modules/...` ⇒ 该布局下 `exists=false` ⇒ `listCatalogProviders()` 返回 **0 家**(真实位置**有 39 个** json) |
| 2 | `src/web/plugin-compat.ts:**36**` | 有(写作 L35) | ✅ 有(行号差一行)。`platformPkgCount()` 实测 **0** ⇒ 预检退化为「平台包目录不可读」 |
| 3 | **`poc/workspace-scoped-picker/lib/index.js:34-35`**(`DEFAULT_SEAM_CANDIDATES`)**+ README:36** | **漏了** | 🆕 同样写死同一路径。**表现不同**:**响亮失败**(有 `DSH_SEAM_DIRECTORY_PICKER` 逃生口、抛错列已试路径)——**已实测复现**(把该 lib 传到测试服 import ⇒ 抛「无法解析官方 seam 基类」,两候选都不存在、真实位置在 `/usr/lib` 且存在)。后果更重:它是**平台级 bundle**,一挂 = 实例内**目录选择器不可用**。test106 上尚无用户 profile ⇒ **尚未触发**,但用户首登建 profile 后即触发 |
- **逃生口未暴露** ✅ 成立:`DSH_PACKAGE_DIR` / `DSH_COMPAT_ROOT` / `PI_AI_DATA_DIR` 在 `install.sh` / env 样例 / README 里 **0 命中**(只出现在交接单自身与导出物的 memory)。
- **修复思路的基础成立** ✅:`readlink -f /usr/bin/dsh` → `/usr/lib/node_modules/@deepseek-ai/dsh/lib/bin.js` ⇒ 靠 `DSH_USERS_PLATFORM_DSH_BIN` 解软链拿包根这条路可用。
- 全仓 `rg "lib/node_modules"`(排除 `node_modules`/`lib/**`)**0 命中** ⇒ 除这三处无其它同类写死。
### 状态
- **只检查、未改码**(用户指令是「检查是否有这些问题」)。源仓 HEAD 仍 `022b1f7`、工作树干净。
- ⚠️ 若按单子的 §3 只改两处 ⇒ **会漏掉第三处**。建议 `dsh-install.ts` 再导出一个 `dshSeamCandidate()`(或把 seam 候选纳入同一套探测)。
- 本机与测试服 `/tmp` 探针**已清**(测试服全程只读,只在 `/tmp` 放过脚本且已删)。
---
## 04:3x–05:0x 用户「确认修改」⇒ **落地修复**(源仓 `cbaf3ad`,已 push)
### 落地内容
| 文件 | 改动 |
|---|---|
| **`src/web/dsh-install.ts`(新)** | 全平台唯一入口:`dshPackageRoot/dshScopeDir/piAiDataDir/resetInstallPathsCache`;按序探测 = env → **平台配置的 dsh 可执行文件解软链反推包根**(最可靠)→ 常见全局根 → `npm root -g` → 历史默认值;只依赖 node 内建 |
| `src/web/model-catalog.ts` | `catalogDir()` → `piAiDataDir()` |
| `src/web/plugin-compat.ts` | 删 `PLATFORM_DSH_ROOT`/`SCOPE_DIR` 常量,三处改用 `dshScopeDir()` |
| **`poc/workspace-scoped-picker/lib/index.js` + `README.md`** | **补第 3 处**:seam 候选改为按序探测包根(插件跑在**实例进程**内拿不到平台代码 ⇒ 自带一份,两处注释互相指认) |
| `scripts/verify-dsh-install.mjs`(新)+ `package.json` | 15 项断言(env 覆盖 / 解软链 / 包名不误认 / 派生量 / 缓存语义 / 本机实况),接入 `npm run verify` |
### 我改了导出会话修复件的两处(**关键**)
1. **env 名**:它用 `DSH_USERS_PLATFORM_DSH_BIN`(导出侧名),源仓是 **`DSHS_DSH_BIN`**(`config.ts:239`)⇒ 照抄会让"最可靠那一路"在源仓侧**永不生效**。改为**两侧都认**(`DSHS_*` 优先、`DSH_USERS_PLATFORM_*` 兼容)。
📌 顺带查明:**我们服务器根本没设 `DSHS_DSH_BIN`**(`/etc/dshs.env` 无)⇒ 我们走"常见全局根"那一路(`/usr/local` 命中);test106 走 env 那一路(`/usr/lib` 命中)。**两条路都得留。**
2. **补第 3 处**(见上)。
### 验证(全实测)
- 本机:`tsc --noEmit` **0 错误**;新回归 **exit 0**;既有 5 个 verify 脚本**全绿**;单测 **16 pass / 0 fail**。
- 服务器 `bt-server` 回归:`platformPkgCount` = **223**、厂家目录 = **39**(未回退、无破坏);已 build + restart。
- **测试服 `test106`(原故障机,`/usr/lib`)三种姿势全部解析正确**:① 无 env → `/usr/lib/node_modules/@deepseek-ai/dsh`(scope 240 项、pi-ai 数据 **39**);② `DSHS_DSH_BIN=/usr/bin/dsh`;③ `DSH_USERS_PLATFORM_DSH_BIN=/usr/bin/dsh`。全程只读(只在 `/tmp` 放脚本且已删,未动 `/root/dsh-users-platform`)。
- 回执已写:`dsh-laijing-github/_回复源仓会话_安装路径缺陷已修复_20260914.md`(含"导出会话待做:重建导出物 + 复跑探针 + 把 3 个 env 写进 install.sh/README")。
### 🔎 两条方法论教训(差点骗了我自己)
1. **`grep -v node_modules` 会把要查的行本身也滤掉**(这些行就含 `node_modules` 字样)—— 我第一次据此得出"源仓 0 处"的**假结论**,改用 ripgrep 才看到真相。⇒ 排依赖目录要用 `--exclude-dir`/`.gitignore`,**不要用行过滤**。
2. **`rg` 在本机 PATH 里可能不存在**:`rg ... 2>/dev/null` 失败会**静默返回空**、看起来像"0 命中"。⇒ 结论性地断言"扫干净了"之前,先确认工具真的跑了。
---
## 04:4x 复盘(用户问「有什么影响 / 为什么上一轮没发现」)
### 影响面(分层)
| 面向 | 影响 |
|---|---|
| **本平台(我们两台服务器)** | **零影响** —— `npm root -g` = `/usr/local/lib/node_modules`(npm 默认 prefix)恰好等于写死值 ⇒ 一直正常(实测 `platformPkgCount`=223、厂家目录 39) |
| **`install.sh` 部署的机器(如 test106,`/usr/lib`)** | ① 厂家目录读空 ⇒ `/api/me/model-providers` 返回 `[]` ⇒ 新增厂家**只剩「内置 DeepSeek」+「自定义」**;② **`platformPkgCount()`=0 ⇒ 兼容性预检的安全网整体失效**(不再拦 semver/导出符号不兼容的插件,T05 白做);③ `workspace-scoped-picker` import 即抛 ⇒ **目录选择器不可用**(目前未触发,一有用户 profile 就触发);④ 逃生口未写进 install.sh/README ⇒ 使用者**无从自救** |
| **开源发布** | 属**发布阻断项**:第一个用发行版 Node 的部署者就会踩,且**无任何报错**,会以「这功能好像没实现」的形式变成 issue |
⚠️ **关键巧合**:用户 09-14 00:1x 抱怨的「厂家选择怎么这么少、国内一家都没有」,在**导出部署上就是本缺陷的症状**;在我们服务器上是**另一个根因**(我第一版真没接目录)。**同一现象、两个根因** —— 我上一轮只修了自己那个,没意识到另一种部署上还有第二个。
### 为什么没发现(根因链)
1. **失败被设计成静默**:`listCatalogProviders()` 的 readdir 抛错被 catch 吞掉、返回 `[]`,注释还把它写成「目录不存在 ⇒ 返回空数组,不抛错:调用方据此降级」——**降级被当成特性写进了注释**,失败就等价于「没做」。
2. **只在一个布局上验收**:我们服务器 `/usr/local` 恰好是 npm 默认 ⇒ 写死值**是对的** ⇒ 我做的端到端验收(38 家全绿)只证明了「**在这台机器上**对」。判据里**没有第二种布局**。
3. **前端验收用桩掩盖了后端**:`verify-platform-admin-section.mjs` 把 `/api/me/model-providers` 打了 fixture ⇒ 前端断言全绿,与后端能否读到目录**无关**。
4. **同一个未经检验的假设被复制了三轮**:`workspace-scoped-picker`(档案 18 v3,最早)→ `plugin-compat.ts`(档案 71/T05)→ `model-catalog.ts`(我,档案 87)。**每轮都在同一种布局上验收通过** ⇒ 「同环境反复通过」掩盖了「跨环境从未验证」。
5. **我看见了预警信号却没当成待验证项**:`plugin-compat.ts` 的注释原文写着「平台内置包根目录(**本部署事实**;env 可覆盖…)」——"本部署事实"四个字就是作者**自己知道这是假设**的痕迹;我写 `model-catalog.ts` 时参考了这段代码,把它当"现成写法"照抄,**没把它转成一条待验证假设**。
6. **成本极低的一步没做**:当时只要在验收里加一句 `DSHS_DSH_BIN=/nonexistent` 或换个 root,立刻就会暴露 —— 这是**有能力发现却漏掉**,不是"发现不了"。
### 防再犯(可执行的)
1. 凡「读**别人**安装位置/版本」的代码 ⇒ 路径**必须走解析层**,且**至少一条单测用假根**(已落地:`scripts/verify-dsh-install.mjs`,15 项)。
2. **静默 catch 必须留痕**(本次**未做,待办**):`listCatalogProviders` 与 `platformPkgCount` 的降级路径应至少 `console.warn` 一次、或把"目录不可读"透出到 `/api/dsh/status` —— 否则"功能没生效"不可观测,这次就是靠人肉比对才发现。
3. **验收判据要显式包含"非本机布局"**(一句 env 注入即可)。
4. 看到注释里出现「本部署事实 / 目前是 / 暂时」⇒ **当场转成待验证项**。
---
## 04:45–05:1x 用户「一次性做完 不要留尾巴」⇒ 收尾全部清掉
| 尾巴 | 处置 |
|---|---|
| **降级静默**(P1 能潜伏的根因) | ✅ 已改为可观测:`model-catalog` 读不到 ⇒ `console.warn`(每进程一次)+ 新增 `catalogDiagnostics()`,并把 `{dir,readable,count}` 透出到 `/api/me/model-providers` 的 **`catalog`** 字段;`plugin-compat` 的 `platformPkgCount()=0` 同样告警一次;插件 **0.3.13** 前端**分开提示**「不可读(含路径)」与「目录为空」 |
| **picker 源码 ≠ 线上** | ✅ 升 **0.1.5** 重打包铺发(两实例实装 0.1.5 且含新代码)——**顺带做了最要紧的回归:在 `/usr/local` 下它的 lib 仍 import OK**(没把本来能用的改坏) |
| **逃生口未暴露**(源仓侧) | ✅ 源仓 `README.md` 配置表补 `DSHS_PACKAGE_DIR` / `DSHS_COMPAT_ROOT` / `DSHS_PI_AI_DATA_DIR` 三行(含"写死会静默失效"的警示) |
| **无回归网** | ✅ `verify-dsh-install.mjs` 扩到 6 组(新增**「读不到 ≠ 目录为空」的区分** + 告警只发一次);`verify-platform-admin-section.mjs` 加 2 条(目录不可读/为空文案不同),词典 **284/284** |
| **台账** | ✅ 新 **档案 88**(含「为什么三轮都没发现」的根因复盘)+ **T07 归档** + `INDEX.md`(§二 88 行 + §四 T07)+ `交接单/README.md §一` + 四件套(audit rc=0 / 一致性 ✓)+ 镜像 scp |
| **提交** | ✅ 代码仓 `cbaf3ad`→**`1d72e8f`**、文档库 `d1d1f11`→**`704cd86`**(均已 push,远端哈希已核) |
**本机/线上状态**:两实例 `business-plugins` **0.3.13** + `workspace-scoped-picker` **0.1.5**;后端已 build + restart;回归 `platformPkgCount` **223** / 厂家目录 **39**(端点回 38)/ picker import OK。
**唯一剩的、不在我 lane 的**:① 导出物侧(`install.sh` / env 样例 / 导出 README)补 3 个 env + 重建导出物 —— 已在回执里交给开源导出会话;② `INDEX.md §二` 中 **档案 82–86 未登记** 与 `skills/dsh-plugin-diagnose` 未登记(别人的 lane);③ 我刚才发现 `skills/dsh-opensource-release/SKILL.md` **又被其 owner 改了**(对账报 1 处不一致)⇒ 按规矩**未碰**。
## 08:0x–08:2x · guest 新会话(DSH 官方推荐插件 Top20)+ **P1(state 400)结清**
**新会话 `session-9217f396`(今早 07:57)**:用户问「dsh 官方推荐实用插件前 20」⇒ AI 产出**两份交付**:`DSH插件精选清单.xlsx`(下载用)+ 尝试 `univer_import` **失败**(GLIBC 2.35)⇒ 改用 **Facade API 原生重建** `DSH插件精选清单.univer`(4 sheet、逐格回读、数字格式生效、标记 ready)。
用户原话问它:「不能在浏览器会话页面中查看吗 univer插件没有这个功能吗」。
**我的复核(服务端链路全通)**:
- `GET /univer-api/state?file=…&sessionId=…`(按客户端真实形状 encodeURIComponent)⇒ **200**:`gatewayRunning:true`、`viewerUrl` 就绪、worktree `wt-mu0h2qv0-pa2aq3` 状态 ready、含 1 个 sheet 单元。
- `GET /univer-api/viewer/?file=<fileKey>` ⇒ **200**(37 KB viewer HTML)。
⇒ **`.univer` 现在能在会话里在线看**;**xlsx 不能**(要导入 = 原生绑定 = glibc 挡)。
**✅ P1 结清(档案 76 §七)**:挂了整天的「`/univer-api/state` 生产 400」按真实形状复测 = **200**。并给出**三类 400 分辨表**:① 插件信封 `{ok:false, code:INVALID_REQUEST}`(缺 sessionId);② 插件信封 `INVALID_FILE_PATH` / `SESSION_SCOPE_UNAVAILABLE`(文件不存在 / 旧会话失效 ← 09-13 那批 400 最可能是这两类);③ ⚠️ **代理层 Fastify 默认形状**(error: Bad Request / message: Client Error / statusCode 400)—— **我手工 curl 未编码中文路径**踩到,一度误判成「插件恒 400」。
**教训铁律**:curl 带中文/空格路径必须 `-G --data-urlencode`;见到 Fastify 默认错误形状,先怀疑「请求没到插件」。
**仍存在的两条(宿主环境,不是插件缺陷)**:
1. **xlsx/docx 导入导出** —— `exchange-node-binding` **无 JS 回退、无 env 覆盖** ⇒ 要么换 glibc ≥ 2.35 基底;要么走**数据级替代**(实例里已有 SheetJS:xlsx 读出→灌进 `.univer`,反向同理;掉样式/公式保真度)。**后者我能做**(与 office-file-generation 同一思路的镜像)。
2. **截图 / PDF / compile_svg** —— 宿主上**没有任何浏览器**(已 find 全盘确认);⚠️ **且装了也会撞内存**:headless Chromium 要 +200~400 MB,而 guest 配额 544 MiB(univer 网关已占 ~350 MB)⇒ 一跑大概率 **cgroup OOM(137)**。**结论:不建议装**,真要就得先提配额。
**未做到**:浏览器侧未能亲验(`agent-browser` 未安装,需 ~500 MB Chromium)⇒ 若刷新后仍看不到卡片/dock,需让 guest 里的 AI 跑一次 client 探针,或授权我装 agent-browser。
## 08:3x–08:5x · ⭐ **重大突破:不用换基底、不用容器,就能让 univer 原生绑定可用**
**起因**:用户问「为什么不升级 glibc」→ 我给出代价/收益对比后,用户说「按照你的方案试试」⇒ 我先做我承诺的**零风险干跑验证**(只取一份 glibc 2.35 rootfs,不启 dockerd、不碰防火墙),结果**比预期好得多**。
**实验链(全部实测)**:
1. 宿主现状再确认:Alibaba Cloud Linux 3 / **glibc 2.32** / kernel 5.10;`docker` 已装但**守护没跑**、无镜像;平台 supervisor 里有 `k8s-spawner.js`(容器形态在架构上留了口子)。
2. `objdump -T` 实测:两个 univer 绑定都要 **GLIBC_2.33/2.34/2.35** 的**版本化符号**;`libsql` 只要 2.18(所以它一直好用)。
3. 从 Ubuntu 22.04 取 `libc6`(**注意 deb 里是 `data.tar.zst`,Python 3.12 的 tarfile 不认 ⇒ 用宿上的 zstd 解**)⇒ 得到 glibc 2.35 的 20 个 so + ld.so。
4. ❌ 第一次只给 `libc/libm/ld` + 宿主 `/lib64` ⇒ **`GLIBC_PRIVATE` 冲突**(`libpthread.so.0: undefined symbol __libc_siglongjmp`)—— 因为我混进了宿主 2.32 的 libpthread(2.35 里它已并入 libc)。**必须整套 2.35 一起给**。
5. ✅ 给全 20 个 so 后:`node -v` 正常,`require` **两个绑定全部成功**(`exchangeExportSnapshot/exchangeImportToSnapshot`、`rustNumfmtFormat/...`)。
6. ✅ **端到端**:gateway + worker 都跑在 2.35 上 ⇒ **`univer_import` 一个 xlsx 成功**(列宽 44/176/88、单元格文本、SUM 公式全进来了)—— 正是昨天失败的那一步。
7. ✅ **运行时已就位**:`/usr/local/dsh-runtime/glibc-2.35/`(**5.0 MB**,20 so + ld.so + `COPYRIGHT-libc6.txt`);`process.report.getReport().header.glibcVersionRuntime` 自证 = **2.35**。放 `/usr/local` 是为了让 **bwrap 沙箱内可见**(`/usr` 只读挂载)。
**⇒ 结论**:**不需要换发行版基底,也不需要 docker/容器** —— 只要给 **worker / gateway 这两个进程**挂自带 glibc 2.35 即可(它们是独立进程,混 libc 的经典危险不适用)。
顺带收益:公式引擎也能切回 Rust(我的垫片本就是"绑定能用就用 Rust"⇒ **自动恢复**)。
**待接线(下一步)**:① fork 侧把两处 spawn(`worker.ts` / `processes/gateway/launcher.ts`)改成经 `ld.so --library-path` 启动,env 开关(如 `UNIVER_DSH_GLIBC_2.35_DIR`)控制、可回退;② 平台侧把该 env 给实例(`/etc/dshs.env` 或 supervisor env,实例继承平台进程环境);③ 重建 0.2.28 → 投放 → **全回归**(导入/导出/截图除外/网关/worker + 档案 76 的 socket 链路)。
**许可证**:glibc 是 LGPL ⇒ 已随附 `COPYRIGHT-libc6.txt`;**对外开源导出时不要把这份运行时打进导出物**(它只在服务器上)。
### 08:3x–09:0x · ✅ 接线完成并投放 **0.2.28**(原生 Office 导入导出已启用)
- **插件侧改动**(最小、可回退):新增 `src/host/processes/glibc-runtime.ts`(`nodeLaunch()`:有运行时则 `ld.so --library-path <rt>/lib:/usr/lib64:/lib64 <node> <entry>`,否则原样);在**两处 spawn** 接线 —— `host/adapters/unit-content/worker.ts`、`host/processes/gateway/launcher.ts`。目录**自探测**(默认 `/usr/local/dsh-runtime/glibc-2.35` + `UNIVER_DSH_GLIBC_RUNTIME_DIR` 可覆盖/置空关闭)⇒ **平台侧零改动**(不改 env、不重启 dshs)。
- ⚠️ **本机踩坑**:`launcher.ts` 是 **CRLF** 行尾,用 `\n` 模式替换会静默匹配不上(worker.ts 是 LF)⇒ 改本机 TS 前先 `cat -A` 看一眼行尾。
- **验证(真机证据)**:`POST /univer-api/gateway/start` → `{"ok":true,"reused":false}`;`/proc/<pid>/cmdline` 确含 `ld-linux-x86-64.so.2 --library-path …`;`/proc/<pid>/maps` 里 `libc/libdl/libm/libpthread` **全部**来自 `/usr/local/dsh-runtime/glibc-2.35/lib/`;`/univer-api/status` = gateway running;实装 `0.2.28`、实例 active。
- **顺带搞清一条**:`/univer-api/state` 需要**活会话**(实例重启后 SessionStore 内存态未水化 ⇒ `SESSION_SCOPE_UNAVAILABLE`)⇒ 这就是 09-13 那批 400 的第二种可能;自检请用 **`POST /univer-api/gateway/start`(不需要会话)**。
- 落档:档案 76 **§八**(实验链 / 落地 / 真机验证 / 遗留注意)已写入并同步;四件套 rc=0。
- **仍未解**:截图 / PDF / `compile_svg`(缺浏览器 + 内存不够,与本条无关)。