Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下13.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

416 lines
48 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(第 14 片)
> ⚠️ **本目录日志已按【月】分片**(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-下12.md` **下一片**:`2026-09-下14.md`
---
## 10:12–10:25 会话 `a2665dd3`:**独立复核「另一会话的内存结论」(用户要求确认)**
**被评为"确认"的三条 → 复核结果**
| 对方结论 | 我的实测 | 判定 |
|---|---|---|
| exitCode **8×134 + 1×137**(137 = cgroup SIGKILL) | `--since today` 统计 = **8×134 + 1×137**;137 时刻 **10:03:11** | ✅ **逐条一致** |
| guest cgroup **384/384/384**(高位 100%) | `usage=368.2 / peak=384.0 / limit=384` | ✅ 一致 |
| gateway ≈390.4 MB |实例本体 ≈44.0 MB | **331.5 MB**(gateway pid 429466)|**91.6 MB**(dsh pid 427842) | ⚠️ 同量级,**数值有偏**(gateway 热身前/后波动 330→390;本体随加载阶段 44→92) |
| 「是内存,不是恢复逻辑」 | 与其一致:OOM 是真因;恢复循环(`GET /`)是**结果**不是原因 | ✅ 成立 |
| 「502 是 CF 的,nginx 里没有 502」 | ❌ **不成立**:`/www/wwwlogs/alotbuy.com.log` 里状态位 502 共 **579** 条(`/api/remote.mux` 362 · `/univer-api/state` 39 · `GET /` 29)⇒ **nginx/源站自己也返 502**(上游 127.0.0.1:3080 失败)。CF 页面是同一失败在 edge 的另一层呈现 | ❌ **需修正** |
**★ 对方漏掉的关键一环(我已于 10:10 修掉)**:**陈旧 socket**。内存杀掉 gateway 后,残留的 socket 文件让**之后每次启动都失败**(`bind()` EADDRINUSE)→ 对外只报 `did not become ready within 10000ms`。
⇒ 「无法启动」是**两个原因叠加**:**内存把它杀掉** + **陈旧 socket 让它再也起不来**。**0.2.17 已修(绑前 unlink)并在 guest 上验证通过** ⇒ **方案 (a) 必须带上这个修复**,否则升配额后仍会偶发"永久无法启动"。
**★ 方案 (a) 的宿主容量复核(关键修正)**
- 实测宿主:`total 1870 MB`|`used 1062`|**`available 808 MB`**|**`swap 已用 679 / 1024 MB`**(说明**已在换页**)。
- 对方给的 `640 × maxIdleInstances 2` = **1280 MiB** ⇒ **放不下**(808 可用 + 345 空闲 swap ≈ 1153 < 1280)✗。
- ⇒ 若走 (a):**必须把常驻/激活实例数压到 1**(`maxIdleInstances 4 → 1`),或做**分级配额**(univer 用户 640 / 其他 256–384;但配额在 `orchestrator.ts:625` 硬编码,分级=改码)。**长期仍应扩宿主内存**(花钱)。
**一句话**:对方的定因(内存)与"恢复逻辑没坏"成立;但(i)"nginx 无 502"错,(ii)漏了陈旧 socket 这一环(已修),(iii)(a) 的容量账要按 `available 808 + swap 679` 重算 ⇒ 640 只能配 1 个常驻实例。
---
## 10:14–10:2x · 开源项目命名定稿:**DSH 多租户平台 / DSH Multi-Tenant Platform / `dsh-multitenant`**
**用户指令**:「名字就叫 dsh 多租户平台 对应的英文,项目中文名称「太妙」(认可)」⇒ 本会话中间候选 `dsh-hosting` 被取代。
**命名口径(已定稿,写进 README「名称与渊源」+ NOTICE §4 + 台账 §七)**
| 项 | 值 |
|---|---|
| 中文名 | **DSH 多租户平台** |
| English name | **DSH Multi-Tenant Platform** |
| 技术标识 | **`dsh-multitenant`** | env 前缀 `DSH_MULTITENANT_*`(36 种)| 数据根 `/var/lib/dsh-multitenant` | `dsh-multitenant.{service,env}` | 库文件 `dsh-multitenant.db` |
**占用实测**(R-O8 要求):npm `registry.npmjs.org/dsh-multitenant` → 404;`dshbase.com/plugins/dsh-multitenant` → 404 ✓ 可用。
**执行**:改脚本 GLOBAL 的**目标值**(`dsh-hosting`→`dsh-multitenant`、`DSH_HOSTING_`→`DSH_MULTITENANT_`、路径与库名同改)+ `DST` 目录名 → 改 `_verify_tsc.mjs` 的 `EX` → `--force` 重建 → 手工层批量替换(README 42 处 / LICENSE 1 / NOTICE 4 / install.sh 22)→ README 标题改「**DSH 多租户平台 · DSH Multi-Tenant Platform**」+「名称与渊源」加「中文名 / English name / 技术标识 / 命名口径」→ 再重建验证。
**新坑(已写进技能 §8 坑 14)**:**废弃的旧候选名必须进阻断性探针** —— 否则 `_overlay` 里的中间名会**静默残留**(本次正是靠把 `dsh-hosting` / `DSH_HOSTING_` / `/var/lib/dsh-hosting` 加进 `LEAK_PROBES`,才发现 **72 处**残留)。
**验证(全绿)**:阻断探针 **0 命中**(含全部曾用名)| 旧名残留 grep 为空 | `tsc --noEmit` **exit 0** | `bash -n install.sh` OK 且 `--help` 显示新名 | 140 文件 | env 前缀 36 种一致。
**技能 v1.0.3**(R-O8 补命名口径与已排除候选;§8 补坑 14/15;全文路径改 `dsh-multitenant`)→ 归档副本已同步(md5 `631ad317…`),四件套 exit 0,锁已释放。
> ⚠️ 小插曲:本轮有一次 `--claim-exec` 因**命令被重试**而报「抢不到锁」—— 只读确认 OWNER 是**我自己的会话名**(10:17 那次声明),未做任何接管,随后正常释放,现**无锁**。R9 未被触碰。
## 10:14–10:40 会话 `a2665dd3`:**gateway 内存构成实测 + 「加堆限」方案被实测证伪(已回滚)**
**用户问**:为什么 gateway 要这么多内存?有办法优化改造吗?
### 一、构成实测(生产,pid 429466)
| 项 | 值 |
|---|---|
| `VmRSS` | **335 MB**(多次采样区间 **335–400**,噪声 ±30) |
| 其中 `[anon]`(V8 堆 + 匿名映射) | **293 MB = 87%** |
| node 代码段 | libsql native | 41 MB | 1 MB |
| `VmSwap` | 46–93 MB(**已在换页**) |
| gateway 环境里的 `NODE_OPTIONS` | **不存在** ⇒ **没有堆上限**(不继承实例的 160) |
| 产物 | `artifacts/gateway.cjs` **单文件 59 MB**(esbuild `bundle:true, packages:'bundle'`;只 external node builtins + libsql + 3 个 native binding) |
**根因**:它不是插件而是**整套 Univer 协同服务端**(`@univerjs-pro/collaboration-service` 等闭源 SDK + 多 unit 类型 + 协同运行时 + 视图资产服务 + sqlite 文件存储),**全部内联**、`startServer()` 时一次性加载 ⇒ **常驻内存是真实需求**。
### 二、★方案 A(给 gateway 加 V8 堆限)**实测无效,已回滚**
- 试了 `NODE_OPTIONS=--max-old-space-size=160 --max-semi-space-size=8`(改 `gateway/launcher.ts`,0.2.18):
**RSS 335 → 400 MB、swap 93 → 46 MB,总量持平**(cgroup 368→369)⇒ **不是 GC 懒惰,是真需求**;且加限还引入 `134 abort` 风险。
- ⇒ **已回滚到 0.2.17**(`launcher 含堆限: 0 处`)并复测(RSS 363–386 / swap 58)✓。**教训:这类"调参省钱"的假设必须先 A/B 实测再留档**(本次幸好测了)。
### 三、其余方案的可行性摸底
- **B(裁 unit 类型 / 只留 sheet)**:❌ **基本不可行** —— 加载面由**闭源第三方 SDK** `@univerjs-pro/collaboration-service` 决定(我们自己的 `collab-service.ts` 只引用 `snapshot?.board?.name` 之类),没有现成 preset 开关;要裁只能篡改 bundle(高风险)。
- **C(代码分割)**:收益**不确定** —— 若 SDK 在启动时就实例化所有 unit 类型,切分无益;需先测(可做,但要 A/B)。
- **D(把 gateway 移出实例 cgroup,独立 systemd 预算)**:⭐ **现在看最优** —— 不动闭源 SDK、不动插件行为、实例的 384 不再被挤,且"探针还能继续测实例本体";代价是隔离/生命周期模型变化(要评估 R5:仍以该 uid 运行,但生命周期不再随实例)。
- **A 已证伪;换 libsql(1 MB)与多用户共享 gateway(权限面扩大)不值得。**
---
## 10:24–10:3x · 开源 README:**出处改为「文末提一句」**(用户纠正措辞与位置)
**用户指令**:「在最后提一下引用了谁就行 不要上来就重点讲用了谁,感觉是在宣传别人的项目,已经很多地方深度改造过了」
**改动(只动 README,不动法律文件)**
1. **删掉**开头的 `## 名称与渊源` 章节(原在「目录」之后、「特性速览」之前)+ 目录项 —— 那段把上游摆在了正文最前。
2. **正文改以本项目为主语**:
- `### v1.0.0 相对上游 \`dshs\` 做了什么` → **`### v1.0.0 的六组改造`**;
- 删掉「上游提供了**骨架**:…… v1.0.0 把「能跑」补齐为……」,改为「本项目把「能跑」补齐为「能对外长期运维」,改动分六组:」;
- 版本表 v0.x 行去掉「基于上游 `dshs` 的」。
3. **出处只在文末一句**(「第三方组件与致谢」):「起步阶段参考了 dshs(上游作者(已按要求不再具名),MIT)……此后在本仓库做了**大量深度改造**……**感谢作者把它开源出来。**」——去掉了「离开它就没有这个项目」「只是接着往前走了一步」这类过度抬举。
4. **授权章节**:第一层作用域措辞改「起步时参考的骨架代码」;删掉「也是诚实的做法/可仅使用上游部分自行扩展」(把读者往上游引)。
5. **法律部分一分不减**:`LICENSE-UPSTREAM-MIT.txt` 逐字节未动;`LICENSE` 第一层说明、`THIRD-PARTY-NOTICES.md` §4 保持完整 —— 「轻描淡写」只作用于 README 正文。
**验证**:README 里 `上游/骨架/dshs/上游作者(已按要求不再具名)` 只剩**文末 3 处**(授权表 1 + 致谢 1 + 授权摘要 1);`nginx 上游` 是无关技术词;阻断探针 **0 命中**。
**技能升到 v1.0.4**:新增 **R-O8 附则「出处的措辞与位置」**(位置 / 主语 / 措辞 / 边界 / 代码注释 五条)+ **§8 坑 16**(`--claim-exec` 与其它步骤写在同一命令里,命令被重试会出现「抢不到锁」**假告警** —— 先只读看 OWNER 是不是自己,**放锁前必须再确认 OWNER**,否则可能放掉别人的锁 = R9 反向违反)。归档副本已同步(md5 `8b95d10d…`),四件套 exit 0,锁已释放。
## 10:27–10:45 会话 `a2665dd3`:**D 方案与「共享 gateway」的多租户评估**
**两个新查到的事实(决定结论)**
1. **gateway 没有「空闲自停」**:`gateway-process.ts` 只有 `stop()`(实例停机时调用),**没有 idle 超时** ⇒ 一旦启动就常驻,直到实例停。
2. **应用层路径白名单没启用**:代码里有 `allowedRoot`(`UNIVER_ALLOWED_ROOT` → `server.ts` → `univerfile-manager` 校验「非 `.univer` / 越出 allowedRoot → 400」),但 **launcher 不注入该 env**(实测 gateway environ 只有 `UNIVER_COLLAB_GATEWAY_PORT` / `_SOCKET` / `UNIVER_VIEW_ASSETS_ROOT`)⇒ **生产上没有 allow-list**,隔离**完全依赖 uid + bwrap**。
**D(把 gateway 移出实例 cgroup)的多租户评估**:D **不是共享** —— 仍是"每用户一个 gateway、以该用户 uid 跑",只改 cgroup/生命周期归属。
| 维度 | 现在(在实例内) | D |
|---|---|---|
| uid | 100002(该用户) | 不变 ✓ |
| **文件系统视野** | **bwrap 合成根**:私有 `/tmp`、`/etc` 是 tmpfs、`/usr`·`/lib64` 只读、仅 bind 该用户目录 | **宿主完整 fs**(/etc、/opt、**其它用户的 `users/<uid>`**)✗ |
| 应用层校验 | **未启用**(见上) | 同样未启用 ⇒ 只剩 uid 权限兜底 ✗ |
| 资源 | 与实例共享 384(互相挤) | 各自独立预算 ✓(= D 的收益) |
| 生命周期/记账 | 随 scope 起停、天然记账 | 要自理(停实例后它还活着?陈旧残留?)+新记账 |
⇒ **判定**:D 技术上能保住 uid 隔离,但**牺牲命名空间/文件系统隔离** = **隔离降级**(R5"扩大可见面")⇒ 要走必须同时补:① 注入 `UNIVER_ALLOWED_ROOT=<该用户 ws>` 启用 allow-list ② 独立 unit 带同样 cgroup 预算 + 硬化 ③ 出权限影响评估并由用户拍板。
**「不同用户共用一个 gateway」= ❌ 不可行**(四条硬理由):① 它是**有状态数据平面**(unit/快照/协同会话在内存)⇒ 共享即数据混在一起;② 路径校验未启用 ⇒ 只能靠闭源 SDK 的应用层鉴权,**无法审计**(供应链 + 越权,R5 扩大);③ 单个用户的大表格能把共享 gateway 压爆 ⇒ **全租户受影响**;④ 无法按租户归因资源。唯一成立的同尺度 = **同一用户自己的多会话共用一个**(现状)。
**★ 由此产生更优的小方案 E(我现在的推荐)**:**给 gateway 加「空闲自停」**(如 10 min 无请求 → 自行退出;下次用再起,0.2.17 已保证能重起)。
- 收益:**不测 univer 时零占用**(探针变"按需施压",日常不再挤实例);**不动隔离、不动闭源 SDK**、改动小(host 侧定时器)。
- 代价:首次打开多 ~5–10 s 启动(当前就绪超时 10 s 需同步调大)。
- ⇒ 把"结构性冲突"降级为"按需成本",且**不碰多租户模型**。
## 10:30 追加:**方案 E 的实现落点(已摸清代码结构)与适用范围**
**代码结构(已读)**
- 生命周期所有者 = `src/host/processes/gateway/supervisor.ts` 的 `GatewaySupervisor`:`status()` / `ensure()` / `start()` / `dispose()`,已有 `gatewayStartupTimeoutMs`(=10 s,即报错里那个 10000ms)、`gatewayRequestTimeoutMs`、`gatewayPort`;`GatewayProcess.stop()` 已存在。
- **关键:`ensure()`/`status()` 每次都先 `gatewayIsHealthy()` 探活**,探不通就重起 ⇒ **gateway 自己退出后,host 会自动重新拉起**(无需 host 侧记账)✓
**E 的正确实现位置 = gateway 侧自停**(不是 host 侧定时器)
- 原因:**worker 与浏览器是直接走 unix socket 的**,host 看不到这部分流量 ⇒ 用 host 侧"最近调用时间"判空闲会**把忙误判成闲**(可能把正在编辑的 gateway 停掉)✗
- 做法:`src/gateway-app/server.ts` 记录「最后一次 HTTP 请求 + 活跃 WS 连接数」(`transport/http.ts` 的 listener 包一层 touch;`transport/ws.ts` 计数连接);定时检查,**无请求 ∧ 无活跃 WS ∧ 超过 `UNIVER_DSH_GATEWAY_IDLE_MS`(默认 10 min,0=关闭)→ `process.exit(0)`**。
- 无需在退出前 unlink socket(0.2.17 已保证下次绑得上)✓
- 配套:`gatewayStartupTimeoutMs` 10 s → **20 s**(冷启动 5–10 s,10 s 太紧)。
**★ E 的边界(必须说清)**:E 只消除**空闲期**占用;**用起来时仍需要 ~480 MB**(实例 90 + gateway 390)⇒ **384 配额下"用的时候照样装不下"**。所以:
- 只做 E ⇒ 探针从"常驻挤压"变"按需施压"(更可控,但用的时候仍会 OOM —— 这恰是探针要观察的);
- **E + (a) 提配额(640/256,常驻数 ≤1)** ⇒ 平时不碍事 + 用起来也不死 = 完整体验。
---
## 10:28–10:4x · 开源 README:**开头改为「亮点」**(用户指出「基线 ≠ 亮点」)
**用户指令**:「部署到一台服务器,多个用户注册并经管理员审核,各自获得一套相互隔离 —— 这个只是项目的基础不是重点,**重点是多租户各自进程的安全隔离、状态恢复、插件和技能的管理**。多看看项目找出这个项目的亮点」「对了多想想 **不要弄了半天亮点都体现不出来**」。
**做法:先读码再写亮点**(不凭印象)
- 按体量排序找最重的模块(`proxy.ts` 1339 · `orchestrator.ts` 1062 · `business-plugins.ts` 742 · `skills.ts` 686 · `k8s-spawner.ts` 687 · `pg.ts` 508 · `plugin-compat.ts` 337 · `leader.ts` 254 …),逐个读头注与关键函数;
- 又读了 `isolation.ts` / `firewall.ts`(端口守卫)/ `fs-guard.ts`(路径围栏)/ `crash-policy.ts` / `reconcile.ts` / `session-preset.ts` / `security-scan.ts` / `spawn.ts` / `patch.ts` / `nginx/generate.ts` / `file-service.ts`;
- 关键数值**逐一回代码核对**(心跳 25000ms、`CrashBreakerOpenError`+`retryAfterMs`、目录 3408 条/覆盖 1854、技能 rank 600/400、扫描 P0/P1 与 8MB)。
**README 改动**
1. 开头删掉「部署到一台服务器,多个用户注册…」(= 基线),换成 **一句定位 + 三条主线速览**(隔离 / 恢复 / 治理)+ 一句「注册审核、独立实例、域名访问是**基线**不是亮点」。
2. `## 特性速览`(6 条形容词式 bullet)→ **`## 亮点`(四大节表格,每条带机制 + 反直觉细节 + 为什么这么设计)**:
- **① 进程级安全隔离**:确定性 uid(`baseUid + row_id`)+ `setpriv` ⇒ `0700` 是**内核边界**;**端口守卫必须在 OUTPUT 链按客户端 uid 匹配且 `-I OUTPUT 1` 插链首**(否则 ufw/conntrack 的 ACCEPT 先吃包;不支持就 fail loud);nftables 只匹配**主动发起**(TCP 纯 SYN / UDP `ct state new`,否则回包一起被拒 ⇒ 实例整体不可用);**浏览器信任围栏**(剥离 `Origin/Referer/Sec-Fetch-*`、Host 覆写回环);env 白名单重建;路径围栏可钉 POSIX 语义;上传 P0/P1 分级扫描。
- **② 故障自愈与状态恢复**:崩溃策略是**可单测纯函数**(退避 + 熔断跨轮存活 + `503 instance_circuit_open` 带 `retryAfterMs`);**按需守护不常驻**(稳态 1 进程/活跃用户);回收后导航过渡页 / XHR 等就绪 / **401 透明重放** / 回页面自动唤醒;注入脚本 25s 心跳 + 断流(**连续两次失败**才恢复);**注入脚本先模板求值再 `node --check`**(grep/`new Function` 都是假绿);K8s informer + reconcile 对账 + 仅 leader 写;**会话档位自诊断**(只读解压多帧 zstd 读实际 preset)。
- **③ 插件与技能受控管理**:双判据兼容性预检(semver **默认语义** + 运行时导出符号);**「兼容 ≠ 可用」**的托管友好性预检(绝对 URL = 阻断);启用三步(探活/快照回滚/逐插件隔离);内存预估+超限拦截;provider 联动;同名上传=**替换**;技能两层(rank 600/400)+ **两阶段原子替换**;**watch-based ⇒ 装卸免重启**;官方目录 ~3400 条(覆盖 54%)。
- **④ 工程纵深**:双后端同接口(local/k8s)、DB 双后端、关键决策皆纯函数、`npm test` + 9 个 `smoke:*`。
3. `功能详解` 加一句「本节是**能力清单**(有什么);机制见**亮点**」,避免与亮点重复;`安全模型` 补 3 行(同机旁路 / 到实例的请求 / 实例环境)。
**验证**:阻断探针 **0 命中**;README 436 行、`## 亮点` 锚点存在;导出物 140 文件。
**技能升到 v1.0.5**:新增 **R-O8 附则 2「README 内容口径」**(开头=亮点非门槛 / 亮点先于功能清单 / 每条必须带机制与数值 / **先读码再写亮点**+具体读法 / 讲出「难在哪」/ 补工程纵深)。归档副本已同步(md5 `39288e87…`),四件套 exit 0,锁已释放。
---
## 10:34–10:4x · README:去掉「议论式」表述(第三轮纠正)
**用户指令**:「这是开源项目描述 写这个是干什么呢,**不是让你讨好我**」—— 直指开头那句
`> 重点不是「能起进程」——多租户演示十分钟就能写出来。重点是这三件事:`
**问题定性**:那句话是**在跟作者对话**(回应我上一轮的"基线 vs 亮点"),不是写给读者的。读者不知道也不需要知道"作者曾经写错过什么";「十分钟就能写出来」这种表达放在开源描述里既没信息量、又像自我表演。
**改动**
1. **开头块引用整段删除**,换成:**一句 tagline**(「把 DSH 变成可对公网运营的多租户服务」)+ **用户视角描述**(注册审核 → 独立实例与文件根 → 网页桌面 + 自有域名)+ **三条重心的名词式列表**(隔离 / 恢复 / 治理)+ 一句指向「亮点」。
2. **去掉重复定位句** —— 原先 tagline 与描述段把「把 DSH 变成…」说了两遍,删掉一处。
3. **四个小节标题去括号议论**:`### 一、进程级安全隔离`(原带「(不是「同机不同目录」)」)· `### 二、故障自愈与状态恢复`(原带「(面向「长期开着」)」)· `### 三、插件与技能的受控管理`(原带「(「能装」和「装了不出事」是两件事)」)· `### 四、工程基础`(原「工程纵深(为什么这些机制可信)」)。**对比与理由留在表格单元格里当技术说明**,标题只写名词。
**复查**:`重点 / 十分钟 / 演示 / 难的是 / 凭什么 / 拖死` 等议论词在 README 中 **0 命中**;阻断探针 0;README 437 行。
**技能升到 v1.0.6**:R-O8 附则 2 再补三条 —— **面向读者不是面向作者**(禁「重点不是 X 而是 Y」)· **小节标题别加议论** · **别重复定位句**。归档副本已同步(md5 `ee943798…`),四件套 exit 0,锁已释放。
> 📌 **三轮纠正的合力(这才是真正的口径)**:① 出处**只放文末**(别替别人宣传)→ ② 开头写**亮点/重心**而非基线门槛 → ③ 措辞**面向读者**、不写议论与自我表演。三条已全部固化进技能 R-O8 附则 1/2。
---
## 10:37–10:5x · 开源文档审计 + 删掉「补齐了 DSH 的哪些缺口」
**用户指令**:「详细检查准备开源的相关文档 是否有相关问题以及 之前提到要重点检查的问题」+「(架构里的)它补齐了 DSH 的哪些缺口 这个内容也 不是重点」
- 第二条已执行:删掉 `## 架构` 下的 `### 它补齐了 DSH 的哪些缺口`(那 4 行讲的还是账号/访问面 = 基线),换成一行指向「亮点」。
**审计方法**:机检为主(脚本比对)+ 抽样人工核。**完整记录写进台账 §八**。
**✅ 通过 9 项**:目录锚点 **15/15** 有效 · 相对文件引用全在 · README 提到的 npm script 全在且 `smoke:*` 计数 = 9 · **install.sh 的 env 名全部真实存在** · **配置表没有一个是代码里不存在的**(反向检查)· 依赖 **178 包**许可证分布与文档**完全一致** · 文档内 **0** 内部档案号 · `ci.sh`/workflow/package.json 引用脚本全在 · 配置默认值逐项核对一致。
**🔧 已修 4 类**(全部在 `_build_export.py` 里有规则,重建不丢)
1. ⚠️ **真 bug(代码不是注释)**:`orchestrator.ts` 的「目录选择器初始化脚本」默认值指向 `poc/workspace-scoped-picker/ensure-workspace-picker.cjs` —— 该插件已随 R-O3 移除 ⇒ 默认值改**空串**(有 `existsSync` 守卫,空即 no-op),`DSH_PICKER_ENSURE_SCRIPT` 保留为可选钩子。**已进 `LINE_REWRITE`**。
2. 4 处注释点名已移除脚本(`ws-cleanup.cjs` / `business-plugins.ts` / `admin.ts` / `orchestrator.ts` 注释)→ 中性化,**已进 GLOBAL**。
3. README 配置表**漏记 11 项**,其中最要紧的是**崩溃熔断 6 项**与**空闲回收 3 项**(正是「亮点」里的功能!)→ 已补全(含 `RESTART_BACKOFF_MAX` / `CRASH_MAX_RESTARTS` / `CRASH_WINDOW` / `CRASH_STABLE` / `CRASH_BREAKER_COOLDOWN` / `CRASH_BREAKER_MAX_COOLDOWN` / `MAX_IDLE_INSTANCES` / `INSTANCE_IDLE_TTL` / `IDLE_REAP_INTERVAL` / `BUNDLED_SKILL_DIR`)。
4. **`install.sh` 写入值与 README「默认值」列不符**(脚本 `ISOLATION_MODE=account` / `ENABLE_PATCH=true` vs 代码默认 `soft` / `false`)→ 表后加「补充说明」:脚本取值差异 + **平台注入变量 3 个**(`ROLE`/`HANDOFF_PATH`/`USER_ROOT`)+ CLI 专用 + 可选钩子。
**验证**:重建后 `tsc --noEmit` **exit 0** · 阻断探针 **0 命中** · 悬空脚本名 grep 为空 · `install.sh` 语法 OK · 140 文件。
**⏳ 待用户拍板 6 项**(**未擅自改**,R7:>10 文件需先点头)
- **① 29 个文件 / 42 处 `docs/*.md` 悬空引用**(`docs/k8s.md`×37、`k8s-deploy.md`×3、`blueprint.md`×1、`domain-config.md`×1;全在注释里;其中 `docs/k8s.md` 上游**从未公开过**)→ 建议做一次纯注释替换(映射到 README 章节)。
- ② 183 处「档案 NN」注释编号(可与 ① 一次做完)。
- ③ **7 处占位符**(`<YOUR_GITHUB_ACCOUNT>`×3 / `<CONTACT_EMAIL>`×3 / `<COPYRIGHT_HOLDER>`×1)发布前必须替换。
- ④ `install.sh` 未在真 Linux 跑过 ⑤ `LICENSE` 自拟条款 + 分层授权前提待确认 ⑥ `poc/` 两个插件是否加回。
## 10:38–11:0x 用户转向「保留功能前提下的架构/功能优化」→ 出评估档 80(P0 已上线)
**用户两句话**:① 「看看能否彻底抛弃 dshs 的架构 以及跳转 portal.html,不要在本项目中体现 dshs 任何内容,把 admin 的门户管理都放到设置选项中 弹窗展示」→ ② 「看看如何优化架构和功能,在保留现有功能的基础上 优化重构后有更好的效果」。
**我的判断(写进档案 80)**:**不推倒重来** —— 平台本体(账号/审批/实例生命周期/反代/配额/DB)正是"要保留的功能";而**今天几次事故根因全是「层层补丁叠加」**(模板转义把注入脚本写崩 ×2、配额与插件集合脱钩致 OOM 137、缺有界恢复、自愈 5 代补丁)⇒ 方向 = **收口 + 显式化 + 构建期校验**。
**9 项优化(按事故排序)**:①注入脚本外置+构建期硬校验 ②`npm run verify` 统一校验 ③**实例配额跟随插件集合**(自适应内存;今天 384<390 的实证)④**门户管理就地化 = 弹窗 iframe**(实测平台页**无 `X-Frame-Options`** ⇒ 同站子域可嵌;⚠️ 与档案 05 PoC-2 ① 结论冲突 + **触发 R5**,推荐 iframe 而非能力下沉)⑤自愈收口成一份状态机 ⑥有界恢复 ⑦门户页面结构 ⑧可观测性统一 ⑨文档/代码同仓。
**不做**:重写平台 / 管理 API 下沉进实例 / 改内部命名(systemd·路径·DB)/ 给 univer 设内存上限或禁用(用户明示它是压力探针)。
**P0 已上线(静态页免重启,7 文件)**:`index`→工作台、`login`→登录、`register`→注册、`portal`→管理门户(副标题 →「平台管理」)、`admin`→管理台、`wake`→正在启动工作区、`design.css` 注释去名;线上逐个 `<title>` 取回已验证。
**顺手修掉我自己的工具 bug**:`scripts/docs-index-stats.py` 读取时**没加 `newline=""`** ⇒ 把 INDEX **CRLF 转成 LF**(正是今天记录过两次的陷阱)。已改为「读也 `newline=""` + 按文件自身 eol join」,并修掉历史遗留的 1 处孤立 LF 与摘要行;复跑 3 次确认幂等(CRLF 198/孤立 LF 0),`git diff` 只剩 13/4 行真实改动。**教训升级:自建工具同样必须守 `newline=""`。**
**文档**:档案 80 + INDEX 行 + docs-manifest,四件套 rc=0,对账 一致 133(余 4 不一致 + 2 仅本地 = 其它会话积压);锁与占号(80)均已释放。未 commit / 未 push。
⚠️ 另记:`04-调整方案/.lock-78` 是**并行会话遗留**(它把档案写成 79 却留了 .lock-78),我未触碰(R9)。
---
## 10:43–10:5x · 「模式 B = 未验证」全量标注(用户纠正)
**用户指令**:「模式 B · Kubernetes **去掉这个,根本没验证**,应该是**待开发验证**」。
**判定:不能删代码,但必须停止把它当可选项宣传。** 模式 B 有完整代码(`k8s-spawner` 687 行 / `leader` 254 / `reconcile` 170)+ `deploy/` 11 个清单 + 单测 + `poc/` 实验记录,但**从未端到端部署验收**(真实生产只有模式 A)。⇒ 一律标注「实验性 · 未验证」,并与模式 A 在**同一张表里用状态行对照**。
**改动(README 12 处 + install.sh 1 处 + LICENSE/NOTICE 3 处)**
1. **`## 部署形态` 重写**:开头一句「当前可用的是模式 A」;表格新增 **「状态」行**(A ✅ 生产使用中 / B ⚠️ 未验证,请勿用于生产);把原来的「已完整落地(Phase 0–4)」式段落改为「设计意图 …… **这条路径还没有走过一次完整的部署验收**」。
2. **配置表 7 处** k8s 变量加前缀「(模式 B · 未验证)」/「(模式 B · 未验证;必填)」,`DEPLOY_MODE` 行同步。
3. **亮点/功能详解/安全模型** 里 5 处 `K8s Pod 边界`、`K8s 侧同一套闭环`、`双部署后端共用同一接口`、`数据层双后端` 全部加注「模式 B · 未验证」。
4. **目录结构**:`deploy/` → 「K8s 清单(模式 B · 实验性,未验证)」;`poc/` → 「K8s 可行性 PoC 实验记录 …… 仅实验,未形成可部署形态」;`fs/` 行注明 K8s 实现未验证。
5. **FAQ 新增一条**:「Kubernetes(模式 B)现在能用吗?」→ 直答「**还不能算可用**」并说清缺哪一步。
6. **`install.sh`** 报错文案:`模式 A 仅支持 Linux(K8s 用 deploy/ 下的清单)` → `本脚本只做模式 A(单机);K8s 清单见 deploy/(模式 B,实验性、未验证)`。
7. **归属措辞降级**(LICENSE / THIRD-PARTY-NOTICES / README 授权表与致谢):上游贡献里的「双部署**形态**」→「双部署**框架**」,避免暗示未验证路径是可选项。
**验证**:`grep 未命中` 全通过;残余 `已完整落地|Phase|3 副本` **0 命中**;阻断探针 0;`bash -n install.sh` OK;140 文件。
**技能升到 v1.0.7**:新增 **R-O9 · 只写「已验证的」能力**(宣称必须可复现:**有代码+单测+PoC ≠ 可用**;未验证的标「实验性 · 未验证」并与可用路径同表对照;删「已完整落地/Phase」式表述;配置表与脚本文案同步;FAQ 直答;归属措辞降级;附**落点清单**)。归档副本已同步(md5 `22f0f649…`),四件套 exit 0,锁已释放。
> ⚠️ 修技能时**误删了附则 2 表格的一行**(`别重复定位句`)—— `Edit` 的 old_string 只写了那一行却替换成整段,导致表格断裂;已即时发现并补回 + 补分隔线。教训:**用 Edit 追加内容时,new_string 必须包含 old_string 的全部内容**。
## 10:33–10:55 会话 `a2665dd3`:**方案 E 已实现 + lab 验证通过(0.2.19)**
**用户**:「可以试试」⇒ 实施 E(gateway 空闲自停)。
**改动(3 处 + 版本 0.2.19)**
| 文件 | 改动 |
|---|---|
| `src/gateway-app/server.ts` | 新增 `startIdleWatchdog({httpServer, wss})`:每个 HTTP 请求 touch `lastActivity`;`wss.clients.size > 0` 视为"有人在看"(刷新计时);**无请求 ∧ 无客户端 ∧ 静默超过阈值 → 打日志 + `process.exit(0)`**;阈值 `UNIVER_DSH_GATEWAY_IDLE_MS`(默认 **600000 = 10 min**,**0 = 关闭**);定时器 `unref()` + 间隔自适应(阈值/4,夹在 5–60 s) |
| `src/host/processes/gateway/launcher.ts` | 透传 `UNIVER_DSH_GATEWAY_IDLE_MS`(可按实例调,不用重建) |
| `src/host/config.ts` | `gatewayStartupTimeoutMs` 默认 **10_000 → 20_000**(冷启动本来要 5–10 s,10 s 太紧 —— 也是 "did not become ready" 的成因之一) |
- 产物核验:`gateway.cjs` 含 `startIdleWatchdog`×2 / `UNIVER_DSH_GATEWAY_IDLE_MS`×1;`lib/index.js` 含透传 ✓;`tsc` 三个改动文件零错误。
**★ lab 实测通过(独立 socket /tmp/uvlab.sock,阈值 20 s,不碰生产实例)**
- 启动日志:`[dsh-univer-gateway] listening on unix:/tmp/uvlab.sock`
- 25 s 后:**`[dsh-univer-gateway] idle 20s with no client → exiting to release memory`** ⇒ 进程自行退出 ✓✓
- 踩坑:lab 的 `NODE_PATH` 必须 = **`dirname <pkg>`**(即 `.pnpm/<pkg>@…/node_modules`),写成 `$GP/../node_modules` 会多一层 `node_modules` → `Cannot find module`。
**产物**:`dsh-univer-office-0.2.19.tgz`(42,046,985 B / md5 `7ec224c95fa471076227f1b0ed2d3342`);投放中(`http=200` 待确认)。
**回答用户"这插件不就是显示文件吗,为什么这么难"**(要点,已口头给出):它不是"预览器"而是**服务端活文档系统**(协同模型:unit/worktree/快照/变更集);核心是**闭源** `@univerjs-pro/collaboration-service`(加载面不可裁);叠加我们的约束(多租户 + nft 封 loopback ⇒ 只能 unix socket;实例 384 MiB)⇒ 难的不是"渲染",是"在沙箱里常驻一个完整办公套件服务端"。
---
## 10:48–11:0x · README 结构按社区标准重排 + 22 项自审
**用户指令**:「说明文档的顺序还需要优化,参考优秀项目的说明文档,如何结构化展示内容,以及**是否应该把授权等信息放在最前面**,可以去找找有没有写 github 说明的相关 skill」。
**A. 先找技能**(按规矩先搜市场)
- SkillHub 检索到三个对口技能:**`readme-craft`**(README 写作/审计/重写;蒸馏 awesome-readme · Standard README · Art of README · Make a README · GitHub Docs · thoughtbot)、`standard-readme`、`readme-grader`。
- 下载到**工作区内**临时目录(`$HOME` 写入被沙箱拒;SkillHub 的 download API 只在工作区内可落盘)读完即清理;**未安装到本机**。`readme-craft` 详情页带两份厂商安全报告(keen / sanbu 均"安全,无风险")。
- WorkBuddy 内置市场搜 `readme` 只有 `git-copilot`(偏提交信息),无专用结构技能。
**B. 回答「授权是否放最前」= 不放,但要加一行摘要**
- 依据:**6 个权威来源共识排序**均把 License 放在**最后**;readme-craft 的 **Trust 评分项**明文「License 在最后?」。
- 但我们是**双授权**(非 MIT)⇒ 授权是**决策信息** ⇒ 在徽章下方加**一行授权摘要**(+锚点链接),完整条款仍在末节,并以「授权摘要 + **非 SPDX** 说明」收尾。
**C. 重排为 16 节(认知漏斗 Hook → Onboarding → Content → Trust → Meta)**
- 关键移动:`安全模型` 从「版本与迭代之后」**上移到 `架构` 之后**(原位置属漏斗倒序);`插件移植指南` 提到 `开发` 前;`第三方组件与致谢` 与 `授权` 对调 ⇒ **授权压轴**。
- **用脚本按 `## ` 标题切分再拼装**(不手工搬),重排后**重算 TOC** 并机检:**16/16 锚点可解析、顺序与章节一致**。
**D. 补齐审计扣分项**
- **H3 Hero Visual**:Hero 区(前 50 行内)新增**多租户拓扑图**;`架构` 节里那份重复拓扑改写成**两条链路**(请求 + 自愈,含熔断 10min→6h)⇒ 同时消除信息重复。
- **C3 API 概览(原来 0 分)**:新增 **`## 控制面 API`**,10 组端点**全部用 `grep -oE "app\.(get|post|put|delete)\("` 从 `src/web/routes/*.ts` 抓真实路径**(含 `skills/mine` 两阶段暂存→apply、`plugins/mine/apply` + `task/:id` 轮询)。
- **H2**:让 `package.json` 的 `description` 与 README 一行描述**同一起句**(写成 `_build_export.py` 规则)。
- 顺带修:表格单元格里**不能写裸 `|`**(`GET|POST` 会截断表格)。
**E. 自审结论:93/100 → S 级**(Hook 25 · Onboarding 25 · Content 20 · Trust 9 · Structure 9 · Polish 5)。**扣分只有"发布后才能补的"两项**:CI 徽章 0/3、维护者信息 0/3(`<CONTACT_EMAIL>` 未填);另信息重复扣 1。
**技能升到 v1.0.8**:新增 **R-O8 附则 3 · README 结构顺序**(10 条硬规则 + 22 项审计口径 + 定稿顺序图)。归档副本已同步(md5 `31d98264…`),四件套 exit 0,锁已释放。临时技能目录已清理。
## 11:0x 用户授权「按方案改进,要新架构 + 命名细节」→ 出档案 81(总纲)+ R0 完成 + R2 提前做掉一项
**用户原话**:「按照方案改进,要求改造后是一个新的更好的架构包括命名方式等细节」+「按06-工作台ui规范调整优化ui,包括登录窗口样式等,如点击登录后停留几秒不能操作按钮也没有加载动画」。
**交付 1:档案 81《目标架构与命名规范(重构总纲)》**
- **命名**:对外统一「**工作台**」(英文 **DSH Workspace**);内部统一前缀 **`dshs`**(单元 / 源码目录 / 数据目录 / DB / env 前缀 / 包名 / 注入脚本 / 注入全局标记 / CSS 类全表,每项都写了**迁移方式**)。
- **兼容策略(硬要求)**:内部改名一律 **双名期(symlink + 新旧 env 双读)→ 切换期(删旧名)**,绝不硬切 —— 今天刚因"hooks 绝对路径失配导致全机写操作被拒"上过一课。
- **六层架构**:静态 / 注入 / 控制面 / 数据面 / 管理面 / 可观测;**4 处关键改动**:注入脚本出模板字面量 · 自愈收口成状态机 · **配额跟随插件集合** · **有界恢复**。
- **分期 R0–R4**(每期带验收与回滚):R0 ✅ 本轮完成|R1 四项(需一次重启)|R2 管理面就地化(**先 R5 评估**)|R3 内部标识迁移(两次窗口,最后做)|R4 文档同仓。
**交付 2:R0 全部完成**
- 静态页去痕迹(7 文件,已上线);`npm run verify` 统一入口(build + 单测 + 注入脚本**运行时**校验 + 静态页不变量)。
- **新增 `scripts/verify-static.mjs`**:① 去痕迹(`web/**.{html,css,js}` 不得含内部平台名)② title 非空 ③ wake.html 五个 id + 熔断分支 ④ **内联 `<script>` 必须能 `node --check`**(今天两次事故都是"脚本语法错 = 静默失效",此前无任何校验能发现)—— 现 10 段内联脚本全通过。
**交付 3:R2 提前做掉「登录/注册页提交忙碌态」(用户报障,已上线)**
- 根因:`login.html` 提交处理**没有任何忙碌态**;`/api/auth/login` 走 bcrypt 数秒、`/api/dsh/enter` 还要拉起实例(可达 20s)⇒ 期间按钮可点、无反馈。
- 改法(对齐 06-UI规范 Token,只加不删):`.btn[disabled]` / `.btn.is-busy` / `.btn .sp`(14px 转圈)+ `prefers-reduced-motion` 关闭 + 通用 `setBusy(busy,label)`(记住原 innerHTML、退出原样恢复)+ **两段式文案**「登录中… → 正在进入工作台…」;register 同构(成功后保持禁用防重复注册)。
- **真 Chrome 实测**:点击后 **220 ms** 即 `disabled=true` + `is-busy` + spinner +「登录中…」;失败后恢复「登 录」并显示「用户名或密码错误」。截图 `_patch77/shots/login-busy.png`。
- 部署:静态页 + 校验脚本 scp(**免重启**)。回滚 `git checkout -- web/login.html web/register.html`。
**文档**:档案 81(含 §七 进度追加)+ INDEX 行 + manifest 已推送;四件套 rc=0;锁与占号(81)已释放。未 commit / 未 push。
---
## 11:04–11:1x · README 撤掉前排授权摘要 + 🔴 **误放别的会话的锁(事故)**
### 一、README(按用户意见)
用户指出「授权……」虽然章节在最后,但**前排正文里还留着我加的那句授权 blockquote** ⇒ **撤掉**。
- 现状(已核实):`## 授权与商业使用` 本来就在**最后**(第 464 行 / 共 484 行);前排只剩 **① License 徽章(第 7 行,链接到末节)+ ② 我加的第 12 行摘要**。
- 处置:**删掉第 12 行那句 blockquote** ⇒ 全文授权信息只剩:顶部**徽章** + 目录项 + **末节**(含文末一行摘要)。徽章已写明 `personal free | commercial paid`,不丢信息。
- 验证:TOC **16/16 锚点正确、顺序一致**;阻断探针 0;484 行。技能 **v1.0.9** 同步改口径(附则 3 第 2 条:**授权不进前排正文**,靠徽章 + 末节摘要)。
### 二、🔴 事故:把另一个会话的全局执行锁 `rm -rf` 掉了(已恢复)
**经过**:为同步技能归档副本,我把 `--claim-exec` 与 `--release-exec` 写在**同一条命令**里,并把抢锁输出**重定向到文件**只显示了 `OWNER` 首行 ⇒ **没看见「✗ 抢锁失败」**。那时锁属于 **`R1-注入层-1104`**(11:03 起)。命令末尾的 `--release-exec` 实现是 `rm -rf "$LOCKEXEC"` ⇒ **别人的锁被我删掉**。
**我的两个失误**:① 抢锁结果没当场看;② claim 与 release 同命令(**这正是我自己在技能坑 16 里写过的禁忌**,写了却没守住)。
**恢复**:该脚本的 release **不留档**,于是按 `handoff-guard.sh:43` 的格式**原样重建** —— `R1-注入层-1104` / `开始:09-13 11:03` / `在做:(未声明单号)`(值取自抢锁失败时的输出)。已验证:信息模式显示占用者仍是 `R1-注入层-1104`,我自己再抢**被正确拒绝** ✓。**该会话的 `--release-exec` 仍可正常释放**,流程未断。
⚠️ 若 `R1-注入层-1104` 其实已结束,这个锁会变成**残留锁** ⇒ 需**用户本人**撤(处置权只属于用户)。
**同时披露**:在那段「以为自己是持有者」的窗口里,我还做了两件事 —— ① `cp` 覆盖了**自己的**技能归档副本(低风险);② 跑了四件套,其中 `docs-manifest.py` **写了一次 `docs-manifest.json`**(派生文件、可重生成)。两者都与对方的工作无内容冲突,但**纪律上不该发生**。
**已固化的改进**:技能 §8 坑 16 **升级为 🔴 事故级**(claim 输出必须当场看 / 放锁前再确认 OWNER / 误放后按 `handoff-guard.sh:43` 重建并告知用户);技能版本 v1.0.9 → **v1.1.0**。
⏳ **待补**:技能归档副本仍停在 v1.0.9(锁被别人持有 ⇒ **按纪律停手**),锁释放后按 §10 清单同步。
## 11:0x–11:5x 「继续处理直到改造完毕」:R1 全部落地并验收 + R3 双名期第一步 + R2 的 R5 评估
**R1(需重启,已完成)**
- ① 注入脚本外置:`assets/inject/{recovery,assist}.js` + `proxy.ts` 用 `loadInject()` 运行时读(fail-fast)⇒ 27KB 内联模板字面量删除,「转义写崩」从根消失
- ② 自愈收口:`recovery.js` 头部写明**唯一状态机表**(5 类触发面 × 判定 × 动作)
- ③ 有界恢复:10 min 内恢复 >3 次 → 停手 + 手动「重试」按钮(治无限循环)
- ④ 配额随插件集合:`instanceMemMb()`(160+Σ预估,下限384/上限1024)+ V8 堆**由配额独占**(`withHeap()` 摘掉 env 里的旧值再按配额补回,`DSHS_HEAP_MB_OVERRIDE` 可强指)
- **验收**:服务 active(PID 435201)、门户 200;**guest 新实例 MemoryMax=544MB + NODE_OPTIONS=--max-old-space-size=256**;guest 实例页含 `overRecoverBudget`/`__dshRetryBtn`/状态机表/新视觉 ✓
- ⚠️ **事故(约 1 分钟不可用)**:`__dirname` 在 ESM 里不存在 → 平台启动即失败、systemd 重启 3 次;改用 `dirname(fileURLToPath(import.meta.url))` 后恢复。**教训:`npm run build` 通过 ≠ 能跑,ESM/CJS 运行时差异编译期查不出 ⇒ 部署后必须立刻 curl 自证。**
- ⚠️ 另一发现:`DSH_INSTANCE_NODE_OPTIONS` 不在 unit / manager env 里却仍在进程 env ⇒ **改配置够不到**,只能代码侧独占堆。
- 备份 `/opt/dsh/backups/pre-r1-20260913-110923`。
**R3 双名期第一步(零中断,已完成)**:`/opt/dshs`、`/var/lib/dshs`、`dshs.service` 三个 symlink + daemon-reload ⇒ **两名字同 PID 435201**;回滚一条口令(`rm -f` 三个 + daemon-reload)。剩余:env 双读 → 逐条改绝对路径 → 切换期(改单元真名 + 迁 DB)。
**R2(待用户拍板)**:已出 **R5 权限影响评估**(结论:建议批准「iframe 内嵌 + 仅 admin + 只读优先」;实测 portal.html 无 `X-Frame-Options`)。
**R4**:两条路(文档并入代码仓 / 保持双库但单向导出)待用户选。
文档:档案 81 §八/§九 已写入并推送;四件套 rc=0;双锁已释放。未 commit / 未 push。
## 11:18 新需求立项:**多语言(i18n)· 排最后(R5)**
用户原话:「加个需求 相关页面能否 支持多语言,可以最后处理」。
- 已写进 **档案 81 §十**(范围 / 方案 / 为什么排最后 / 验收口径)+ §四 分期表加 R5 行 + **03-路线图 待办表**加「R2–R5 待排期」行 + INDEX 04-81 行补记。
- **范围**:✅ 平台 6 个静态页(`/`/login/register/portal/admin/wake)+ 注入脚本文案 + 平台 API 错误码→文案映射;❌ **不动**实例内 dsh 官方 UI(R2 红线)与业务插件文案。
- **方案**:`assets/i18n/{zh-CN,en}.json` 词条表 + `assets/i18n.js` 极小运行时(`?lang=` > cookie `dshs_lang` > `navigator.language` > zh-CN 兜底;`data-i18n` 标注就地替换;缺 key 回退中文并 warn)+ 页脚语言下拉;`verify-static.mjs` 加「裸中文」防回归判据。
- **排最后的三条硬理由**:① R2 门户改造会横穿页面,先抽白抽 ② R3 命名未收尾,`assets/i18n/` 目录要对齐 `dshs` 体系 ③ 纯体验增强、不修事故(今天 4 类事故已闭环)。
- 同步记录 R2(待 R5 权限确认)/ R3(双名期第一步已做,剩 env 双读+路径改写+切换期)/ R4(待选 a/b)的状态。
---
## 11:18–11:3x · 致谢「只写一行」+ 🔴 发现导出基线在漂
### 一、按用户意见精简(第七次文案纠正)
用户指出致谢行「**有必要讲这么多吗,好好想想**」。原文三段里有两段是在讲**我们自己**:
- 「此后在本仓库做了大量深度改造」= **自我表扬**(致谢不是讲功绩的地方)
- 「项目名也从它的 X 换成了我们的 Y(避免指代混淆…)」= **内部事务**,读者不关心
- 「骨架范围枚举(认证与审核 · 网页桌面 · …)」= 与 `LICENSE` 第一层 / `THIRD-PARTY-NOTICES.md §4` **重复**
**改为一行**:`起步时参考了 [dshs](作者 上游作者(已按要求不再具名),MIT)—— 感谢作者开源。`(法律范围保留在 LICENSE / NOTICE 两处 ✓ 未丢)
**同类一并清掉**(这才是"好好想想"):
- 版本与迭代里重复的议论句「本项目把『能跑』补齐为『能对外长期运维』」→ 删(标题已是「六组改造」)
- 授权节尾「—— 这是 MIT 的硬性要求,必须保留」→ 删解释,只留规则本体
- 部署形态末「因此 README 与 install.sh 一律以模式 A 为准」→ 删(关于文档自身的话术)
- DeepSeek Harness 那条收紧为一行
- **三处「机制见亮点」指针砍到 2 处**(Hero + 功能详解),NOTICE 致谢行去掉与致谢无关的自家验证状态括注
**规则固化**:技能 R-O8 附则 1 新增 3 条 —— **致谢只写一行**(禁骨架枚举 / 禁自我表扬 / 禁改名理由)· **别解释我们为什么这么写** · **指针行不重复**。
**验证**:TOC 16/16 锚点正确;探针 0;`tsc --noEmit` exit 0;README 478 行。
### 二、🔴 导出基线在漂(本轮最重要的发现)
重建时探针突然报 **`mcn-suite`** ⇒ 溯源发现:**源码仓 `D:\github\dsh_shenxian` 的 HEAD 仍是 `da3e0f9`,但工作树已有 20 个未提交文件**(`orchestrator.ts` / `crash-policy.ts` / `proxy.ts` / `spawner.ts` / `config.ts` / `web/*.html` / `test/*` / `package.json` …)—— **多个会话正在并行改源码**(R1-注入层 / R5多语言立项 等)。而导出脚本**复制的是工作树** ⇒ **导出里混入了别人未完成的改动**,且新增标识(`'dsh-plugin-mcn-suite'`、内存预估表)随他们提交冒出来。
- 已处理:新增 GLOBAL 规则把 `'dsh-plugin-mcn-suite'` → `'某插件套件'`、注释去 MCN 化(`dsh-univer-office: 384` 按特许保留 ✓);重建后探针 **0 命中**。
- **发布前必须二选一**(已写进技能 §10):① **改成按 commit 取**(`git archive <commit>`,才是真正的冻结版本);② 或接受"含未提交工作",则**每次重建都要重跑探针**并复核 README 描述与实际一致。
### 三、待补(锁在别的会话手里)
技能已到 **v1.1.x**,归档副本仍停在 **v1.0.9** —— 锁先后被 `R1-注入层-1104`(11:03) / `R5多语言立项-1120`(11:18) 持有 ⇒ **按纪律停手**,同步清单已写进技能 §10。
---