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

241 lines
48 KiB
Markdown
Raw Normal View History

# 工作日志 · 2026-09(第 10 片)
> ⚠️ **本目录日志已按【月】分片**(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-12.md
> **上一片**:`2026-09-下8.md` **下一片**:`2026-09-下10.md`
---
## 18:2x 代码仓三方同步(用户:「改为3小时同步一次」)
- 建自动化 **「代码仓三方同步(DSH)」 FREQ=HOURLY;INTERVAL=3**;原「遗留项自动推进」8 小时保留(职责不重叠)。
- 三方原状:Gitea origin/master 停在 **ebe8075**(共同祖先);本机 0e141a4(+2);服务器 b23e386(+3)。
- **关键诊断**:忽略行尾后,business-plugins.ts/whitelist.ts/security-scan.ts 的「1450/638/296 行差异」**全是 CRLF 假差异**(core.autocrlf=true + 无 .gitattributes);真实差异只有 3 文件,且**本机更新**(client.js/package.json=v0.2.5 对比度精修;orchestrator.ts 含类型+防御)。
- 处置:update-ref 建回滚点 backup-pre-sync-1815(0e141a4) → 备份 4 个本机独有文件 → reset --hard srv-incoming(b23e386) → 回填 → commit **da3e0f9** → push(ebe8075..da3e0f9,**FF,无需 force**)。
- 结果:本机 = Gitea = da3e0f9;服务器 b23e386(落后 1,**未动其工作树**)。
- **两个坑(已写进自动化 prompt)**:① 本仓库 git diff 会把行尾差异算成整文件改写 → 一致性判定用 `git hash-object` / 加 `--ignore-cr-at-eol`;② `git branch <名> <commit>` 本机静默不落地 → 改 `git update-ref`。
## 18:3x 两项方案变更落地(持锁 plan-session-1827,已释放)
- **① AnySearch**:用户拍板「三方插件一律 admin 导入候选池 + 用户自助启用,**不铺**」→ 落 3 处口径:档案 64 §8.3 追加修正块(第 2 条直铺命令作废)、03-路线图 档案64 行、BRIEF §3 行。执行口径改以 **档案 65 §7.3** 为准。
- **② 子代理 approval**:源码级取证(**dsh-subagent/lib/types/child-agent.js**)—— 子代理沙箱**只继承「父会话显式 override」**(注释原文:never deployment defaults or one-shot grants)+ approval **硬编码钉死 never**(语义 = 需要审批的操作**自动拒绝**)。→ 平台的 DSH_PERMISSION_MODE 走"部署默认值"通道,恰被官方排除 ⇒ 子代理永远无 shell。
· 用户判据「不扩大用户访问边界」**成立**(子代理同实例/同 uid/同 cgroup/同 bwrap)。
· 但**平台层无干净解法**:env 无效(被官方排除)/profile patch 无效(改的仍是默认值通道,approval 是硬编码常量)/改会话种子已被档案 45 否决/改官方包违 R2。
· 已在 **档案 42 文末追加「修正(2026-09-12)」**小节 + 03-路线图对应行改为"已取证"。
- 校验:docs-audit / docs-manifest / docs-consistency 三件套 rc=0。
- **下一步**:实例内新会话派子代理跑 bash 实测 —— 可用则本项自动闭环。
## 18:3x 子代理 shell **闭环**(用户追问:用户自己设权限是不是就支持了)
- **答案:支持**。源码复核 `overrideOf`(dsh-sandbox-policy 与 dsh-user-approval)→ 读的是**会话日志里最后一条 sandbox/mode 事件**、**不与部署默认值比较** → 只要用户在会话内切过一次档位,子代理即继承 danger-full-access(append source: delegation)→ 该模式**无待审批操作** → 钉死的 never 不再构成阻碍 ⇒ **子代理可用 shell**。
- ⇒ **平台侧零改动**,走官方机制;边界不扩大(继承的是用户自己选的档位)。
- 操作步骤:新开会话 → `/permission workspace-write` 再 `/permission danger-full-access`(**先切走再切回**,确保日志写事件)→ 派子代理跑 bash 验证。残留不确定:设置 UI 选到同一档位时是否写事件。
- 已更新:档案 42「追加结论」+ 03-路线图 对应行改为「已闭环」。校验 audit/manifest/consistency rc=0。
## 18:3x 档案 42 ③ **实测通过并关闭**
- 用户实测:实例会话中派子代理执行 `bash -c 'echo subagent-ok'` → **原样返回 subagent-ok** ⇒ **子代理 shell 可用**。
- 结论:**平台侧零改动**、无需放权评估(不扩大用户访问边界)→ 档案 42 ③ 关闭。
- 已更新:档案 42 追加「实测结果」段 + 03-路线图 对应行改为「已闭环并实测通过」。校验三件套 rc=0。
## 18:3x 待改清单(**因锁被占而暂缓**:session-brief-refresh-1835 自 18:32 持全局锁,按纪律停手)
- [ ] **业务技能投放口径**:用户已讲过(T03 §决策点 4 原话「业务技能能否打包到插件中一起安装和使用,**不要分开管理**!」)→ 结论 = **随功能插件包投放,不走门户技能管理单独投放**。待改 2 处:`03-路线图 §二` P2 行(现写"是否投放仍待定")、`BRIEF §3`(现写"业务技能是否投放")。
- [ ] **删除 Cookie 域收窄**(用户 2026-09-12 明确"这个事情删除"):`03-路线图 §二` 暂缓行 + `BRIEF §3` 里的提及。依据:档案 05 PoC-2 ② 复核 = 共享 Cookie 是**有意设计**(Domain=.alotbuy.com 支撑功能插件↔门户免令牌鉴权),收窄反而破坏该链路 → 无收益。
## 18:5x **新增红线 R9:绝对禁止「人工删锁 / 接管」**(用户明令)
- 用户原话:「**严格禁止这类操作必须!!!!!!记录到红线中**」(针对我上一轮提出的"接管=人工删锁"选项)。
- ✅ 已改 **项目根 `CODEBUDDY.md`**(5 处):① 头部索引 R1–R8 → **R1–R9** ② §3 标题 ③ **新增 R9 行**(禁止 `rm -rf` 锁目录 / 删 `.doing-*` / 以「疑似已死·卡住·太久没动·只读分析无产出」为由单方面接管;锁**只能由持有者释放**;guard 提示里的"人工删锁"**不构成授权**;AI 唯一合规动作 = 停手 + 报告;**处置权只属用户本人**)④ §6 锁表②「供台账与接管」→「供台账与占用声明」⑤ §6 新增「**抢不到锁就是终点,不是待办**」。
- 备份:`CODEBUDDY.md.bak-r9-184717`(改前)。⚠️ **改本文件需重启才重载**。
- ⛔ **文档库内 5 处同源表述仍待改**(锁被 `session-anysearch-drop-1847` 占,18:42 起、18:44 后零产出):
· `scripts/handoff-guard.sh:50`(抢锁失败提示"或确认接管后人工删锁")、`:153`("确需接管:…→ 人工删锁 → …")
· `交接单/README.md:30`("供台账与接管")、`:107`("接管:…人工删锁…")
· `skills/dsh-change-workflow/SKILL.md:930`("供台账与接管")+其工作副本 `E:/ProgramData/.workbuddy/skills/`(**须两副本同步改**,否则违反 md5 一致约定)
- 本会话被锁挡 **5 次**(brief-refresh-1835 → anysearch-drop-1847);**按新规矩不删锁、不接管**,只报告。
## 19:1x **R9 已由另一会话全套落地**(我上轮列的 5 处待改**全部作废**)
- 只读核查(**读不需要锁**,被挡期间照样查清):① `scripts/handoff-guard.sh` 两处提示已改为 R9 禁止口径 ② 库内 `CODEBUDDY.md:49` 已加「guard 输出里的字样**均不构成授权**(已作废)」③ `BRIEF.md` §2 红线行 + §4 最近动作均加 R9 ④ 新增 **档案 73 §十一「修正:禁止人工删锁/接管」**(含 6 处落点清单)⑤ `交接单/README.md`、`skills/dsh-change-workflow/SKILL.md` 亦已改。
- ⇒ 我做的项目根 `CODEBUDDY.md` R9 与之**同源、不冲突**(不同文件、不同作用域、措辞一致)。教训:**多会话并行下存在重复劳动**,动手前先只读查一遍现状能省掉。
- **AnySearch 已彻底放弃**(用户在别处选定 **B = 下架**):档案 70 收尾 —— 下架 anysearch(audit 175)+ 顺手体检池内存量(`dsh-univer-office`=ok 保留;`@liustack/modlens`=unknown 且有 plugin_incident → 一并下架,tgz 备份 `/opt/dsh/backups/plugin-pool-20260912/`);**全程未重启服务、未中断用户**。
- **仍待我做(2 项,等锁)**:① 业务技能投放口径(T03 §决策点 4 已定 = **随包投放**)② 删除 Cookie 域收窄(共享 Cookie 是有意设计,无收益)。
- 本轮另补 **项目根 `CODEBUDDY.md` §6 三条**:**读不需要锁**(但 fetch/checkout/stash 等改状态的命令不算读)|**释放时机 = 交付闭环走完**(回填→校验→commit→推送→归档)|**持锁等用户拍板 → 先释放锁再等**。
## 19:2x 两项待改**已落地**(持锁 plan-session-1918:第 1 次被 session-cleanup-1916 挡 → 有界等待后第 2 次抢到,已释放)
- ① **业务技能投放口径**:`03-路线图 §二` 该行改为 **✅已定** —— 用户原话「业务技能要打包进插件里一起安装和使用,不要分开管理」→ **随功能插件包投放**(`交接单/T03` §4.1),**不走**门户技能管理单独投放;`BRIEF §3` 去掉该待办项。
- ② **Cookie 域收窄**:`03-路线图 §二` 由「暂缓」改为 **❌已复核 · 不做**(用户要求删掉此待办);`BRIEF §3` 去掉该提及。依据 = 共享 Cookie 是有意设计(支撑插件↔门户免令牌鉴权),收窄会破坏该链路、收益为零(档案 05 PoC-2 ②)。
- 校验:docs-audit / docs-manifest / docs-consistency **三件套 rc=0**。⚠️ **未 commit**(未获授权)—— 闭环按新 §6 规则只走到「校验」,commit/推送待用户发话。
- ⚠️ 顺带发现:**`BRIEF §3` 那条「档案 64+65 · AnySearch 能力 …待用户定 A/B」已过时**(用户已在别处选 B = 下架,档案 70 收尾 18:56 已记录)→ 待下次持锁一并修。
## 19:2x BRIEF §4 一致性修正 + T03 执行条件核实(持锁 plan-session-1920,已释放)
- **只读先查的价值再次验证**:BRIEF §3 那条 AnySearch「待定 A/B」**已被别的会话修掉** → 我上轮承诺的"下次持锁一并修"其实无需做。实改只剩 1 处:BRIEF §4「档案 70 …(已止损,**能力去留待定**)」→「(已止损;**能力已按用户决定下架**)」,与同段开头「AnySearch 已彻底放弃」自洽。三件套 rc=0。
- **T03 执行条件核实**:① 本机产物在 D:/dshworkspace/plugin_package/dist/dsh-plugin-mcn-suite-0.3.0.tgz(1.90 MB / 11:15)② **guest 实例正在运行**(scope dsh-100002-299e7f95,uid 100002 ↔ 4092b965…)→ 「guest 启用」会**重启它**(断约 1 分钟)③ **dataRoot = /var/lib/dshs**(/opt/dsh/artifacts/ 里那批 business-plugins-0.2.x.tgz 是**平台自身**的功能插件包,**不是**候选池)。
- ⇒ T03 剩余步骤(上传候选池 → guest 启用 → 启动实例 → 验收 → 归档)**涉及重启 guest 实例**(R8)→ 已请示用户,**未擅自执行**。
## 20:2x T03 执行前只读核查(按 R8 自主判时机,**未动生产**)
- **判时机**(用户要求「规则你都知道,别问」→ 自主判):guest 的 ws/MCN短视频创作 mtime = **20:06**、sessions = **20:02**(距核查 13–17 分钟)→ **活跃中** → 按 **R8「能避开活跃时段就避开」** 暂缓「启用 + 重启」。
- **核实 3 条事实**:① 候选池 `/var/lib/dshs/business-plugins/` **只有 `dsh-univer-office`**(39.9 MB)⇒ T03 §七 防线②「下架旧 7 包」**无需做**(那 7 包**从未上架**)② **guest 的 bundles 不含任何旧包** ⇒ §七 头号风险(新旧并存 → duplicate loader entry id 崩实例)**不成立**,切换顺序可简化 ③ dataRoot = `/var/lib/dshs`,库 = `dshs.db`。
- **⚠️ 新发现的不一致**:guest bundles 里仍启用 **`@liustack/modlens`** —— 该包今天已从候选池下架(档案 70 收尾),但 profile 仍启用它(包文件尚在 node_modules,**暂不致命**)→ 属「**已下架却仍启用**」态 ⇒ **与 T03 共用同一次重启窗口摘除**(避免两次中断用户)。
- 落档:`交接单/T03` 追加「只读核查结论」段(含时机判据);`03-路线图 §二` 新增该待办行。三件套 **rc=0**。
- 计划:等 guest 空闲(判据 = 最近 30 分钟无活动)→ **一次窗口做完**「上传新包 + guest 启用 + 摘 modlens + 重启 + §八验收」,避免多次中断。
## 20:2x guest 最新两会话排查(只读,未改任何文件)
- 对象:`8cdf723e`(09-11 21:58 建,11 turn,最后事件 20:13)与 `eda867a0`(09-12 20:02 建,1 turn/45 工具,20:11 结束)。两会话档位均 `danger-full-access`/`never` ✓(档案 55 的 P1 未复发)。
- **P0 根因(实锤)= 实例 OOM**:`20:11:10 kernel: oom-kill:constraint=CONSTRAINT_MEMCG, oom_memcg=/system.slice/dsh-100002-299e7f95.scope, task=node,pid=355329,uid=100002` → 平台 `crash-restart` `exitCode 137` → 20:12:11 stable。**两个会话的工具调用在同一时刻(20:11:05 / 20:11:09)被中断**,用户 20:12:12 问「是报错了吗」。
- **当前仍在临界**:`MemoryCurrent=400175104 / MemoryMax=402653184` = **99.4%**(新实例才跑 10 分钟);`dsh-instance-mem.log` 显示 uid 100002 峰值**长期贴顶 382/384 MiB**(02:35Z 起);近 3 天该实例 crash-restart ≥7 次(09-11 23:56 连崩 5 次 exitCode 1;09-12 00:19 / 01:28 / 09:37 / 20:11)。
- `NODE_OPTIONS=--max-old-space-size=256` **已正确注入**(档案 58 修复在位)→ 说明 256 MiB V8 堆 + 实例其余开销**仍撑不住 384 MiB cgroup 上限**。
- **P1 = `dsh-univer-office` 与本平台 loopback 封锁天然不兼容**:nft `table ip dsh_egress` 有 `meta skuid 100000-199999 ip daddr {47.77.182.89, 127.0.0.0/8, 172.17.0.1, 172.18.16.212} tcp ... reject with tcp reset`(计数 159)。插件需「bundled Gateway」监听 `127.0.0.1:9080+` 走 loopback HTTP → 实例内 curl=000 / python=Connection refused / node fetch failed(RST 而非超时)。
- 后果:`univer_new` 报 `bundled Gateway did not become ready within 10000ms`×3;`抖音爆款视频榜_2026-09-12.univer` **全盘 find 无此文件**(从未落盘);前端 `/univer-api/state` 从 20:08:08 起持续 **400**,已 153 次且**仍在刷**。
- ⇒ 档案 71 静态预检判 `ok` 没错,但**检测不出运行时架构冲突**(预检盲区)。
- ⇒ agent 反复起 gateway(9081/9082/9099 多进程)**可能正是 20:11 OOM 的推手**。
- 附带:DNS 类失败 2 次(`ENOTFOUND lg.racknerd.com`、`api.bgpview.io`),外部站点侧,非平台问题。
- 方法沉淀:取证路径 = `/var/lib/dshs/users/<uid>/home/sessions/--var-lib-...-ws-<工作区>--/<session-id>/session.jsonl.zstd`(**不是**档案 55 写的 `users/<uid>/home/sessions`,uid `4092b965…`=guest、`cce6d1cd…`=admin)。
- ⚠️ **工具缺陷(未修,待授权)**:`dsh-server-docs/scripts/sess-list-presets.mjs` **首行注释缺 `/**` 起始符** → `node` 直接 `SyntaxError: Unexpected token '*'`,该脚本当前不可用。**按 R7 未擅自修改文档库**。
- ⚠️ 与 T03 计划的关联:20:2x 那条定的「等 guest 空闲再重启」窗口,**guest 此刻仍活跃且刚 OOM 崩过** → 内存贴顶是重启窗口的额外风险项。
## 20:3x guest 内存归因 + 隔离复现实验(只读 + /tmp 隔离,未碰生产)
- **内存构成实测**(cgroup v1:`/sys/fs/cgroup/memory/system.slice/dsh-100002-af3f3fa9.scope/`):
- `usage_in_bytes` 382 MiB / `max_usage_in_bytes` **384 MiB = limit**(曾精确打满)
- `memory.stat`:**rss 363.7 MiB(95.4%)+ cache 仅 17.8 MiB** ⇒ **几乎全不可回收,内核 OOM killer 无路可退**
- `rss_huge 82 MiB`(THP 放大);smaps:Anonymous 372404 kB、Pss_Anon 363.7 MiB、`[heap]` 段 63.9 MB
- **内存非恒定贴顶,而是 316~384 MiB 波动**(清理后实测降到 316)→ **波峰正好在死亡线**
- **插件账本(隔离 cgroup 实测,纯只读 import)**:node 空跑 56 MiB → **+`dsh-univer-office` = 121 MiB(该插件 +65 MiB)** → +libsql 125 MiB。⚠️ univer 磁盘 **168 MB**(含 `exchange-node-binding-linux-x64-gnu` 29 MB、`engine-formula-rust-binding` 10 MB 等原生绑定);`node_modules` 总览还有 puppeteer-core/chromium-bidi/libsql/zod×2。
- **对照**:`dsh-instance-mem.log`(时间戳为 **UTC**!)显示 admin(uid 114801,**未装 univer**)峰值 **233 MiB** vs guest(uid 100002)峰值 **382 MiB** → 差 149 MiB,其中 65 MiB = univer 加载实测值。
- **三条隔离实验(`systemd-run -p MemoryMax=402653184`,跑完即回收,全程未碰生产)**:
1. **插件账本**:同上,未撞墙(125 MiB)。
2. **JS 堆压力 + `--max-old-space-size=256`** → `FATAL ERROR: Reached heap limit`(Mark-Compact 254 MB),systemd `code=dumped/status=ABRT` ⇒ **死亡路径 A = V8 堆限自崩**。
3. **纯堆外分配(Buffer)** → `kernel: oom-kill:constraint=CONSTRAINT_MEMCG, task=node` + `code=killed/status=9/KILL` ⇒ **死亡路径 B = 内核 OOM SIGKILL**,**与 20:11 真实事故记录逐字同构**。
- **结论**:不是单一"坏东西",而是**配额本身零余量** —— V8 老生代上限 256 MiB(占配额 2/3)+ 插件 65 MiB + dsh 本体/堆外 ~50 MiB + 页缓存 18 MiB ≈ 386 MiB > **384 MiB 上限**。**两条死亡路径都实测复现**,且**都不产生可读日志**(一条 ABRT、一条 SIGKILL)。
- **宿主容量**:物理内存仅 **1.83 GB**(MemTotal 1915896 kB),available 924 MiB,swap 1 GB 已用 194 MB → 每实例 384 MiB ×2 + orchestrator ≈ 用满。**这是容量问题,不是 bug**。
- 生产实例全程 `active`,未重启、未中断用户;实验 unit 无残留,`/tmp/memsim` 与本地 `_tmp_memsim` 已清理。
## 20:4x 内存治理方案调研(**只读**,未改任何配置)
- **关键发现①:平台已预留 env 覆盖点,改 V8 堆限零代码改动**
`src/supervisor/orchestrator.ts:402` → `NODE_OPTIONS: process.env.DSH_INSTANCE_NODE_OPTIONS ?? '--max-old-space-size=256'`
⇒ 运维只需在 **`/etc/dshs.env`**(service 的 `EnvironmentFile=`)加一行 `DSH_INSTANCE_NODE_OPTIONS=...`;**改 .env 只需 `systemctl restart`(不必 daemon-reload)**。
⚠️ 副作用:该 env 被实例内**所有 node 子进程继承**(用户自己跑 node 也被限),非 node 任务(python/ffmpeg)不受影响。
- **关键发现②:档案 58 的设计假设与实测严重脱节**(源码注释自述,这是定靶心的依据)
- 设 256 时的依据:「dsh 实测老生代仅 **27~30 MiB**,三倍余量足够」→ 现在实测被会话数据填到 **250 MiB**,**低估近 9 倍**
- 配额依据:「384 − 145 = **239 MiB 留给用户任务**」→ 实测 dsh 自身吃到 **363.7 MiB**,**留给用户任务只剩 ~20 MiB**(用户跑 python/ffmpeg 立刻爆)
- 另有 `NODE_COMPILE_CACHE=<home>/.node-compile-cache`(削 code range 88 MiB 冷启动开销)
- 注释亦载明:dsh 是 223 包 / 717 JS 文件 / 21.2 MB 源码,私有内存里 **88 MiB 是 [anon](V8 code range)**
- **配额硬编码在代码里**:`orchestrator.ts:625` `-p MemoryMax=384M`;改它要走「改码 → npm run build → drain + 重启」(注释里给了回滚口径)。
- **方案结论(待用户拍板,本轮未执行)**:① 压 `DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160`(零代码,省 ~90 MiB)② guest 在「功能管理」卸 `dsh-univer-office`(省 65 MiB + 消除 `/univer-api/state` 无限 400)③ 配额 384→512 **暂不动**(宿主 1.87 GB,2 实例即 1.02 GB,风险大于收益)④ 加内存阈值告警(现状贴顶是静默的)。**①②合计可把峰值从 383 压到 ~230 MiB**。长期解法 = 升宿主内存(花钱,须用户决策)。
- 两个动作都要重启/重载,**会断在线实例约 4 秒**(T05 实测口径)→ 按 R8 已向用户报明,等窗口。
## 21:0x 用户拍板「先做 1、4」→ **已落地并实测验证**
- **锁**:全局执行锁 + 服务器侧操作锁(`instance-mem-tune`);完工**反序释放**(先 op-lock 后 exec-lock)✓
- **改动 1(零代码)**:`/etc/dshs.env` 追加 `DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160` → `systemctl restart dshs`。**走的是平台预留覆盖点**(`orchestrator.ts:402`),未碰官方 dsh。
- 实测:orchestrator 进程 env 已读到;**实例 `NODE_OPTIONS=--max-old-space-size=160`**、`v8 heap_size_limit=163M` ✓
- **效果:实例 `rss` 363.7 → 195.5 MiB(−46%)**,cgroup 总量 382 → 274 MiB(余量 ~110)
- 验证方式(R4 合规):自建临时 session 直插(**不走登录、不踢用户**)调 `POST /api/dsh/enter` 拉起实例 → 验证 → `[cleanup] 已删除 1 条`
- **改动 2**:`instance-mem-sample.cjs` 加**阈值告警**(85% × 连续 3 次 → `WARN` 前缀 + `⚠` 标注;未达连续则标 `(高位 N%,x/3)`)+ **`tooSoon` 保护**(<60s 的重复触发只保持计数,防手动连跑虚高);cron 每小时 → **每 10 分钟**(否则 3 小时才告警,抓不到分钟级 OOM)
- **档**:新增 `04-调整方案/74-实例内存治理-V8堆限下调与阈值告警.md`;`INDEX.md` 登记 04-74;三件套(audit/manifest/consistency)**rc=0**。**未 scp 到 /opt/dsh/docs、未 commit**(按 §4 等用户发话)
- ⚠️ **部署陷阱(已写入项目根 `CODEBUDDY.md §7` 第 5 条)**:本机 `D:\github\dsh_shenxian` 的 `core.autocrlf=true` ⇒ **工作树 CRLF**,服务器是 **LF**;直接 scp 会把 CRLF 带进生产 → 必须 `tr -d '\r'` 先转,**且只转本次要传的那一个文件**。另注:`grep -c $'\r'` 在 git bash 里会误报(`od -c` 才准)
- **技能**:新增 `dsh-instance-diagnose`(三层定位 / cgroup 三件套 / 两条死亡路径 / 隔离复现 / 6 条踩坑)
## 21:0x 追测:**「谁占了内存」的重要纠正**(会话数据 vs 加载成本)
- **整个会话(2546 事件、11 turn)的全部字符串只有 0.9 MB**(最大单串 40820 字符,说明 dsh 对工具输出有截断)⇒ **对话内容根本不是内存来源**,"减会话长度/web_fetch"几乎无用
- **空载实例(零会话,仅 enter 后)smaps 拆解 = 285 MiB**:匿名 214.0(`V8 堆/匿名数据` 155.9 + `[heap]` 58.1 + JIT 2.2)+ 文件映射 69.1(其中 **node 本体 59.8**)+ 其他
- ⇒ **占用主体 = 「dsh 本体 + 148 官方插件 + 业务插件」的加载**(模块对象与源码),**不是操作**。配 `--max-old-space-size=160` 后 V8 按"许可"预留到接近上限,故匿名映射仍 ~214 MiB
- ⚠️ **两个实验设计坑(下次别踩)**:① `systemd-run` 起服务**不继承调用者 env** → 必须 `--setenv=NODE_OPTIONS=...`,否则堆限根本没生效(用 `v8.getHeapStatistics().heap_size_limit` 自证)② **同质字符串会被引擎优化**(`'x'.repeat(N)+i` 走 cons string 惰性拼接,不复制前缀)→ 实测"投喂 23 GB 只涨 140 MB"的假数据。**结论:内存类模拟实验不可靠,优先用真实数据统计**
## 21:1x 插件内存成本逐个实测(**推翻"装得多就占得多"**)
- 方法:隔离 cgroup(`MemoryMax=600M`)+ `node --expose-gc`,逐个 `import` guest 的插件测增量(**`.cjs` 不支持顶层 await → 必须 `.mjs`**)
- **结果(rss 增量)**:`dsh-univer-office` **+65.1 MiB** | `libsql` +7.8 | **平台自研 ×3(`@dsh-local/portal-entry`、`workspace-scoped-picker`、`business-plugins`)= 0.0 MiB** | `puppeteer-core` **0.0 MiB**
- ⇒ **成本由"装了什么"决定,不是"装了几个"**:一个重型插件 ≈ 无穷多个轻量插件。**平台自研 3 个合计 0 MiB(写法是轻的)**,是正面样板
- **`puppeteer-core` = 0 MiB 是"懒加载有效"的实证**(只 import 主入口、不启动浏览器)
- **univer 为什么重**:`lib/index.js` = **单文件 bundle 5400+ 行 / 6 MB**,**顶层静态 import** 拉起重型依赖(连 `@puppeteer/browsers` 都在,一个表格插件带浏览器依赖),全文件**仅 2 处动态 import** ⇒ 优化空间在「改懒加载」,但它是第三方的(要 fork 或提上游)
- **guest 的 `bundles` 实际清单**(`profiles/web/package.json`):`@deepseek-ai/dsh-base`(148 官方插件基座)+ `dsh-web-app` + 3 个 `@dsh-local/*` + `dsh-univer-office`。注:`@liustack/modlens` **只在 node_modules 残留、不在 bundles**(已禁用)
- ⚠️ 插件的 `pnpm remove`(禁用)**必须重启实例**才真释放 —— Node 模块缓存不卸载
- 已把上述实测数据与两条实验坑补进技能 `dsh-instance-diagnose`
## 21:3x 用户报「继续检查 guest 最新对话还是有问题」→ 已定位
- **用户的问题 = univer 打不开**:会话 `eda867a0`(21:28 最后更新)里,用户 21:24「再试试呢」→ agent 重试 `univer_new` **又失败**(`bundled Gateway did not become ready within 10000ms`);21:27「如何打开 univer」→ agent 深挖源码后转向用 python 产出 xlsx+docx(21:25 已生成 `reports/抖音爆款视频表_2026-09-12.xlsx` + `日报.docx` + 4 图表)
- **★ 独立验证出的根因 = 架构级不兼容(配置不可修)**,两条**独立**阻碍:
1. **Host→Gateway**:实例内 node 连不上自己起的 `127.0.0.1:9080`(nft `dsh_egress` 对 `skuid 100000-199999 → 127.0.0.0/8` 是 `reject with tcp reset`)
2. **Browser→Viewer**:源码**硬编码** `gateway = http://127.0.0.1:${port}`、`viewerUrl = ${gateway}/?file=...`,client 拿它当 **iframe src** ⇒ 浏览器去**用户自己电脑**的 127.0.0.1 找 Viewer ⇒ **即使解封 loopback 也打不开**
- ⇒ 该插件的 Viewer **假设「浏览器与实例同机」(本地部署)**,与托管多租户平台根本不兼容 → **治理结论:候选池下架 / 用户禁用**(顺带省 65 MiB)
- **内存现状(跑会话后)**:cgroup **370 / 384 MiB**(余量仅 14);smaps 对比空载:`V8 堆/匿名 155.9 → 275.5`(+120)、文件映射 69.1 → 110.1 ⇒ **只要跑了活,V8 堆就涨到"许可上限"附近**。**但全程无崩溃、无 OOM** ✓ —— 堆限 160 的防崩作用已验证
- 服务级 20:53 后**零 crash-restart** ✓
- 技能 `dsh-instance-diagnose` 已更新:① `NODE_OPTIONS` 更正为 **160**(含档案 74 与覆盖方法)② 新增「**已知与本平台不兼容的插件**:`dsh-univer-office`」整节(含两条阻碍的机理与验证命令、替代路径)
- 排查用会话副本已清理;**全轮只读,未改生产**
## 21:5x 用户拍板「把托管友好性作为插件评估与改造的一个点」→ **已立档(档案 75)**
- **新增档案 `04-调整方案/75-插件评估新增托管友好性与资源成本两个维度.md`**(📋 只立标准,**未改预检脚本**)+ `INDEX.md` 登记;三件套 **rc=0**;**未 commit、未 scp docs**
- **维度 A 托管友好性(H1–H4)**:核心判据一句话 = **「插件的一切对外交互,是否都能走平台已有的那一条入口」**(浏览器侧只用相对路径;实例侧不依赖 loopback 网络服务)
- **H2 = 阻断级**:**client bundle 里出现绝对 URL(`http://` 字面量)= 会交给浏览器 = 托管平台必失效且平台侧无法补救**(univer 死因)
- H1 硬编码 loopback / H3 自建监听=警告;H4 交付 URL 由谁生成为人工研判
- 检测复用档案 71 的 `walkJs` 静态扫描框架,**不另起一套**
- **维度 B 资源成本**:加载 `rss` 增量,**>30 MiB 标注 / >60 MiB 强提示**(univer 实测 +65.1;自研 ×3 = 0.0 是正面基线)
- **改造规范 R-a~R-e**(自研/改造插件必守):① 对外入口唯一(挂 Host 的 webServer,同域相对路径)② **内部通信走 IPC(unix socket / stdio),不用 TCP loopback** ③ 不新增监听端口 ④ 重型依赖懒加载 ⑤ 交付浏览器的地址一律由 Host 生成且为相对路径
- **与既有档案关系**:是**档案 71 的新增维度**(沿用其分级语义);与 70(预检盲区致事故)同属系统性补盲;与 74 的资源成本同源
- 技能 `dsh-instance-diagnose` 的「已知不兼容插件」节已加**指针**指向档案 75(不复制内容,保持单一来源)
- **落地(待排期)**:预检脚本加 H1/H2 静态扫描 + H2 命中判 `blocked` + 静态体积提示 → **属生产代码变更,须另立交接单**(规划与执行分离);T03 的 MCN suite 起按 R-a~R-e 验收
## 22:0x 用户问「现在可以改造这个插件了吗」→ 可行性已验证(**三个门槛全过**)
- **门槛① 源码可得** ✅:已 clone 上游到 `D:\github\dsh-univer-office-src`(**浅克隆**,395 文件 / **216 个 ts** / 15 MB,含 `src/`、`packages/`、`scripts/`、4 个 tsconfig、`test/`)。仓库 `github.com/dream-num/dsh-univer-office`(走代理 10800)。⚠️ 候选池里的 tgz **只有产物无源码**(`files` 只发 lib/artifacts/docs/skills,构建脚本都没发布)→ **改造必须回到上游**
- **门槛② 依赖可装** ✅(**原本最大不确定项**):`.npmrc` 把 `@univer-cli`/`@univerjs`/`@univerjs-pro` 三个 scope 全指向**私有 registry `https://insider-npm-registry.univer.work/`**,且仓库**未配 authToken**;实测该 registry **公开可读**(包元数据 HTTP 200,返回正常 npm 元数据)→ **无需 token 即可装**
- **门槛③ 改动点定位** ✅(TypeScript 源码,模块化清晰:`src/{gateway-app,host,client,viewer-app,viewer-support,render-machine,workers,shared}`)
| 类 | 位置 | 要点 |
|---|---|---|
| ① | `src/gateway-app/server.ts:11` / `:136` | `const LOOPBACK_HOST = '127.0.0.1'` + `server.listen(port, LOOPBACK_HOST, …)` → 需支持 unix socket |
| ② | `src/gateway-app/main.ts:16-17`、`src/host/processes/gateway/gateway-process.ts:29`、`protocol.ts:20` | origin 构造 `http://` / `ws://127.0.0.1:${port}` → 需可配置 |
| ③ | `src/host/provider/gateway-univer-service.ts:538` | `viewerUrl: ${gateway}/?file=…` → 需改相对路径 |
| ④ | (**新写**) | **Host 侧新增反向代理**(`/univer-api/viewer/*` → gateway),**含 WebSocket upgrade 转发** ← **真正的工作量在这** |
- **工作量诚实评估**:①②③ 合计不到 10 行;**④「代理层含 WS」是新功能**(且要穿 nginx → orchestrator 两跳)。构建链重:`build` = lib+worker+gateway+render+viewer **五个产物**,150+ devDependencies(vite 8 / ts 7 / tailwind 4 / insiders 快照依赖)
- **已启动**:`pnpm install --frozen-lockfile`(`prepare` 会自动触发 build)→ **这是决定改造成立与否的最后 gate**(构建不出来 = 改造不成立)。本机环境:node v22.22.2 ✓ / pnpm 11.21.0(源要求 11.23.0)/ D 盘余 1.2 T ✓
- **待定(要用户拍板)**:若继续,须先定「**fork 放哪 / 谁维护 / 上游更新怎么合**」——这是长期承诺
## 22:1x 用户说「开整」→ **构建验证通过,改造已开工**
- **✅ 构建 gate 通过(决定性)**:`pnpm install --frozen-lockfile` 成功,**`prepare` 自动跑完 `build`**(5 个产物)——`✓ built in 10.16s`、`built ...\artifacts\viewer`、`Done in 14m 45.1s`。只有 `INEFFECTIVE_DYNAMIC_IMPORT` 良性警告(i18n locale chunk 优化提示)。**⇒ 改造成立**
- 产物:`artifacts/{gateway.cjs 57M, unit-content-worker.mjs 30M, viewer/, render-machine/}` + `lib/{index.js 5M, client.js 1.1M}`
- **我的改动已验证编译进产物**:`gateway.cjs` 里 `UNIVER_COLLAB_GATEWAY_SOCKET` ×1、`socketPath` ×12
- 增量构建快(rollup 10 秒级),首跑 14m45s 主要在装依赖
- **已改 3 个文件(gateway 侧监听层,均向后兼容)**:`src/gateway-app/server.ts`(`LOOPBACK_HOST` 常量 → 新增 `socketPath`/`host` 选项,`listen()` 支持 unix socket)、`main.ts`、`gateway-entry.ts`(真入口,读 `UNIVER_COLLAB_GATEWAY_SOCKET`)
- **★ 关键发现①(简化方案的钥匙)**:gateway 全部 API 在 **`/uf/<base64url(univerfile)>`** 前缀下(`main.ts` 自带端点清单),WS 是 `/uf/<enc>/universer-api/comb/connect`;**Viewer 加载后按绝对路径请求 `/uf/*`** ⇒ 代理规则两条即可:`/uf/**` + `/univer-api/viewer/**`
- **★ 关键发现②(坑)**:**Node 内置 `fetch` 不支持 unix socket**(无 `socketPath`)⇒ 健康检查 `protocol.ts:4` 与 `GatewayClient` 都要改。**但改造面比预想小**:host 侧 fetch 仅 **5 处**,且**已有集中封装 `src/host/adapters/gateway/client.ts`(89 行,只用 `response.json()/ok/status` 三个能力)** ⇒ 在 `GatewayClient` 内加一条 unix 分支(`http.request({socketPath,…})` 包最小 Response-like)即可覆盖大部分
- **剩余改动清单**:④ `launcher.ts` 传 socket env | ⑤ `gateway-process.ts:29` origin 构造 | ⑥ `protocol.ts:2,20` 健康检查 | ⑦ `gateway-univer-service.ts:538` viewerUrl → 相对路径 | ⑧ `adapters/gateway/client.ts` 加 unix 分支 | ⑨ `host/webServer/router.ts` **新增代理层**(`/uf/**` + viewer)
- 代码在 `D:\github\dsh-univer-office-src`(**未打包、未碰生产**)
## 22:2x 改造阶段 1 代码完成(①~⑨),构建通过
- **新增 4 个模块**:`shared/gateway-socket.ts`(socket 路径解析 + **`unix:<path>` 端点标识** `unixOrigin`/`parseUnixOrigin`)、`shared/unix-http.ts`(**HTTP over unix socket 最小客户端**,只覆盖 ok/status/text,**零新依赖**)、`shared/viewer-paths.ts`(浏览器侧路径常量,避免 service→webServer 依赖)、`host/webServer/viewer-proxy.ts`(**流式反向代理**,去掉 hop-by-hop 头)
- **改动**:launcher(传 `UNIVER_COLLAB_GATEWAY_SOCKET`)|gateway-process(端点按传输打标)|protocol(健康检查 + probeGateway 支持 socket)|**adapters/gateway/client(加 unix 分支,一处覆盖绝大部分调用)**|router(viewer 代理分支)|plugin(`/uf` 注册)|gateway-univer-service(**viewerUrl 与 base 均改相对路径**)
- **★ 关键设计决策(易踩)**:dsh 的 webServer 是**前缀匹配但无「最长优先」** ⇒ `/univer-api` 会吃掉 `/univer-api/viewer/...` ⇒ **viewer 代理必须放进现有 router 的 dispatch 内**,单独注册会被 404 截胡;`/uf` 与之不冲突,可独立注册
- **★ 踩坑**:只 grep `viewerUrl:` 会漏 —— 另有 `const base = ${gateway}/?file=` 派生 `openUrl`/`worktreeUrl`/`mergeUrl`(**全是交给浏览器的 URL**)。改完产物内旧拼接残留 = **0**
- **构建**:`✓ built in 13.69s` rc=0;产物含 `createGatewayViewerProxy`×3、`parseUnixOrigin`×4、`VIEWER_PAGE_PATH`×3、`UNIVER_DSH_GATEWAY_SOCKET`×1
- **剩余**:① 3 处散点 fetch(`render-source-operations.ts:147`、`resource-operations.ts:86`、worker 侧 1 处)socket 模式下也要改 ② **WS 代理(阶段 2)**:dsh `webServer.registerUpgrade` 是**精确路径**匹配,而 gateway 的 WS 在 `/uf/<enc>/univeriser-api/comb/connect`(含动态段)⇒ **需按文件动态注册**(`ctx.effect` 可增删)③ 自带 `test:host`/`test:integration` 冒烟 ④ 打包 → 候选池 → guest 启用 → 端到端验收
## 22:4x ★ socket 传输在**真实 Linux** 验证通过(改造核心假设成立)
- **方法(不碰生产)**:构建产物(`lib` + `artifacts` + `test`,36 MB 压缩)传到服务器 `/tmp/uv-verify`,用 **guest 实例已装的 node_modules 做只读符号链接**补原生依赖(`libsql` 等)
- **结果(unix socket 模式)**:`[dsh-univer-gateway] listening on unix:/tmp/uv-verify/gw.sock`;`GET /` → **200**,且 `<title>Univer</title>`=true、`<div id="app"></div>`=true(**这正是原先失败的健康检查判据,在 socket 上成立**);`GET /uf/AAAA` → 400(路由可达)。**TCP 对照同样通过**
- ⚠️ **Windows 限制(本机测不了 socket 的原因)**:本机 **AF_UNIX 返回 EACCES** —— 用**纯 `node:net`** 也复现 ⇒ **平台限制,与改动无关**;`UNIVER_DSH_GATEWAY_SOCKET` 链路本身通(gateway 收到路径并尝试监听)
- **TCP 回归**:`test/integration-smoke.mjs` 在 Win 与 Linux **均 OK**(18 项:new/status/Unit/import/export/**screenshot**/print-pdf/resources/worktree lifecycle…)⇒ **改动未破坏原功能**
- **清理**:服务器 `/tmp/uv-verify`、本地临时文件已删;**生产完好**(guest 实例 `active`、依赖正常)
- **⏳ 待拍板(R8)**:端到端验证必须**部署到 guest 实例**(启停 = 重启实例、断在线)—— 这是唯一需要用户点头的一步
- **仍待做**:① WS 代理(阶段 2,按文件动态注册 `registerUpgrade`)② 边缘功能 socket 支持(截图资产、worker snapshot)
## 23:1x 改造**全部完成**(代码 + 验证 + 打包),仅剩部署验收待拍板
- **WS 代理(阶段 2)完成**:`createGatewayUpgradeProxy`(转发 upgrade:重建 101 响应头 + 双向 pipe + 失败降级)|`viewerSocketPaths(fileKey)`(两条:`/uf/<enc>/universer-api/comb/connect`、`/uf/<enc>/events`)
- **注册机制**:dsh `registerUpgrade` 是**精确路径**且 gateway 的 WS 路径含动态段 ⇒ **按文件懒注册**(router `/univer-api/state` → `onViewerFileOpened` 回调 → plugin 内 Map 去重 + `ctx.effect` 统一 dispose)
- **统一传输入口** `shared/gateway-request.ts`:`requestGateway(endpoint, path, init)` 按 endpoint 前缀选 socket/TCP ⇒ `client.ts` 与 `render-source-operations.ts`(截图资产)均改用它,**消除重复分支**;`unix-http.ts` 扩展 `headers.get`/`json()`/`arrayBuffer()`/`signal`
- **未做(已标注为已知限制)**:worker 侧(`unit-content-worker`,协同/快照类)仍走 TCP —— 其内部 12 个 URL 用标准 `fetch` 拼接,改造面大且属边缘;**导出 xlsx 走 Host 的 `GatewayClient`,不受影响**
- **验证全绿**:构建 rc=0 | 回归 `integration-smoke` **OK**(18 项功能)| **Linux socket 复验通过**(`GET /`→200 且 Univer 标记 true;TCP 对照同)| 版本升 **0.2.15**、`pnpm pack` 产物 `/d/tmp/dsh-univer-office-0.2.15.tgz`(41 MB)
- **清理**:服务器 `/tmp/uv-verify` 等 + 本地临时文件已删;**生产完好**(guest 依赖正常)
- **⏳ 唯一待拍板**:**部署到 guest 实例做端到端验收**(R8:启停 = 重启实例、断在线)
## 23:2x 用户「确认执行」→ **已部署到 guest,但端到端仍差一步**
- **✅ 投放成功**:`POST /api/plugins/business`(admin,base64 传 tgz)→ `version 0.2.15`、`replaced:true`、**`compat.level=ok`(223 平台包全匹配)**、**无 P0 命中**(`trustedOverride:false`);仅 `network-egress` 警告(来自 README/assets 里的 URL 字符串,非阻断)
- **⚠️ 发现平台缺陷(重要)**:guest 侧「禁用→启用」**失败**(`安装失败,请重试或联系管理员`)。根因:**`business-plugins.ts` 的 `pnpm add` 缺 `-w`** ⇒ `ERR_PNPM_ADDING_TO_ROOT`(profile 内有 `pnpm-workspace.yaml`,必须显式 `--workspace-root`)。且该路由 `runPnpmAs` 用 `stdio:'pipe'` **吞掉 pnpm 的 stderr** ⇒ journal 里查不到真实原因。**手动 `pnpm add -w` 成功**(5.5s,装到 0.2.15,属主正确 100002)
- **✅ 平台侧配置完成**:`orchestrator.ts` 的 baseEnv 增加透传(`DSH_INSTANCE_UNIVER_SOCKET` → 实例内 `UNIVER_DSH_GATEWAY_SOCKET`,**不设则保持原 TCP 行为**);`npm run build` 通过;`/etc/dshs.env` 加 `DSH_INSTANCE_UNIVER_SOCKET=auto`;重启服务后 orchestrator 已读到;**实例 env 实测注入成功**(`UNIVER_DSH_GATEWAY_SOCKET=auto`)
- **✅ 组件级验证全过**:实例内 `os.tmpdir()=/tmp` → socket 路径 `/tmp/dsh-univer-gateway-<pid>.sock`(**103 字节,未超 108 上限**);**在实例命名空间内(`nsenter`)用插件真实产物跑 gateway → `listening on unix:/tmp/…sock`,socket 文件生成** ⇒ **改造在实例内确实有效**
- **⚠️ 端到端仍未通过**:用户浏览器 **23:24:10 请求 `/univer-api/state` → 23:24:18 返回 400(8.2s)**;实例当前**无 gateway 子进程**、`/tmp` 下**无 .sock**。400(非 500)指向**校验类错误码**(`INVALID_*` / `SESSION_SCOPE_*`),8.2s 说明确曾尝试启动
- **⏳ 下一步**:请用户在实例里**再触发一次 univer 操作**(23:24 那次实例刚重建 ~23:24:04,插件加载可能未就绪);若仍 400,开 `UNIVER_DSH_GATEWAY_DEBUG=1` 取 gateway stderr
- **另注**:同期间发现 **admin 实例 crash-loop**(`dsh-plugin-mcn-suite` 缺 `xlsx` 依赖,`crash-loop-circuit-open`)—— **与本次改造无关**,属 T03 遗留问题
## 20:4x T03 推进到「上传前一刻」—— **按纪律停手**(线上有别的会话在动)
- **窗口条件已满足**:guest 实例 `systemctl is-active dsh-100002-299e7f95.scope` = **inactive**(已停)+ 最后活动 20:06(41 分钟前)⇒ **零中断窗口** ✓
- **T03 执行路径已完全摸清**(本轮收获,下次直接可用):
· **上传** = `POST /api/plugins/business`(admin),body = `{filename, file:<base64 tgz>, trust?:{confirmed,reason}}`;cookie 名 **`sid`**;端口 **3080**;`UPLOAD_BODY_LIMIT = 180 MB` ✓(1.9 MB 包绰绰有余);P0 命中且无 trust → 409 fail-closed。
· **启用** = `POST /api/plugins/mine/apply`(**用户**自己发)⇒ 按刚固化的规则「用户在实例里自己启用」,**这一步不该我代劳**。
· **摘 modlens** = 改 guest `profile/web/package.json` 的 bundles 列表(零中断,因实例已停)。
- **⛔ 停手原因**:`bash scripts/op-lock.sh status` 显示 **🔴 `instance-mem-tune`**(占用者 `mem-opt-2046`,20:47 起)→ 线上有会话正在动生产 ⇒ 按纪律停手;已 `release t03-upload-pool` 撤掉我刚占的位。
- **⚠️ 发现 op-lock 的设计缺陷**:它**按「操作名」占位**(锁根下一个操作一个子目录)⇒ 两个会话占**不同名**的操作**都会成功** ⇒ **它拦不住「并行动生产」**(不像全局执行锁那样真正互斥)。本次即「占位成功但线上已有人」—— 只能靠人主动看 `status`,机制本身不设防。建议改为「单锁位 + 占用者字段」,或 `claim` 时发现已有任意占位即默认拒绝(`--force` 才可覆盖)。**待下次改文档时补进 `交接单/README §六`。**
## 21:0x T03 上传候选池 —— **被 T05 兼容性预检拦住(HTTP 409 `compat_incompatible`)**
- 条件全满足(op-lock 空、guest `inactive`、59 分钟无活动)→ 占 op-lock → scp tgz(md5 `f98a3303…` 与单子一致 ✓)→ `mksess.cjs` 造 admin 会话 → curl `POST /api/plugins/business`。
- **判据(服务端回显)**:`{"kind":"dep-range","pkg":"@deepseek-ai/dsh-client-ui-sidebar","detail":"平台为 0.1.2-rc.1,插件要求 ^0.1.0-rc.5(不满足)"}`。
- **原因(semver 的 prerelease 规则,不是误报)**:带 prerelease 的版本只能被「**同 major.minor.patch 且带 prerelease**」的比较器匹配 → `0.1.2-rc.1` 的 tuple (0,1,2) 在 `^0.1.0-rc.5`(= `>=0.1.0-rc.5 <0.2.0`)内**没有对应比较器** → 判不满足。**插件声明过严**(作者意图「≥0.1.0-rc.5」,但 `^` 写在 prerelease 上不等价)。
- **根因(时间差)**:**T05 兼容性预检 16:35 才上线**,而 T03 的包是 **11:15 打的** → T03 单子没预料到这道新关卡。
- **副作用 = 零**:候选池**未变**(池内仍只 `dsh-univer-office @0.2.14`);临时 admin session **已删**(`deleted sessions=1` ✓);服务器与本机临时文件已清;op-lock **已释放**(无人占用 ✓)。
- **待定处置**:(a) 改插件 `package.json` 依赖声明放宽到能匹配 `0.1.2-rc.1` → 重打包 → 重传(**推荐**,不掩盖问题);(b) 人工确认实际兼容后上传带 `trust:{confirmed:true,reason}` 放行(快,但掩盖声明问题)。**下一步:先只读验证「插件实际用了 sidebar 的哪些 API / 平台 0.1.2-rc.1 是否提供」再定。**
- 经验:`op-lock.sh release` **必须带同一个 `ME=`**,否则被按 R9 拒绝(本次踩 1 次,属**正确**保护)。
## 22:2x T03 **安装失败 → 根因定位 + 修复 + 重传完成**(用户报障「安装失败」)
- **现象**:用户在「功能管理」启用 mcn-suite → 失败。取证:apply 请求 200 → 实例被拉起(22:12:57 `dsh web: http://…`)→ 但 `node_modules` 无包、bundles 无、日志无 mcn 痕迹、profile mtime 被改 ⇒ **平台 install 阶段失败并回滚**。
- **复现**(mksess + POST apply + 轮询 task):`#failed | 失败 ERR: 安装失败,请重试或联系管理员` —— **阶段停在「正在安装 1 个插件…」,不是探活阶段**;⚠️ **平台把真实错误泛化吞掉了**(违反 A10「失败面要留证据」)。
- **★真实根因**(手动跑平台同姿势 pnpm 才逼出来):
`ERR_PNPM_NO_MATCHING_VERSION No matching version found for @deepseek-ai/dsh-client-runtime@>=0.1.2-rc.1 <0.2.0-0`
⇒ **这些 `@deepseek-ai/dsh-client-*` 不在公开 npm registry 上**(平台用的是本地那份 0.1.2-rc.1),而 **pnpm 会去 registry 解析 peer 依赖** → 解析不到 → 装不上。**这与「预检拒收」是两层不同问题**:改范围只过了预检,装的时候照样卡。
- **修法**:`package.json` 增加 **`peerDependenciesMeta`:4 个 peer 全标 `optional: true`**(准确表达「由平台提供、不走 npm」);版本 **0.3.1 → 0.3.2**;`npm pack` 重打包(1,951,946 B)。
- **验证**:以 admin 身份手动 `pnpm add` 新包 → ✅ **成功**(`+ dsh-plugin-mcn-suite 0.3.2`,2.9s)→ 随后清理残留(`pnpm remove` 报 CANNOT_REMOVE_MISSING(package.json 已恢复、无该依赖)→ `rm -rf` 兜底)→ bundles 回到平台 5 个 ✓
- **上传**:HTTP 200、**`replaced: true`**、`compat.level=ok`;候选池 = `dsh-plugin-mcn-suite @0.3.2` + `dsh-univer-office @0.2.14`。
- **现场**:服务器与本机诊断脚本/临时包全部清理;admin profile 已复核一致(node_modules 无残留、deps 正常);op-lock 已释放。
- **⚠️ 建议修平台缺陷**:apply 失败时应回显 pnpm 的真实 stderr(现在吞成"安装失败,请重试或联系管理员",导致必须手动复现才能定位)。
- **待办**:请用户再试一次启用(0.3.2 已就绪)。另:用户新提「功能管理列表改卡片、一行 2 个」→ 新需求,待做。
## 22:3x 「功能管理」插件列表 → **卡片形式(一行 2 个)v0.2.6**(用户要求「一起改了吧」)
- **改动**(`poc/business-plugins/lib/client.js`,4 处纯渲染层):① 新增**内层网格容器** `display:grid; gridTemplateColumns:repeat(2, minmax(0,1fr)); gap:10px`(**单独包一层**,不直接改外层 `wrap` —— 它同时装标题/搜索/按钮/提示等纵向块;插件项收集进 `cards[]` 再统一 push)② 每项由「行」改「卡片」:`padding 12px 14px`、`borderRadius 12px`、`background: var(--dsw-alias-bg-layer-1,#fff)`、**常显 1px 边框**(原非错误项为 transparent)③ hover 改按规范 §4.5:`border-color: brand-primary + box-shadow 0 4px 12px rgba(47,111,237,.15)`(原为换背景色)④ 卡片内 `alignItems: flex-start`(复选框/徽章顶部对齐,长说明不撑歪行高)。
- **规范依据**:06-工作台UI规范 §4.5(卡片 r12 + hover 主色扩散影)、§2.4、§2.3;颜色仍走 `--dsw-*`(section 嵌在 dsh 面板内)。
- **验证**:`node --check` 语法 OK(556 行);版本 0.2.5 → **0.2.6**;`npm pack` → `dsh-local-business-plugins-0.2.6.tgz`(产物名带 `dsh-local-` 前缀,**部署脚本要的是 `business-plugins-*.tgz`** → scp 时改名)。
- **部署**:`/opt/dsh/artifacts/business-plugins-0.2.6.tgz` → `node scripts/ensure-biz-plugins.cjs --all`(**未加 `--restart`**,不主动打断在线实例)→ admin/guest 均确认 **0.2.6**、包内含改动 ✓;op-lock 已释放。
- **生效条件**:client bundle 在**实例启动时**加载 ⇒ **用户需重启自己的实例**才看得到。
- **未做**:浏览器渲染验证(本机无 Chromium)→ 待用户目视验收。
- 落档:档案 67 追加「v0.2.6 卡片化」小节。