chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进

1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,305 @@
---
name: dsh-decision-method
description: dsh 多租户平台(alotbuy.com)「改造 / 优化功能交互 / UI 界面」的**决策方法论**。当用户提出一个新需求、问「这是不是最优方案 / 还有没有更好的做法」、要在多个方案里选型、要判断某个决策是否该做 / 该不该扩大范围、要**自主给技术实现选取最优解**、或者要复盘「为什么这么定」时使用。核心 = 决策素材库(用户有效决策 **U1-U28** / AI 有效决策 A1-A25 / 反例 X1-X14,**素材源含 180 条用户真实发言**)+ 「如何确认最优解」的十问与判定矩阵 + **技术实现的默认裁决顺序(8 条,AI 自主用、不问用户)** + 决策流程十步 + 交互 UI 改造专项清单 + 决策语言对照表 + **复跑脚本 `extract-user-voice.py`**。**v2.0.0(2026-09-14)结构变更:素材库(U/A/X)已拆到 `references/`,按需读;本文件只留判定核心 + 触发词索引**(索引绑定可识别动作)。**与 dsh-change-workflow 分工:本技能管「怎么想、怎么定」,那个管「怎么落地」。** 与 dsh-feature-first 分工:那个管「谁定什么」,本技能管「怎么定得对」。
version: 2.7.4
updated_at: 2026-09-15
created_from: 本工作区 62 份改造档案 + 5 天工作日志(2026-09-08 ~ 09-12)全量提炼
agent_created: true
---
# dsh-decision-method — 平台改造的思考与决策方法
> **一句话**:把「这个需求该怎么定」从**直觉**变成**可复用的判定**。
> 材料来源 = 本工作区 `dsh-server-docs/04-调整方案/` 62 份档案 + `.workbuddy/memory/` 五天日志里的**真实决策痕迹**(含被驳回的)。
> 每条模式都带**实例出处**,可以回溯核验,不是抽象原则。
---
## 0. 定位:和 `dsh-change-workflow` 的分工
| | 本技能 | `dsh-change-workflow` |
|---|---|---|
| 管什么 | **需求和方案怎么定**(选型 / 收敛 / 判方向 / 拍板) | **定了之后怎么落地**(六阶段 / 红线 / 档案模板) |
| 什么时候用 | 用户刚提需求、问"最优方案"、要在选项间选、要判"该不该做" | 决策已定,开始改码 / 验证 / 归档 |
| 产出 | **选项表 + 决策点 + 判定依据** | 代码改动 + 验证记录 + 档案 |
> 顺序:**先本技能定案 → 再 dsh-change-workflow 执行**。规划会话只做前者,执行会话只做后者(交接载体 = `dsh-server-docs/交接单/`)。
### 0.1 素材来源与复跑方式(**honest provenance,勿含糊**)
| 素材 | 位置 | 说明 |
|---|---|---|
| 改造档案 | `dsh-server-docs/04-调整方案/NN-*.md` | 每份含「触发 / 用户裁定 / 方案对比 / 事故踩坑」——**会话的结论层** |
| 工作日志 | `.workbuddy/memory/YYYY-MM-DD.md` | 五天过程日志,含用户原话引用——**会话的过程层** |
| 现行事实层 | `BRIEF.md` / `INDEX.md` / `README.md` / `06-工作台UI规范.md` / `交接单/README.md` | 约定与红线 |
| **原始会话转录** | `~/.workbuddy/projects/<工作区目录名>/*.jsonl` | **用户真实发言的原始记录**(含被否决、被纠正的内容) |
**复跑命令(只读)**:
```bash
# 抽出本工作区全部历史会话里的「用户真实发言」(2026-09-12 实测 = 180 条,跨 09-08→09-12)
python3 dsh-server-docs/scripts/extract-user-voice.py
# 查某个决策的来龙去脉
python3 dsh-server-docs/scripts/extract-user-voice.py --needle 复用价值
```
> ⚠️ **本方法 v1.0 的素材边界(如实记录)**:首版只从**档案 + 日志**提炼(当时会话检索接口返回 0 命中)。
> 2026-09-12 用上面的提取器回补核对 —— **U1–U12 里 11 条能在原始发言中找到对应原话**(覆盖良好),
> 据此**补上了 U13–U19** 这 7 条只在原始对话里才看得见的模式。**以后迭代本技能,先跑这个脚本。**
---
## 1. 决策素材库 · 用户的有效决策(U1–U28) → **`references/素材库-U-用户决策.md`**
> **已被拆出本文件**(原占全文 55%)。本文件只留「**判决核心 + 指针**」:素材库是**查阅型**(看了更准、不看也不违规),判定核心才是每次都要用的。
> **触发词 → 直接查哪条**(**必须去读**,别凭印象答用户口径):
| 你正在判断什么 | 去查 |
|---|---|
| 「这件事该不该问用户 / 我是不是又在上抛了」 | **U20 · U21 · U22** + 本文件 §4.1 两条总闸 |
| 「要不要自己开发 / 有没有现成的」 | **U23**(官方库优先 + 体检三关) |
| 「多期方案 / 兼容性怎么定」 | **U24** |
| 「要给用户看什么 / UI 怎么排」 | U5 · U7 · U11 + 本文件 §6 |
| 「这个改动会不会让项目变差」 | **U28**(十维净变差即命中 ⇒ **停下复盘**;无正向做法 ⇒ **立即停止**)+ 红线 R11 |
| 「要不要降级/延期/先这样跑」 | **U27**(要的是解决问题,不是将就妥协)+ A6 边界(目标不打折、路径取最小代价)|
| 「方案有风险/问题,能不能先干着看」 | **U26**(先优化到「当下最优解」+ 残余风险写清,再走下一步)|
| 「抢了锁之后怎么收口 / 能不能先放着」 | **U25**(锁的生命周期 = 任务的生命周期;带锁结束不算完成)+ 本文件 §4 判据 |
| 「删 / 留 / 清理 / 收尾怎么定」 | U3 · U6 · U14 |
| 「用户那句话到底是什么意思」 | 读全表(每条 = 原话 + 落地 + 判据) |
---
## 2. 决策素材库 · AI 的有效决策(A1–A22) → **`references/素材库-A-AI推理.md`**
> **已被拆出本文件**。**触发词 → 直接查**:
| 你正在判断什么 | 去查 |
|---|---|
| 「我的证据够不够 / 结论能不能下」 | A1 · A4 · A5 · **A18**(静默失败会伪造结论)· **A19**(验收要含第二环境) |
| 「我凭什么说这是限制 / 做不到」 | **A17**(配置项 / 已抽象层 / 真硬编码,三层取证) |
| 「写状态 / 写支持度 / 交付回执」 | **A15**(三态词表:✅已验证 / ⚠️待开发验证 / ❌已知不支持) |
| 「删除 / 迁移 / 回滚 / 替换」 | A8 · **A16**(伴随物清单)· **A20**(存量烂账选只增不改)· **A22**(先补位再退役) |
| 「我做完了吗 / 能不能宣布完成」 | **A25**(本机改完 ≠ 交付:先画「改动层 → 生效链路」;四条"自我安慰"不算交付)|
| 「我装的门禁/钩子到底管用吗」 | **A24**(拦截面必须覆盖真实行为面 —— 先核账再装)|
| 「判据 / 分类 / 索引怎么设计」 | **A21**(必须报分布、要有区分度) |
| 「失败面 / 报错 / 能力不确定」 | A10 · A11 |
| 「流程走到哪一步了 / 我是不是跳步了」 | **A23**(流程类失效 = 触发词没命中;规则要进常驻层、触发用动作词)+ `dsh-change-workflow` 六阶段 |
| 「拍板前还要问自己什么」 | A2(必含"不做")· A3(决策点)· A6 · A7 · A9 |
---
## 3. 反例库:被驳回 / 被纠正的决策(X1–X13) → **`references/素材库-反例-X.md`**
> **已被拆出本文件**。**每条反例 = 一条避免规则**。**触发词 → 直接查**:
| 你正在判断什么 | 去查 |
|---|---|
| 「我刚被纠正了,同类还有哪些坑」 | X1(批量)· X4(归因)· X9(过度上抛)· X10(答非所问)· X11(越界)· **X12**(把用户材料当权威)· **X13**(把自己的解析失败当成版本差异)· **X14**(一批改造拆成多次中断动作)|
| 动手前的"别踩"清单 | 读全表 13 条 |
---
## 4. 如何确认最优解(本技能的核心)
### 4.1 判定矩阵:先给方案贴标签
> **⭐ 两条总闸(顺序在前:先问「是不是我的 lane」,再问「要不要停手」)**
> **闸 1 · lane**:这件事**落在谁的 lane**?—— ① **我自己负责的**(我的插件源码/产物、该 profile 的依赖、我自己的临时脚本、我方案内的执行细节)⇒ **自己拍**;② **平台级 / 全局 / 别人 lane 的**(`/var/lib/**`、全局符号链接、别人的 profile、别人的产物)⇒ **只报告、不动手**,**哪怕改它能让自己流程跑通**(2026-09-13 用户原话:「谁让你去改这个的」「不是自己负责的任务相关文件不要去改」→ **X11**)。
> **闸 2 · 门禁**:命中**真门禁**才停等确认 —— 现行只剩两条:① **不可逆破坏性操作**(删数据 / 迁 DB / 清目录)② **边界外**(业务目标与优先级 / 花钱与资源承诺 / 对外承诺与合规 / 需用户提供的凭据 / 无客观优劣的偏好 / 影响面超出本平台)。**其余一律自决**(含部署上线、重启、改配置),事后一句「我选了什么(可推翻)」(**U20 / X9**)。
> ⚠️ 两条闸**对称**:闸 1 治「**越界动手**」,闸 2 治「**过度上抛**」—— 2026-09-13 两类各犯过一次。
| 判定问题 | 若答案是 | 处置 |
|---|---|---|
| 这次是**扩大**还是**收窄**可见面/权限?(R5) | 扩大 | **停手**,出「权限影响评估」四问 + 等确认 |
| | 收窄 | 可直接做,但**遮蔽类必须真启动一次实例验证** |
| 有没有**不扩大也能实现**的方案? | 有 | **必须先提**;提不出才说明为什么没有 |
| 改动**影响多少文件**?(R7) | >10 或有"所有/整个/全库" | 先出**受影响清单** + 等确认;先 1 个对象单点验证 |
| 会不会**中断在线用户**?(R8) | 会 | 先说明影响面 + 取得确认;能避开活跃时段就避开 |
| 会不会**中断在线用户**?(R8)——**按实际影响面判,不按动作名字判** | **不会**(只换包 / 传产物 / 改静态页 / 投放候选池) | **属边界内的「部署与同步」⇒ 做完即上线,不要问**(U20 / X9);先做完,再用一句「我选了什么」交代 |
| 这个改动**失败**会怎样? | 实例起不来 / 全站不可达 / 不可逆 | 必须有回滚点(备份路径 + 恢复命令)**先就位** |
| **删除/迁移**的收益 vs 潜在破坏? | 收益小、破坏大 | **不删**——标注废弃 + 禁再加功能 |
| **这个改动会让项目某一维度净变差吗?**(**R11 · 十维**:目标/方向/架构/功能/性能/安全/交互/UI/便利性/扩展性) | **会** | **立即停下复盘** → 找保住正向收益的做法;**拿不出 ⇒ 立即停止、只报告**(不许"先做着看",见 **U28**) |
| 会不会造出**第二个漂移源**? | 会 | 改成**指针**(单一来源)——清单/待办/部署事实各只有一个权威文件 |
### 4.2 「确认最优解」十问(拍板前逐条答,答不出就是还没想清)
1. **一句话目标**是什么?做完了**没有**(可判定)?
2. 这条结论我**用什么命令/证据**证明?(说不出 = 还在推断,见 A1)
3. 有哪 **≥2 个选项**、以及**"不做"**这一条?(A2)
4. 这个改动是**扩大**还是**收窄**?(U8 / R5)
5. 有没有**更小**的改动达到同样目的?(A6)
6. 会影响**谁**(全部租户 / 单租户 / 仅 admin)?会**断多久**?(R8)
7. **回滚**怎么做?备份在哪?(写得出可执行命令才算数)
8. 造出**第二个真相源**了吗?(单一来源原则)
9. 用户**能不能感知到**(新入口 / 新反馈 / 新文案),还是只有后端变了?(U5)
10. 谁来**验收**?我能不能给一个**第三方可复现**的命令 + 期望输出 + 退出码?
### 4.3 验收口径分级(越靠后越权威)
| 级别 | 手段 | 能证明什么 |
|---|---|---|
| L1 推断 | 读代码 / 读文档 | **什么都不能证明**(只用来生成假设) |
| L2 命令 | `curl` 状态码 / `--dump-config` / 日志 grep | 机制是否被触达(**注意 `--dump-config` 不反映 bundle/patch 层**) |
| L3 对账 | md5 双端 / `docs-sync-check.sh` / 可见面清单 diff / 工具清单前后对比 | 一致性、无回归 |
| L4 端到端 | 铸造临时 session 实跑 → 越界用例(`../../etc/passwd` 应 400) | 安全与功能闭环 |
| L5 用户实测 | 用户浏览器硬刷新 + 体感确认 | **最终验收**(前端渲染、动画、位置类只能到此为止) |
> **做不到就明说**:「未做浏览器渲染验证(本机无 Chromium,装它成本不成比例)→ 待用户硬刷新确认」是**合格交付**;把 L1 说成 L5 才是事故。
### 4.4 技术实现的默认裁决顺序(**AI 自主用,不问用户**)
> 这是「技术实现找最优解」的可执行算法:**按序自答,第一个"是"就是答案**。全部答"否"才说明确实需要新造东西。
> 配套 `dsh-feature-first`:用户在技术层没有判断依据 → 这 8 条**不构成决策点**,不要上抛。
| 序 | 自问 | 若"是" → 选它 |
|---|---|---|
| **1** | 有没有**既有机制 / 扩展点**能复用? | 扩展,**不新建**(U2:不做两套) |
| **2** | 有没有**更小改动**达到同一效果? | 取**最小改动半径**(A6) |
| **3** | 能不能**用配置解决**而不是改代码?(cordis patch / env / bundle 声明) | **配置优先** —— 不改码 = 免 build、免重启 |
| **4** | 能不能**只改一层**?(门户静态页 / 编排器 / profile / bwrap / nginx) | **单层**;绝不"跨层顺手改" |
| **5** | 失败代价**是否对称**? | 不对称 → 选**可回滚、可保留**的那条(删除/迁移类尤其,A8);**删除类先走 A14「删除前三问」** |
| **6** | 结果**能不能被验证**?(命令 / 日志标记 / 端点) | 不可验证 → **顺手加可 grep 的标记或端点**(A5) |
| **7** | 这个改动是**收窄**还是**扩大**可见面 / 权限? | 收窄可直接做;**扩大 → 回 R5 门禁,先取得确认** |
| **8** | 与**官方契约的耦合面**有多大? | 耦合越小越好;**每一处耦合都要写进升级回归清单**(档案 26 六类) |
**两条硬约束**:
1. **第 3 条优先于第 2 条** —— "能配置就不改码":改码要走 build + 重启,而重启**会中断在线用户**(触及 R8);配置改动(尤其静态页)常常即时生效。
2. **任一条与红线冲突 → 红线赢**。裁决顺序是在红线之内找最优,不是绕过红线。
**产出**:把 1–8 的答案写进档案的「技术选择」段(一行一条,**含被否决的选项与否决理由**)。
→ 这样用户**事后可推翻**、但**事前不被打扰**(对应 `dsh-feature-first` §5 的"默认自主 + 事后可推翻")。
---
## 5. 决策流程十步(新需求到手照这个走)
| 步 | 动作 | 产出 |
|---|---|---|
| 1 | **判类型**:这是「方案请求」还是「任务明确直接执行」? | 方案请求 → 只输出方案,**未经明确授权不改文件** |
| 2 | **开工前置检查**:先看已有技能/资产/记忆,"本机已有的能否满足?" | 决定"扩展还是新建"(→ U2) |
| 3 | **取证**:源码级 / 命令级实证,先拿真实失败请求 | 事实清单(每条带命令 + 期望输出) |
| 4 | **判方向**:扩大 / 收窄 / 中性 | 扩大 → 立即停手出四问 |
| 5 | **出选项表**:≥2 选项 + "不做" + 影响/风险 + 推荐项 | 方案对比表 |
| 6 | **选路径**:优先"最小代价合法路径""能一行代码就不要改平台""删除类判代价对称性" | 推荐方案 + 被否决方案的否决理由 |
| 7 | **列决策点**:已定的标"已定(谁定的/依据)",未定的标"开跑前问用户" | 3–5 个决策点 |
| 8 | **用户拍板** | 决策语言落成文字(见 §7) |
| 9 | **小步落地 + 回滚点就位**(单点验证 → 再推广) | 备份路径 + 恢复命令 |
| 10 | **可复现验收 + 沉淀** | 档案(需求→改动→验证→红线→回滚)+ 红线/技能/记忆三层沉淀 |
> **第 1 步和第 4 步是"停止点"** —— 这两步没结论就不要往下走。
---
## 6. 交互 / UI 改造专项决策清单
> 用户对本项目的界面要求有明确取向,以下是**已验证有效的取向**(源自档案 16/31/36/45/56/57/59/60/61/62 + `06-工作台UI规范`)。
### 6.1 反馈与状态(最高频痛点)
| 场景 | 必须做到 | 实例 |
|---|---|---|
| 有任何等待(实例启动 / 拉目录 / 上传) | **可见进度**,且**导航路径与 XHR 路径都要有** | 档案 59:只有导航有 `wake.html` 动画,XHR 静默 20 秒 → 用户"以为死了" |
| 长耗时 | **3 秒后才亮**覆盖层(短请求不打扰) | 同上:`SLOW_MS = 3000` |
| 状态可缓存 | 页面明示「缓存更新于 X 前(6 小时内直接复用,不联网)」 | 档案 62:功能早已存在**但完全看不出来** |
| 不可编辑 / 只读 | 明示原因,不要"点了没反应" | 档案 36 |
| 失败 | 行内提示(`.toolbar-note` / `.empty`),给出**下一步** | 06 规范 §4.8 |
### 6.2 信息层级与术语
- **首列放用户读得懂的那一列**(说明 > 名称 > 技术标识);技术标识降副行(U7)
- **内部术语不上页面**:「候选池」→「导入到平台」;并显式解释易误解的关系(「**投放 ≠ 生效**」)
- **数值列右对齐 + `tabular-nums`**(否则滚动时数字跳动);超宽字段移出共享网格
- **文件名/标题默认同正文色,hover 才变蓝**(避免整列花掉)
- **图表类改动先看 `06-工作台UI规范`**,冲突时以 `06` 的**实测 Token** 为准
### 6.3 布局与滚动(踩过的坑)
- `main#view` 高度 = `calc(100vh - 55px)`(**55 是实测值**,写 54 会多出 1px 第二层滚动条)
- **"视口自适应高度"必须上下都算** —— 档案 61 第一次修正只算了列表上方 372px,漏了下方 116px(按钮行 50 + card padding 18 + `#view` padding-bottom 48)→ 仍然溢出。正解 `calc(100vh - 520px)`
- `.modal-mask` 是 `display:flex` → **必须显式写** `[hidden] { display: none }`,否则弹窗关不掉
- 同名类二次定义会互相覆盖(`.tab` 有胶囊式与下划线式两套)→ 新页面**另起类名**(如 `.pg-tab`)
### 6.4 危险与不可逆操作
- 确认弹窗 **必须写明后果**(「将重启实例,会话可能中断」)
- 危险按钮用 `--danger` 系;保存/查看类靠**底色深浅**区分(保存=浅灰底深字 `.btn-import`,查看=白底蓝字 `.btn-view`)
- **不静默提升权限**:档位过时只**提示**,不自动改(安全语义变更必须用户知情)→ 档案 45/56
### 6.5 UI 决策的验收口径
- 静态文件(`web/*.html`)改完**立即生效、无需重启**;只有 `src/**` 才 build + restart
- 改完 `node --check` 内联 JS;用**独立 headless Chrome**(`--headless=new` + 独立 profile + CDP)验证,**绝不碰用户日常浏览器**
- **`section` 名是运行时注册的,`curl` 抓不到** → 这类改动只能靠用户硬刷新确认,**如实标注"未做浏览器渲染验证"**
---
## 7. 决策语言对照表(用户怎么说 → 怎么落)
| 用户的话 | 真实含义 | 该怎么落 |
|---|---|---|
| 「按建议处理」 | **只授权那条建议本身** | 不做顺带优化;范围外的发现**先报告**(R7) |
| 「是不是最优方案 / 还有更好的方式吗」 | 要**选项对比 + 依据 + 风险** | 出对比表 + 十问,不要直接开干 |
| 「一次性优化到位」 | 同主题**做透**,别留尾巴 | 一次列全所有子项,附完成清单 |
| 「不需要 / 是不需要的」 | 先**扫引用与依赖**再定范围 | 出"引用实测表",只剔真 0 引用的(U3) |
| 「暂缓 / 还没准备好」 | **parked** | 保留侦察结论 + 列出"需你先办的事",**不再推进,等用户主动提起** |
| 「确认开始执行」 | 决策已定,**可以动刀** | 立即执行;但 R7/R8 门禁仍生效 |
| 「为什么要等我确认才部署呢」 | **部署 / 上线属执行细节** —— 不打断用户的动作不该上抛 | 直接做完上线,让用户**看线上效果**判断需求是否被满足(U20 / X9) |
| 「需要告诉我的是 是否已实现,如果未实现:为什么不能,需要我拍板可以问我」 | 要**结论三件套**,不要过程与自省 | 用 **U21** 四节骨架:判定 → 为什么不行(分层)→ 需你拍板(真需要才写)→ 我接着做(陈述句)|
| 「谁让你去改这个的」 | **越界了** —— 动到了不是我 lane 的东西 | 立即停手 → 能撤就撤(并自证不依赖它)→ 报告;此后动手前先过 **U22 闸 1** |
| 「推送 / 提交」 | 明确授权 git 操作 | 此前一律**不 commit 不 push**;提交也只用定向 `git add` |
| 一行指令(无上下文) | 期望**自主拆解 + 排查到底** | 自己建任务链、自己做根因定位,别逐步问 |
| 「为什么…?」(问现象) | 要**根因链**,不是复述现象 | 先取证(真实请求/日志/实测数),再给"现象→根因→修法"三段 |
| 「有没有越过红线」 | 要**逐条对照全量红线**的结论 | 分"当时成文口径 / 新立口径"两种口径答(A13)|
---
## 8. 决策记录模板(写进档案的固定段落)
```markdown
# <NN>-<标题>(<日期> 落地 / 调研)
- 日期: / - 状态:✅已完成|🔄进行中|⏸暂缓|❌关闭 / - 触发:<用户原话或原始报障>
> **TL;DR**|结论 / 关键 / 状态(紧贴头部,3 行内)
## 背景与动机 # 需求来源、真实失败请求、触发场景
## 用户决策 # 关键分叉 + 选择 + 理由 + 日期(原文引用优先)
## 方案对比 # 表格:方案 / 内容 / 判定 / 理由(含"不做")
## 实现 # 改动文件清单 + commit + 关键片段
## 验证记录 # 命令 + 输出 + 结论;**含失败尝试与假阴性**
## 事故 / 踩坑记录 # 现象 → 根因 → 规避
## 回滚 / 注意 # 回滚命令、副作用、后续待办
```
**三层沉淀(每次决策收口都要做)**:
1. **治本层**:机制与流程固化进 skill,或写进 `04-调整方案/NN-*.md` 改造档案(可复用的留 skill,一次性的归档)
2. **失误层**:教训 append 到 `.workbuddy/memory/YYYY-MM-DD.md`(**append-only**,不改写历史)
3. **记忆层**:长期约定/红线写 `MEMORY.md`(工作区级 + 用户级)
---
## 9. 与其他约束的关系
- **红线优先级高于本技能**:R1 不自动升级 dsh|R2 不改官方主程序与缓存|R3 client bundle 禁 `exports.default`|R4 不用真实账号做登录测试|**R5 权限只准收窄**|**R7 禁未经确认的批量/全仓写入**|**R8 中断在线用户须先知会** —— 任一条命中,**先停手**,本技能的效率论证不构成豁免。
- **执行层**:决策定了之后走 `dsh-change-workflow`(六阶段 + 档案模板 + 并行调度协议)。
- **规划/执行分离**:本技能属**规划侧**;产出交接单(`dsh-server-docs/交接单/`,8 段必填),规划会话**不 ssh、不改码、不重启、不 scp**。
- **单一来源**:清单与状态 = `INDEX.md §二`;待办 = `03-路线图与待办.md`;部署事实 = `DEPLOY-本部署.md`;UI 基线 = `06-工作台UI规范.md`;现行事实 = `BRIEF.md`(首读)。
- **可复跑判定工具**:`docs-audit.py`(歧义/编号/悬空引用,退出码非 0 即需处理)、`docs-sync-check.sh`(双端对账)、`docs-manifest.py`(机读清单)、`handoff-guard.sh`(并发预检,推送前 `PUSH=1`)。
---
## 附:本技能的自检(用完之后问自己)
1. 我有没有**先取证再下结论**?(A1)
2. 我给的方案里有没有**"不做"**这一条?(A2)
3. 未定的决策点,我是**问用户**了还是自己替他定了?(A3)
4. 我的"最优解"能通过 §4.2 十问吗?
5. 我的验收口径是 L1 还是 L5?**有没有把推断说成实测**?
6. 这次操作**会不会命中红线 R5/R7/R8**?
7. 收口时**三层沉淀**做了吗?有没有造出第二个漂移源?
8. 技术实现我是**按 §4.4 的裁决顺序**自己定的吗?还是又把技术选项拿去问用户了?(若问了 → 违反 `dsh-feature-first`)
9. 我这次「停手等确认」,是按**实际影响面**判的吗?还是只看到「部署 / 上线 / 生产」这类**词**就触发了门禁?(X9)
10. 我的答复**第一句**是在答用户问的那件事吗?有没有把「我的失误 / 进度 / 计划」写在前两节?(X10 / U21)
11. 动手前我过「**两条总闸**」了吗?—— ① **这落在谁的 lane**(不是我的 → 只报告,**X11**)② **命中真门禁了吗**(没命中 → 自己拍,别问,**U20 / X9**)
12. 这条回复**能被扫吗**?—— 排版按 `dsh-feature-first §5.4`(首屏 3 行给判定 · 每节 ≤7 行 · 加粗只留关键词 · 表格 ≤5 列 · 一条信息只说一次)
@@ -0,0 +1,165 @@
# 素材库 · AI 的有效决策(A1–A22)
> **归属**:技能 `dsh-decision-method` 的素材库(**按需读**,不是每次都要读)。
> **主文件 / 索引 / 判定核心** = `../SKILL.md`(§4 判定核心 · §5 流程 · §7 语言表 · 附 自检)。
> **用法**:只在「要判某条是否属于既有口径」或「要引用用户原话」时读本文件;**别整段抄进答复**。
> **维护**:条目**只增不改**(编号进位到末尾);用户原话**逐字**引用;每条必须带「实例出处 + 判据」。
---
## 2. 决策素材库 · AI 的有效决策(A1–A16)
> 这些是**被实践证明有效的推理方式**,不是结论。新任务遇到同类岔路时直接套用。
### A1|取证优先于推断:源码级 / 命令级,禁止只靠文档
- **实例**:`--dump-config` 被证伪(它**不反映** bundle/profile patch 层,连已生效的行都不显示)→ 判定口径改为「加载标记 + 浏览器实测」;「手放 node_modules ≠ 安装」(lockfile 才是账本);「空闲回收」**从未生效**(全历史 `idle-reap` 仅 1 次)
- **做法** → 结论前先问"这条我能用什么命令证明",写成**命令 + 期望输出**;文档只当线索
### A2|方案对比表:≥2 选项 + 影响/风险 + 建议,**必须含"不做"**
- **实例**:档案 20 方案 A(加固,推荐)/ B(启 enablePatch,不推荐);档案 45 方案 A(会话迁移,不做)/ B(提示,采用);档案 16 §四 folder_plugins(查明后**建议废弃**)
- **做法** → 表格四列:方案 / 内容 / 判定 / 理由;**推荐项置首并标注 "(Recommended)"**;给不出"不做"这条路说明分析还没做完
### A3|决策点显式列出,等用户拍板(不替用户决定)
- **实例**:交接单 8 段里第 4 段就是「决策点」——**已定的写"已定(谁定的/依据)",未定的写"开跑前问用户"**
- **做法** → 一单最多留 3–5 个决策点,每个给推荐项 + 代价;**未定的决策点不允许执行会话自行拍板**
### A4|能用 A/B 实测就 A/B,别写"应该会更快"
- **实例**:`NODE_COMPILE_CACHE` 冷启动 **5.0s → 4.0s**(实测);反向教训也记:`--max-old-space-size` **不降稳态内存**(优化前后 cgroup 都 ~98 MiB)→ 它买的是**可诊断性**,不是省内存
- **做法** → 性能/资源类结论必须给**前后两个数**;说不出第二个数就明说"未实测"
### A5|把"不可验证"改造成"可验证"
- **实例**:给插件加**加载标记** `[workspace-scoped-picker] loaded root=…` → 把升级回归(C4)从"看 UI"变成"看日志";`GET /api/capabilities` 人机同源
- **做法** → 一个改动如果只能靠"看起来生效了",就**顺手加一行可 grep 的日志/标记/端点** —— 这是最便宜的可验证性投资
### A6|选「最小代价的合法路径」,不选「最彻底的」
- **实例**:档案 45 —— 根治方案是**改写存量会话的种子事件**(属 R5「扩大」+ 多帧 zstd append-only 日志重写,风险收益不成比例)→ 改选**一句提示文案**;档案 42 —— 想禁 `python3.6` 的"遮蔽"会让实例起不来 → 改选**文档引导**(成本 0、风险 0)
- **判据** → 问三句:① 有没有更小的改动达到同样目的?② 这个改动**扩大**了吗?③ 失败时的后果对称吗?
- ⚠️ **边界(U27)**:A6 只约束**路径**(实现取最小代价),**不约束目标** —— 不许把"选最小代价"读成"降低目标 / 延期 / 静默兜底"。**目标不打折,路径取最小代价。**
### A7|能一行代码解决,就不要改平台配置
- **实例**:实例内 Python 抓 HTTPS 报 CA 错 → 正解是脚本里 `SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt`,**不走 R5 注入 env**(因为注入 env = 扩大,而一行代码就够了)
- **判据** → 平台级改动(env / 挂载 / nft / 配额)是**最后手段**,不是第一手段
### A8|遇到"要删/要迁移"先判代价对称性;不对称 → 保留
- **实例**:`folder_plugins` / `workspaces` 表**加废弃注释保留**(跨 10 文件 + 含 k8s/PG 未验证路径 → 删除风险不对称);4 份 cold 档案不迁 archive(收益小);技能包裁剪则相反 —— 真 0 引用的才剔
- **判据** → 删除的**收益 < 潜在破坏**时,选择"标注废弃 + 禁止再加功能",而不是删
### A9|明确「不做」并写明「打开条件」
- **实例**:白名单源码安装 **不做**(与"平台不执行第三方构建脚本"红线冲突)→ 同时留「打开条件清单」(沙箱构建 / `--ignore-scripts` / 仅 admin / 审计 / 全量扫描);熔断实测、管理类插件化也都明确关闭
- **做法** → 「关闭」必须写两句:**为什么不做** + **什么条件下可以重开**;只写"不做"会在下次被重新提起
### A10|失败面要留证据,不要吞错误
- **实例**:插件探活失败**回传 dsh 真实错误**(`duplicate loader entry id` / `ERR_MODULE_NOT_FOUND`),不要泛化成"插件不兼容";坏包隔离并改名标注 `-BAD-missing-index.tgz`
- **判据** → 报错信息是 admin 判断根因的**唯一线索**,泛化等于毁掉线索
### A11|不确定的能力显式抛 `unsupported`,不假装支持
- **实例**:`K8sUserFs.readFile` **显式抛 `unsupported`**(sidecar 尚无端点)→ 「属未验证路径,**不假装支持**」
- **判据** → 本地验证过的能力才写"支持";没验证的路径要么标注"未验证",要么直接拒绝
### A12|只读核对也要留痕(不改也要出结论)
- **实例**:所有"核查 / 取证 / 评估"类任务都产出档案(编号 + 状态 + 触发 + 结论表),即使**一行代码都没改**
- **判据** → 「核查完成」本身就是一个交付物;不落档的结论等于没发生(下次会重复发现)
### A13|自查要分「当时成文的口径」与「新立的口径」
- **实例**:越界自查结论 = 按当时的 R1-R6 **未违反**;按当天新立的 R7/R8 则**5 次实质越界** —— 两种口径都给出,不粉饰
- **判据** → 复盘/自查时**说明用的是哪一版标准**,否则结论不可信
### A14|删除 / 移除 / 下线前,先查清楚再动手(用户 2026-09-12 明确)
- **实例(我的失手)**:我把服务器上两个文件报成"多余、待用户定是否删除",却**没先看它们是什么** —— 一查才发现三件事:
① 本机 `scripts/` 里**都有**(我只 `ls` 了根目录就说"本机没有")
② 服务器那两份**早已被并行会话清掉**(我把一个**已消失**的问题抛给了用户)
③ 顺着 `grep -rn` 找到档案 73,一句话就看清用途 —— `lock-guard-hook.py` 是 **PreToolUse 强制钩子**
- **删除前三问(没答完就不动)**:
1. **它是什么** —— 读内容 / 读关联档案的 TL;DR,**不要凭文件名猜**。
2. **被谁引用** —— `grep -rn <名> --include="*.md" --include="*.ts" --include="*.cjs"`;**全库 `find`,不要只看当前目录**。
3. **删了影响谁** —— 是否有别的会话 / 服务 / cron 在用;**是不是唯一副本**。
- **判据** → **未查明就不动**;查明后确认是"**放错位置的多余副本**"(正本在别处且 md5 一致)才可直接清理。
- 与 **A8** 配合:删除是**风险不对称**动作(删错 = 丢失,留着 = 只占空间)→ **默认保留**,除非已查清。
### A15|写状态必须带「三态 + 级别」,禁止用「支持 / 可用」描述未验证项
- **实例**:开源文档把未验证的 Kubernetes 模式写成**方案 B** → 用户纠正:「模式 B · Kubernetes **去掉这个,根本没验证**,应该是**待开发验证**」。
- **三态词表(强制)**:
| 写法 | 什么时候能用 | 必须附 |
|---|---|---|
| ✅ **已验证** | 有**可复跑**的命令 / 日志 / 截图,且**取数时间**明确 | 命令 + 期望输出(§4.3 L2–L5) |
| ⚠️ **待开发验证** | 代码 / 文档存在,但没跑过端到端 | 「未验证」三个字必须出现在**结论句**里 |
| ❌ **已知不支持** | 实测证伪 | 失败证据 + 复现条件 |
⛔ **禁用词**:「支持 / 可用 / 已实现 / 已落地」**不得**用于 ⚠️ 段;上一轮(档案 76 §10.7)就是**用文件名代替读包**,把「没这能力」说成了结论。
- 落地:开源 README / 档案状态 / 能力清单 / 交付回执 **四处统一用这套词**。
---
### A16|回滚 / 改名 / 迁移类动作:先列「伴随物清单」,只改主体必留隐患
- **实例(同日两起)**:① **回滚只回滚了部署、没回滚源码** → 下次重建又把补丁带回来(0.2.19 误带堆限 ⇒ 0.2.20 才真修);② **改 SQLite 库名漏了 `-wal` / `-shm`** → 358 KB WAL 未被 replay、**丢了 2 条会话记录**(归位后 4→6 恢复)。
- **伴随物清单(动手前逐项打勾)**:
| 动作 | 主体 | **必须一起处理的伴随物** |
|---|---|---|
| 回滚 | 制品 / 部署 | **源码** + 构建缓存 + lockfile + 实例内已加载的 bundle |
| 改名 / 迁移 | 主文件 | **旁路文件**(SQLite `-wal` `-shm`)· 目录 · 符号链接 · 所有引用点(`grep -rn`)· 运行中的进程 / 服务 |
| 批量替换 | 命中文件 | 自引用 / 自赋值(`replace_all` 会命中刚插入的定义行)· 行尾风格 · 语法校验(用 build 当校验器) |
- **判据** → 凡「一个名字 / 一份数据 / 一段制品」被改或退回,**先写出它的伴随物清单**再动手;**改完必须读回复核**(不靠「应该没问题」)。
### A17|判「这是限制」之前先分层取证:配置项 / 已抽象层 / 真硬编码
- **实例(用户当场纠正我)**:我把「DB 是单文件 SQLite」列为集群化的限制 → 用户口径「**可以改为连接数据库集群,数据库不是限制**」;实测 `DSHS_DB_URL` 非空即切 PG(`db/index.ts:19`),`db/adapter.ts` 头注释已声明「routes depend only on this abstraction」⇒ **早有抽象,是配置项不是天花板**。
- **判据** → 任何「做不到 / 是限制 / 是天花板」的结论,先把它归到三类之一并给出**代码行号或命令**:① 配置项(改 env/参数即可)② 已抽象层(换实现后端)③ 真硬编码(必须改码)。
- 反面同时成立:**别把「能配置」当成「已经能用」** —— 同一次实测才发现 `dsh_instances` 归属表**只有 k8s 路径在写**(`LocalSpawner` 根本不收 db)⇒「换库单独做 = 零收益」。
### A18|静默失败会伪造结论:工具静默 + 降级静默,两头都要防
- **实例(同日两起,都差点骗了我)**:① `grep -v node_modules` 把**要查的行本身**也滤掉 ⇒ 得出"源仓 0 处"的**假结论**(改用 ripgrep 才看到真相);`rg` 在本机 PATH 不存在 + `2>/dev/null` ⇒ **静默返回空**,看起来像"扫干净了"。② `listCatalogProviders()` 的 readdir 抛错被 catch 吞掉返回 `[]`,注释还把降级写成特性 ⇒ **"没生效"和"没做"不可区分**,缺陷潜伏三轮。
- **判据** → **断言"扫干净了 / 没有 / 全绿"之前,先自证工具真的跑了**:跑一次已知命中的探测、看退出码、别用 `2>/dev/null` 掩盖失败;排依赖目录用 `--exclude-dir`/`.gitignore`,**不要用行过滤**。
- - ⚠️ **姊妹坑:把失败误读成成功**(2026-09-15 实证)—— 用管道截尾看命令输出时(`| grep` / `| tail`),**失败提示里也含成功关键词** ⇒ 假命中 ⇒ "以为抢到锁了"却在无锁状态改库。
✅ **判据**:**先看退出码**(不要接管道),再**复读关键状态文件/OWNER 并断言**;"输出了像成功的话"**不等于**操作成功。
**降级必须留痕**:静默 catch 至少 `console.warn` 一次,或把「不可读 / 目录为空」**透出到状态端点**(本次已落地:`/api/me/model-providers` 的 `catalog{dir,readable,count}`)。
### A19|验收判据必须包含「第二环境」——同环境反复通过会掩盖跨环境从未验证
- **实例**:写死 `/usr/local/lib/node_modules` 在本机(npm 默认 prefix)**恰好是对的** ⇒ 端到端验收"38 家全绿"只证明了"**在这一台机器上**对";换 `/usr/lib` 布局(`install.sh` 部署的机器)则厂家目录读空、兼容性预检**整体失效**、目录选择器 import 即抛 —— 且**全都静默**。同一个未验证假设被**复制了三轮**(picker → plugin-compat → model-catalog)。
- **判据** → ① 凡「读**别人**安装位置 / 版本 / 布局」的代码,路径**必须走解析层**,且**至少一条单测用假根**(已落地 `scripts/verify-dsh-install.mjs`,15 项断言);② 验收判据**显式包含"非本机布局"**(一句 env 注入即可);③ 前端验收**别用 fixture 桩**掩盖后端不可读(本次就是断言全绿而端点其实是空的)。
- **🔑 信号识别**:注释里出现「**本部署事实 / 目前是 / 暂时**」⇒ **当场转成待验证项**(那是作者自己知道这是假设的痕迹)。
### A20|存量烂账(巨量重复 / 历史遗留):选「只增不改 + 库外视图」,不就地重写
- **实例**:档案 82 = 2024 行 / 89.1 KB,切 **224 块**后 **重复标题 22 个、冗余块 190 个(≈85%)**;但「八、口径提醒」有**两个内容不同的变体** ⇒ **不是纯复制,盲目去重会丢信息**。
- **做法(零改写)**:原文**一字未改** → 文末追加「修正(日期)」小节(实测数据 + 视图路径 + 阅读建议)→ 生成**去重视图**(保留每组信息最全的一份,其余留占位注释)**只写库外** `.workbuddy/cache/dedupe-view/` → manifest 加 `dedupeView` 字段、检索命中时提示"优先读视图"。89.1 KB → 59.0 KB,**零风险**。
- **判据** → ① 先做**变体检测(同名 ≠ 同内容)**再决定能不能去重;② **写入权留在原文**(历史冻结),派生物放库外;③ 根治(拆分)与止血(视图)**分开立项**,别用一次大改解决两件事。
### A21|任何自动判据都要报「分布」;失效就改,别让它假装有区分度
- **实例**:`tier` 判据原为"被引用次数 ≥8" ⇒ **hot 44/89 ≈ 一半**(等于没筛);改为「**谁在引**」四档(hot = 被 L1/L2 现行层引 ≥3 次)⇒ **hot 7/89(8%)** ✓。⚠️ 我第一版把 **L4 台账类**也算现行层 ⇒ hot 反升到 60,**更糟**(台账会顺带列出几乎所有档案号,**索引式提及 ≠ 要读**)。
- **判据** → ① 判据上线时必须报**分布**(各档占比),一眼看出有没有区分度;② **区分"顺带列举"与"真的依赖"**(索引/台账/清单类文件的提及**不算**引用);③ 判据本身要定期体检(老判据会随规模失效)。
### A22|替换 / 退役类动作的顺序:先补位,再退役
- **实例**:univer 承担 ① AI 生成 docx/xlsx ② Office 导入导出 ③ `.univer` 协作预览(**独有**)⇒ 要弃用必须**先把 ① 换成 `dsh-office` 并验证**,再停 univer;「**顺序不能倒**」。同理 `@softspark/dsh-file-preview` 的退役**必须等平台升级(阶段 4)之后** —— 现在退 = 生产立刻失去预览。
- **判据** → 动「下线 / 替换 / 摘除」之前先写三行:**它现在承担什么**(逐项列)→ **每一项换成什么 + 验证过没有** → **换完之前不许停**。
- 与 **A16(伴随物清单)** 互补:A16 管"要一起改什么",本条管"**先做哪一步**"。
### A23|流程类失效多半不是「忘了」,而是「触发词没命中」
- **实例(2026-09-14 事故复盘,档案 95)**:改平台代码 / 铺插件 / 重启 `dshs` 时,AI **没把自己这次动作分类成"落地一次改造"** ⇒ 六阶段流程**整条不存在**(阶段 0 前置检查、阶段 2 方案确认、阶段 5 归档全缺)。自审原话:「**本技能就在本机,我一开始没加载;直到用户追问才加载**」。
- **根因是结构性的**:流程只写在**按需加载的技能**里,而常驻层 `CODEBUDDY.md §2` 自己就写着「**技能的加载由模型判断相关性,不能保证**」 ⇒ **没加载 = 没有流程**。
- **判据** → ① 凡「**动作前必须生效**」的规则,**必须写进常驻层**(项目根 `CODEBUDDY.md` / `MEMORY.md`),技能只放"需要时去拿的方法论";② 常驻层的**触发条件要用「动作词」**("要改平台代码 / 要铺插件 / 要重启服务"),**不要依赖模型自我分类**("我要做一次改造"这种判断本身就会失效);③ 自审时区分**"当时成文口径"与"新立口径"**(A13)—— 本次 95 的自审引的是**过期 R8**(要求"取得确认"),而 R8 已于 09-13 由用户改为"开发环境服务器不必等确认,只需动手前一句话说明"⇒ 两栏都要给,否则违规被高估。
### A24|拦截面必须覆盖「真实行为面」——装在工具上的闸门,拦不住正文里的行为
- **实例(2026-09-15 实测)**:昨天给「提问闸门」装了 `PreToolUse` + matcher `^AskUserQuestion$` 的 hook,想治"AI 老让用户确认简单问题"。今天核账:本工作区日志 `tool=AskUserQuestion` 调用数 = **09-12: 43 / 09-13: 3 / 09-14: 0 / 09-15: 0**(`UserQuestion`/`elicitation` 的命中全是 `--tools` 参数与 host capability 字符串,不是调用)⇒ **真实上抛几乎全在正文里,hook 从装好那天起就 0 命中**。
- **判据** → 设计任何"拦截 / 校验 / 门禁"之前,先**用日志或计数证明行为发生在哪一面**:
① 统计**该面的真实发生率**(不是"应该有");② 若闸门装面上限远低于行为面,**闸门等于装饰**(还制造"已经治好了"的假安全感);
③ 正文类行为(无法被工具 hook 拦)只能靠**常驻层的可执行自检动作**(详见 `CODEBUDDY.md §1` "回话前自检")或 **Stop hook 扫最后一条回复**。
- **可复跑的核账命令**:`grep -c "tool=AskUserQuestion" <工作区宿主日志>`(宿主日志在 `~/.workbuddy/logs/<日期>/<工作区名>__*.log`,记录 `[ToolManager] execute | tool=X`)。
### A25|本机改完 ≠ 交付 —— 先画出「改动层 → 生效链路」再宣布完成
- **实例(同类 3 次,2026-09-13/14/15)**:① 只做到**本地打包、没部署** ⇒ 用户「点开看还是和之前一样」;② 平台登录页/运行时**只改本机、从没部署到服务器** ⇒ 用户看的是旧页面(原话:「**那你看的当然还是旧页面**」)。
- **判据** → 宣布完成前逐项答三句:**① 改的是哪一层?② 这一层的生效链路是什么?③ 最后一步走了吗、在「用户可见面」验了吗?**
链路清单(缺一步都不算交付)见 `dsh-change-workflow` **阶段 5 §0「交付门禁」**表格:静态页 = scp(含 CDN 缓存坑)|平台 TS = build + restart|插件 = tgz → 候选池 → 实例启用 → **重启实例**|文档库/技能 = scp + 对账|配置 = 改 + reload/restart。
- **四条"自我安慰",一条都不算交付**:**本机改完了** · **build 通过了** · **本地打包完成** · **已 commit 了**。
- 与 **A19**(验收要含第二环境)互补:**A19 管"在哪个环境验",本条管"链路走没走完"**;与 **U20 / X9** 互补:部署本身**不必问用户**,但**必须做**。
@@ -0,0 +1,194 @@
# 素材库 · 用户的有效决策(U1–U24)
> **归属**:技能 `dsh-decision-method` 的素材库(**按需读**,不是每次都要读)。
> **主文件 / 索引 / 判定核心** = `../SKILL.md`(§4 判定核心 · §5 流程 · §7 语言表 · 附 自检)。
> **用法**:只在「要判某条是否属于既有口径」或「要引用用户原话」时读本文件;**别整段抄进答复**。
> **维护**:条目**只增不改**(编号进位到末尾);用户原话**逐字**引用;每条必须带「实例出处 + 判据」。
---
## 1. 决策素材库 · 用户的有效决策(U1–U22)
> 「有效」= 事后被证明正确、且已被落地验证。**这些是用户的稳定偏好,不是一次性指令** —— 新需求来时可以按此预判方向。
### U1|面向用户的东西只保留「用户视角」,不暴露平台内部
- **原话**:「只允许在自己的目录下创建工作区」「**不要给用户看完整路径**」;「用户就只能访问(含读取)dsh 服务用户 id 对应的那个文件夹,**连读都不要读取**」
- **落地**:picker 根固定 `<userRoot>/ws`、面包屑显示「我的工作区」、下载走平台代理不暴露宿主路径(档案 17/18/39/56)
- **判据** → 任何 UI/接口会暴露**绝对路径、uuid、内部术语、宿主目录名**的,一律收敛;收敛不需确认,扩大才需要(见 R5)
### U2|不做两套实现,能扩展就不新建
- **原话**:T01「实例内我的技能 → **扩展 business-plugins 不用做两套**」(否决了"新建 `@dsh-local/my-skills`")
- **判据** → 已有扩展点能承载(哪怕要加一个 section / 一个路由)→ **优先扩展**;新建只在"语义完全不同 + 复用会耦合"时才提
### U3|删任何东西之前先扫引用("看起来像资料"≠"没被引用")
- **原话**:「参考资料是不需要」(针对 MCN 技能包裁剪)
- **AI 的正确处置**:扫描后发现三类"像资料"的其实是被引用的**功能件** —— `nuwa-skill-main/` 主体被引为"主方法论"、`references/样例/` 被 `06_生成账号设定卡片.md:67` 写"动手前必须先读" → **保留**;只剔真 0 引用的块;且**剔 `browser-harness/` 目录必须同步改文档**,否则留死引用(档案 T03 §4.1 裁剪表)
- **判据** → 删除类需求的**第一步永远是引用扫描**(`grep -rIn` + 排除说明行),产出"引用实测表"再定取舍;**剔除目录 = 必须同改引用它的文档**
### U4|交付面越少越好(推翻架构洁癖)
- **原话**:「**业务技能要打包进插件里一起安装使用,不要分开管理**」(推翻 AI 既有的"技能走平台共享技能层"结论)
- **判据** → 用户的心智模型("我装一个插件就全有了")**优先于**架构上的"分层更干净";一个包能解决就别拆成两条投放链路
### U5|状态变化必须让用户感知到(无反馈 = 缺陷)
- **原话**:「点重连成功,但**过程中没有任何加载动画**」;「**AI 生成的文件看不到、下不了**,本机地址浏览器打不开」
- **落地**:XHR 挂起 3 秒上覆盖层(档案 59)、右下角「📁 我的文件 / 🧭 能力」、档位提示条(档案 56)
- **判据** → 后端正确但用户"看不见 / 点不动 / 不知道在等什么" = **同等优先级的功能缺陷**;凡有等待、有状态、有产出的地方都要有可见反馈
### U6|同一主题一次性做透
- **原话**:「**一次性优化到位**」;「AI 对话记录(sessions)保留时间**可以长些**」
- **判据** → 用户讨厌"打补丁式小改";同主题一次列全(如清理策略一次落 5 项);用户给方向性偏好("长些")时**自己定量再回填**,不要反复问
### U7|信息层级要站在"看得懂"的角度,而不是开发者视角
- **原话**:「**导入到候选池是什么意思**」「官方插件优先展示应该是**插件说明或中文名称**,需要**一眼知道这个插件是干什么的**」
- **落地**:白名单首列由插件名改**中文说明**(2 行 clamp),技术标识降副行;「候选池」→「导入到平台」;加一句「**投放 ≠ 生效**」的说明(档案 31)
- **判据** → 表格/列表首列放**用户能读懂的那一列**;内部术语不得直接上页面
### U8|权限方向只准收窄,扩大必须先确认
- **原话**:(R5 由来)用户明确划定:收窄可直接做,**扩大一律先出「权限影响评估」四问**
- **判据** → 任何改动先答「这次是**扩大**还是**收窄**」;命中扩大(新增挂载 / 放开遮蔽 / 暴露平台目录或 env / 放宽 ALLOWED_ENV / 放松 nft / 提档位 / 新增可读写路径 / 让 root 执行链路的对象变用户可控)→ **停手等确认**
### U9|明确「暂缓」也是决策
- **实例**:档案 21 后续(禁 HMR / catch-all vhost)「用户已决定暂缓」;域名切换侦察后用户说"还没准备好" → **parked,不再推进,等用户主动提起**
- **判据** → 前置条件在用户侧(备案 / 账号 / 供应商)时,**给出"需你先办的三件事"然后 parked**,不要反复追问或硬推
### U10|术语统一只改用户可见的那一层
- **原话**:「将所有**业务插件**字段改为**功能插件**」→ 后来「将**功能插件**改为**功能管理**」
- **AI 的正确收窄**:只改**显示文案/label**(zh+en 同步),**代码标识符、表名、包名、API 路径、文件名一律不动**;同一 locale 里描述"插件这个事物"的句子也**刻意不改**(分区叫「功能管理」,管的对象仍叫「功能插件」)
- **判据** → 术语需求 = **label 级改动**;一旦要动标识符就是大范围重命名 → 回到 R7 先出清单
### U11|用户给的数值即使偏离规范也是硬约束
- **原话**:「插件管理的官方插件管理 列表页高度增加 和页面底部间隔 **200PX** 即可」
- **落地**:`#view.page-plugins { padding-bottom: 200px }`,**代码注释注明**「`06-工作台UI规范` 常规区间是 14~24px,**200px 是用户明确指定的例外**」(档案 61)
- **判据** → 用户点名数值 → **照做 + 注释固化来源**,防止日后被"按规范修正"回去;与规范冲突时在注释里写清是例外
### U12|危险操作要确认弹窗 + 明说后果
- **原话/定稿**(档案 16 §2.2):批量启用禁用 → **必须点确认弹窗**,弹窗内写明「**将重启实例,会话可能中断**」
- **判据** → 任何会打断用户当前状态的操作(重启实例 / 停会话 / 清数据)→ 前端确认 + 明写后果;后端配合「先备份再执行」
---
### U13–U19|2026-09-12 用原始会话转录回补核对后新增
> 前 12 条来自**档案 + 工作日志**(会话的结构化沉淀)。这 7 条是拿 `scripts/extract-user-voice.py` 抽出
> **180 条用户真实发言**(跨 09-08→09-12、5 个会话)逐条比对后**补上的漏项**。
### U13|我报给用户的数字,会被他当作事实基础
- **实例**:09-12 00:06 用户直接引用我的读数再追问 ——「当前实况:2 个实例在跑,占用 **212 MiB** 和 **329 MiB**:为什么一个用户要占用这么多内存」
- **判据** → **报数必须准确、并带取数时间与口径**;分母/单位错一次就会被当成事实传下去(档案 58 曾把 `systemd` 的**字节**当 KB,读数放大 1024 倍)
### U14|判断「该不该留」用的判据是「**有没有复用价值**」
- **原话**:「是不是搞错了,**AI 生成的代码和脚本等才是需要重点定期清理的**」「这些脚本基本都是根据某个对话任务产生,**没有复用价值**」
- **判据** → 取舍类需求**先问"这东西有没有复用价值"**,而不是问"它看起来像什么"(对照 U3:先扫引用)
### U15|用户会盯**产品级容量约束**(能撑多少人)
- **原话**:「这个应该属于**控制用户上限**,不能让 10 个用户注册每个用户体验都差」
- **判据** → 用户提需求时**隐含容量期望**;凡涉及常驻资源 / 并发 / 注册量,**主动把容量影响算出来讲清**(引用档案 58 的实测:2 vCPU / 1870 MiB,并发上限 ≈ 3 实例)
### U16|用户给的「实现建议」是**意图表达**,不是硬要求
- **实例**:09-12 09:39 用户提「检测鼠标移动/焦点自动拉起实例」→ 被论证「同时动鼠标会 OOM」后**他接受了否决**,并把话题转到容量上限(U15)
- **判据** → 可以(也应该)用更优方案替代,**但必须说明为什么否决**;不要为了"照做"而实现一个坏方案
### U17|用户会充当**执行手** —— 必须给可直接粘贴的完整命令
- **原话**:「还需要建立 ssh 链接 **端口号 32022**」「我的意思有些操作需要 ssh 执行」「**给我个命令去服务器上执行就行**」
- **判据** → 凡需要用户动手的(服务器命令 / 浏览器验证 / 关应用再执行),**给一条完整可粘贴的命令 + 期望输出**,不要让用户拼命令;需要他关掉某程序时,要说清"为什么要关"
### U18|用户会明确「关闭」一条线 —— 关闭后不再推进
- **原话**:「**忘记这个项目把**」(否决 my-deepseek-harness 二开路线);「**还没准备好 后面再说**」(域名切换 parked);「**档案 21(卡顿/流式)可以展缓**」
- **判据** → 看到这类措辞**立即 parked**:保留已得结论、不再追问、不列入待办;**等用户主动提起**(同 U9,但这条是"用户主动关",U9 是"AI 建议暂缓")
### U19|用户大量时间花在**平台能力认知**类提问上
- **原话**:「dsh 插件开发 不同的插件 代码都是独立的吗」「一个插件的功能都在一个文件夹中吗,是否可以像 SKILL 管理一样做一个插件管理页面」「插件可以热更新 热加载吗」
- **判据** → 这类"**是什么 / 能不能**"的往返,**应由「能力清单」类交付物一次性消除**(档案 56 的 `platform-capabilities` 共享技能 + 实例页「能力」面板就是为此而生);**每做一次能力变更,同步更新能力清单**
### U20|「不打断用户的上线」不要问 —— 部署 / 上线属执行细节(2026-09-13 用户纠正)
- **原话**:「**为什么要等我确认才部署呢,我看线上效果才知道是否满足需求**」
- **背景(真实失手)**:v0.2.8 已打好包,`ensure-biz-plugins.cjs --all` **只换 profile 里的包**(不重启、不停实例、**零中断**),AI 却按 R7「只做被明确要求的事」**停下等确认** ⇒ 用户看不到线上效果,**无法判断需求是否被满足**。
- **判据** → **红线的触发按「实际影响面」判,不按「动作名字」判**:
- R8 的**唯一**触发条件 = **会中断在线用户**(重启 `dshs` / 停实例 scope / drain / 改实例配额·env / 改 nginx·nft);
- **不中断**的上线(传产物到 `/opt/dsh/artifacts`、换 profile 包、改静态页、候选池投放)= 边界内已列的「**部署与同步**」⇒ **做完即上线**,事后一句「我选了什么(可推翻)」交代即可。
- **与 R7 的边界**:R7 管的是「**未经确认的批量 / 全仓写入**」和「**范围外的额外改动**」,**不管「该不该上线」**;把 R7 的精神套到上线动作上 = **过度套用**(见 X9)。
### U21|答复要「结论三件套」:是否已实现 → 为什么不行 → 要拍板什么(2026-09-13 用户纠正)
- **原话**:「**需要告诉我的是 是否已实现,如果未实现:为什么不能,需要我拍板可以问我**」
- **背景(真实失手)**:AI 用「我自己的失误(一并交代)+ 探针怎么被污染 + 版本流水(0.2.19/0.2.20/0.2.23)+ 下一步三步计划」回答了「是否已实现」——用户要的结论**在第 5 段之后**才出现;收尾还问「要我现在接着做,说一声即可」。
- **判据** → 回答「是否 / 能不能 / 为什么不行」类提问,**固定四节、顺序不变、没有的节整节删掉**:
① **判定**(✅ 已实现 / ⚠️ 部分可用 / ❌ 未实现 + 一句话;用户列了多项就逐项给)
② **为什么不行**(按层:已排除的原因 → 当前唯一卡点,最多 3 层)
③ **我接着做**(**陈述句**,不是征询句)
④ **需要你拍板**(**必须是整条回复的最后一节** —— 它后面不许再有任何节;**真需要才写**,不需要就删掉整节)
- **四条铁律**:主位必须是**用户问的那件事**(AI 的进度 / 失误 / 计划不得占前两节)· 结论层**零技术标识** · **禁征询式收尾**(已定的下一步直接做)· **待拍板项置末**。
- **修正(2026-09-15 用户明令 · 完整原话)**:「**能根据决策方法 自行决策的就自决策继续处理,不能决策的问题和需确认内容放在回复的最后,按照有序段落展示**」⇒ ③④ **对调**(原为 ③ 拍板 → ④ 接着做),且该节要**逐条编号**(有序段落,不写成散文)。⚠️ 分清三件事:**自决**(能判的别问,直接做完并陈述)· **位置**(不能自决的收到最后一节)· **形态**(陈述句列「选项 + 优缺点 + 我的倾向」,⛔ 不是征询句"要不要我…" / "说一声即可")。
- **修正 2(2026-09-15 用户明令)**:「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」⇒ **上抛前先过「取舍筛」**:候选各写**优点 + 缺点** —— ① 某个**只有优点**(明显更优)或**只有缺点** ⇒ **不需要用户判断**,自己拍掉再陈述;② 只有**各有优有劣、客观标准分不出高下**(真取舍)才上抛;③ 上抛时**必须逐项列出优点与缺点**(只写"可感知差别"**不算** —— 用户原话「现在没法判断」就是这么来的)。落地载体:`dsh-feature-first §5.3 铁律 5 / §5.4 硬约束 8 / 反模式 12`。
- **修正 3(2026-09-15 用户明令)**:「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」⇒ 候选**竖排成段**(A / B / C **各占一行**),⛔ 不许写成 `A:… · B:…` 一行横排,⛔ 也不许把候选做成**表格的列**(表格是横向对比,与"段落式"正相反)。段内「优点…;缺点…」连写即可,不必每个字段再拆行(否则撞 §5.4 硬约束 3「每节 ≤7 行」)。落地载体:`dsh-feature-first §5.3 铁律 5 ④ / §5.4 硬约束 9 / 反模式 13`。⚠️ 触发它的正是**我自己的实际输出**(把三个候选写成 `A 维持现状 · B 按 50 KB 切分 · C 改成按月分片` 一行并列)。
- 落地载体:`dsh-feature-first §5.1–§5.3`(含**可复制骨架**);与 **X10**、**U20 / X9** 同族(都是「别把该自己拍的推回去」)。
### U22|只碰自己 lane 的东西;平台级 / 全局 / 别人 lane 一律只报告(2026-09-13 用户明令)
- **原话**:「**谁让你去改这个的**」+「**不是自己负责的任务相关文件不要去改**」
- **背景(真实越界)**:为打通自己任务的插件投放(撞 `ERR_PNPM_UNEXPECTED_STORE`),AI **自行创建了 `/var/lib/dshs` 全局符号链接** —— 无人授权,且属**平台级路径**。用户当场追问「谁让你去改这个的」;AI 随即自行撤销并确认实例不依赖它。
- **判据** → **动手前先问「这在谁的 lane」**:
- **我的 lane(可自己拍)**:① 我负责的插件源码 / 产物 ② 该 profile 的依赖安装 ③ 我自己的临时脚本 ④ 我方案内的执行细节(部署 / 上线 / 重启 / 改配置)
- **不是我的 lane(只报告、不动手)**:平台级路径与全局文件(`/var/lib/**`、符号链接、systemd 单元、nginx·nft)· 别人的 profile / 产物 / lane · 与本次任务无关的文件
- **与 R7 的关系**:R7 适用于「不是我的 lane」,**不适用**于「我 lane 内的执行细节」—— 两个方向都别套错(对称面见 **U20 / X9**)。
- ⚠️ **动机不构成豁免**:「**改它能让我流程跑通**」正是越界的高发动机 ⇒ 越方便越要先问「这是谁的 lane」。
---
### U23|不要重新开发:先查官方推荐库有没有现成的(2026-09-14 用户口径)
- **原话**:「**需要查官方推荐库是否有类似插件,是重新开发还是改造**」(背景:univer 太重,要"点对话里的文件在旁边窗口打开")
- **结果**:官方推荐库里拿到 **`@softspark/dsh-file-preview` v2.0.0(65 KB tgz)** ⇒ 体积约 univer 的 **1/650**,且未动 univer 一处。
- **判据** → 要加任何能力,**顺序固定**:① 本机/项目已有 ② **官方推荐库**(`Awesome-DeepSeek-Harness-Plugins`,源 = cordis.run 索引 331 个)③ npm `keywords:dsh-plugin` ④ 才考虑自研/改造。
- **候选体检三关(缺一即否)**:① inject 依赖的**官方包存在吗** ② 有没有**被角色补丁禁**的包(⇒ 静默挂死)③ `peerDependencies` 是否**覆盖我们的 dsh 版本**。反例:`dsh-file-viewer` inject 含本平台不存在的 `dsh-client-runtime` ⇒ 装上即静默挂死。
### U24|分期方案必须互相兼容(2026-09-14 用户口径)
- **原话**:「**一/二期方案必须互相兼容**」(集群化改造 Manager/Worker)
- **落地原则**:**分期只分「自动化程度与规模」,不分「机制 / 数据结构 / 协议」** —— 机制与结构**一期定死**,二期只加机器 / 加开关 / 加运维。
- **可执行判据(7 维度兼容矩阵)**:Manager 数(同代码 1..N,**禁止"必须 2 台"的硬假设**)|Worker 数(一期就走 RemoteSpawner+agent,哪怕 Worker 在本机)|存储(一期就写能力探测 + 按目录分层)|DB(**一期必须 PG**,不能先用 SQLite 顶——租约依赖 PG 原子 `UPDATE…WHERE`)|自动接管(开关默认关,但 **lease+fencing+self-fencing 一期全实现**)|备份(一期就用同一套工具/格式,只调频率)|代理路由(零改动)。
- **配套**:新增「**状态三分类**」表(用户数据 / 平台状态 / 机器基线)—— 机器基线(原生运行时、镜像、bwrap 白名单)**不能跟着用户迁移**。
### U25|锁是独占资源:抢到就必须还 ——「带锁结束」不算完成(2026-09-14 用户明令)
- **原话**:「抢到的锁一定要**执行完成后解锁**才算任务完成,**禁止抢锁执行一半不解锁就结束任务**」
- **为什么是硬规则**:锁的意义就是**同一时刻只允许一个执行会话**(本库多会话并行是常态)⇒ **带锁结束 = 把所有其他会话挡在门外**;而本库**无心跳机制**、别人**没有任何判据**能确认你已停 ⇒ 只能空等,或被人误判"已死"而违规接管(**R9** 禁止)。这正是 R9 存在的原因。
- **判据(三条配套)**:
① **抢锁前先把收口步骤列出来**(落地 → 校验 → 推送/对账 → 收尾)—— 别做到一半才发现收不完;
② **中途必须停**(等用户拍板 / 等外部窗口)⇒ **先释放锁再停**(锁是"正在动手"的凭证,不是"占位符");
③ **结束语必须对锁状态负责**:要么写明「已释放」,要么**显式点名**「锁仍在 `<OWNER>`、未释放、原因、下一步」—— 后者**仅限"释放通道不可用"这类极端情形**;⛔ 「忘了 / 做不完就走」一律不允许。
- 与 **U22(lane)** 互补:U22 管"**该不该动手**",本条管"**动手后必须收口**"。
### U26|决策中发现风险/问题 ⇒ **不许带着问题往下走**,先优化到「当前情境下的最优解」(2026-09-14/15 用户明令)
- **原话**:「**决策中发现方案有风险和问题,需要分析并优化到当前情况和状态下的最优解,然后进行下一步处理**,这个也要加入决策方法」
- **为什么是硬规则**:带着已知风险进入下一步 = 把风险**转移给未来**(届时修更贵);而"最优"不是理想方案,是**当下条件(现有资源 / 时间 / 风险面 / 能否验证)下最好的那条**。
- **判据(四步,缺一不可)**:
① **列出来**:把发现的风险/问题**逐条写成清单**(不许只在脑子里);
② **逐条处置**:每条给出**当前情境下可用**的处置 —— 能当场消掉的当场消(改设计/加缓解/降范围),**消不掉的写"残余风险 + 触发条件"**;
③ **说清残余**:哪些是"已知但接受"、为什么不接受不行、什么信号出现就必须回头看;
④ **然后**才进入下一步 —— 且在交付里把 ①②③ **一并写出来**(这就是 `dsh-feature-first §5.1` 骨架里"为什么不行 / 需要你拍板"两节的原料)。
- **配套工具**:能不能消掉要靠 **A24**(先核账真实行为面)· **A18**(静默失败会伪造结论)· **A19**(第二环境)· **A25**(本机改完≠交付)去判;**"改小范围先落地"永远是合法候选**(§4.4 第 2/3 条)。
- ⚠️ **反面**:把"有风险"当成"要不要问用户"(**过度上抛**)或者"先干着看"(**风险转移**)—— 两者都不对:**先自己优化到当下最优,再带着残余风险请用户拍板是否接受**。
### U27|要的是**解决问题**,不是**得过且过、将就妥协**(2026-09-15 用户明令)
- **原话**:「**项目推进要的是解决问题 不是得过且过,将就妥协**」
- **判据** → 面对发现的风险 / 缺陷,**默认目标是"解决"**;下面三种**都不算解决**,一律不许当成交付:
① **降级目标**(把"要做到 A"悄悄改成"做到 A′ 也行");
② **延期**("下次顺手再说 / 等窗口再补" —— 除非**客观不可逾越**且有证据);
③ **静默兜底**("先这样也能跑",而风险与触发条件一个字没写)。
- **唯一允许"暂时接受"的情形 = 客观不可逾越**(技术不可行 / 上游未支持 / 需要用户侧凭据或窗口)⇒ 必须写明**三项**:
① **卡在哪**(证据)② **当前已做到哪一步** ③ **什么条件一出现就必须回头解决**。
- **与 A6 的边界(别读成互相矛盾)**:**A6 管"路径"**(实现取最小代价、不追求最彻底);**U27 管"目标"**(目标不许打折)⇒ 一句话:**目标不打折,路径取最小代价**。
- **与 U26 的关系**:U26 要求「发现问题先优化到当下最优解再走下一步」;**U27 补的是"最优解"里不许包含'降低目标'这个选项**。
### U28|🟢 红线:只做正向迭代 —— 命中「劣化风险」立即停下复盘,无正向做法则立即停止(2026-09-15 用户明令)
- **原话**:「**要确保所有决策是让项目正向迭代和提升**,如果遇到**纯在项目劣化风险**(目标,方向,架构,功能,性能,安全,交互,UI,便利性,扩展性等)需要**立即停下复盘**,如确实**无正向迭代方法**立即**停止**,**禁止继续执行**」
- **判据(每次决策前过一遍十维)**:**目标 / 方向 / 架构 / 功能 / 性能 / 安全 / 交互 / UI / 便利性 / 扩展性** —— 任一维度**净变差**即命中。
- **命中后的三步(一步都不许跳)**:① **立即停下**(不许"做完再看")② **复盘**:写清劣化在**哪一维**、**代价量级**(数字 / 证据)③ **找正向做法**(改小范围 / 换实现 / 分阶段)—— **拿不出来 ⇒ 立即停止、只报告,禁止继续执行**。
- ⛔ **三种伪装**:把劣化说成"必要代价"/用"后续再优化"掩盖已知劣化/把劣化项藏进交付里不写。
- 与 **R5** 互补:R5 只管**权限**(只准收窄);**R11 管全维度净收益**。与 **U27**(不将就妥协)· **A20**(代价不对称就保留)同族。
@@ -0,0 +1,34 @@
# 反例库 · 被驳回 / 被纠正的决策(X1–X13)
> **归属**:技能 `dsh-decision-method` 的素材库(**按需读**,不是每次都要读)。
> **主文件 / 索引 / 判定核心** = `../SKILL.md`(§4 判定核心 · §5 流程 · §7 语言表 · 附 自检)。
> **用法**:只在「要判某条是否属于既有口径」或「要引用用户原话」时读本文件;**别整段抄进答复**。
> **维护**:条目**只增不改**(编号进位到末尾);用户原话**逐字**引用;每条必须带「实例出处 + 判据」。
---
## 3. 反例库:被驳回 / 被纠正的决策(X1–X11)
> 每条**反例 = 一条避免规则**。这些是本工作区里真实发生过的失手。
| # | 反例 | 根因 | 转为规则 |
|---|---|---|---|
| **X1** | 为"让 scp 文件行尾干净",用脚本把 **147 个文件** CRLF→LF;当期只被要求改一句 UI 字符串 | 把"顺手修"当效率;**没做单点验证就全库推广**;忘了本机镜像不是沙箱 | → **R7**:只做被明确要求的事;>10 文件先出清单;先单点验证;传播前 `git status` 比对待传清单 |
| **X2** | 档案 58/59 连续两次重启 `dshs`,**把在线用户踢下线**并引发报障 | 把"改完即验"当完整闭环,漏了"改前先知会" | → **R8**:重启/drain/铺插件/改配额 env → 先说「影响谁、断多久、为何必须现在做」 |
| **X3** | 把 mtime 当并发冲突判据 | mtime **分不清是谁改的**;本库长期不提交 → 无冲突检测 | → 冲突判定只认两个硬信号:**别人的占用锁** + **待推送清单里的未声明文件**;mtime 只作提示 |
| **X4** | 由"空闲回收没生效"外推出"实例不会中断"(**被用户当面纠正**) | 把"某机制失效"误推成"该类现象不存在" | → 现象归因要**枚举全部可能来源再逐一实测**(中断真凶是服务重启 + 崩溃重启,与回收无关) |
| **X5** | 按字面执行"参考资料不需要"去删 | "看起来像资料"与"实际被引用"是两件事 | → 见 **U3**:删除前先扫引用 |
| **X6** | 用截断到 200 字符的 `grep` 输出当 `old_string` 去 Edit 长行 → **误删对方记录行首** | 拿不完整内容当精确锚点 | → **长行 Edit 前必须先用 Read 取全文**;共享文件只用 Edit 精确替换(失败即冲突信号),禁整文件 Write |
| **X7** | 把用户设备上的"旧会话不好用"当成感受问题,未深挖 | 没做会话级取证 | → 报障第一步**先拿真实失败请求/真实状态**(`journalctl` 精确 URL+method+status),再归因 |
| **X8** | "只注入 env 就以为配好了"(bundled 技能层实际未挂载) | 漏了"插件在实例内 `resolve()` + 读盘"这一层 | → 通用判据:**插件在实例内读盘的东西,必须真的出现在命名空间里**;env 只决定"去哪儿找" |
| **X9** | **把 R7 按「动作名字」套到部署上** —— v0.2.8 只换 profile 里的包、**零中断**,却停下等确认;用户回「为什么要等我确认才部署呢」 | 红线被当成**关键词匹配**(见「部署 / 上线 / 生产」就触发门禁),没按**实际影响面**判 | → **按影响面判,不按动作名字判**(U20):R8 只认「会中断在线用户」;**不中断的上线 = 执行细节,做完即上线** |
| **X10** | **答非所问:拿「我的失误 + 探针踩坑 + 版本流水 + 下一步计划」去答「是否已实现」** | 主位错位 —— 写的是**AI 的进度**,用户问的是**功能的可用性**;收尾「要我接着做,说一声即可」= 又把已定的执行细节上抛 | → 用 **U21 结论三件套**(判定 → 为什么不行 → 要拍板什么 / 我接着做);失误只在 ①改变结论 ②用户问根因 时写(`dsh-feature-first §5.1–5.3`) |
| **X11** | **为了让自己的流程走通,自行改了平台级路径**(建 `/var/lib/dshs` 全局符号链接,无人授权)→ 用户追问「谁让你去改这个的」 | **动机取代了边界判断** —— 「改它能让我流程跑通」被当成理由,没问「这落在谁的 lane」 | → **U22 闸 1**:不是我 lane 的(平台级 / 全局 / 别人 lane)**只报告不动手**;越方便越要先问 |
---
| **X12** | **把用户给的材料当权威照抄**(他们扒的是新版 `0.1.5+` 源码,我们跑的是 `0.1.2-rc.1`) | 没先确认「**这份材料描述的是哪个版本**」 | → 用户给的素材要用**我们的实际版本**核对;本次 4 处纠正:包不存在 / 扩展点不存在 / 装上也静默挂死 / 投放通道不符。⚠️ 与 **U11**(用户给的**数值**是硬约束)区分:**数值口径是约束,事实陈述要核对** |
| **X13** | **把自己的解析失败当成"版本差异"**(报「`__DSH_BOOT__` 结构变了」,其实是我的解析正则过时) | 差异归因时**先怀疑版本、没先怀疑自己** | → 报"两版不一样"之前,**必须在两边用同一解析都跑通一次**(或做 A/B 对照);档案 90 已追加更正防误导 |
| **X14** | **把「一批改造」的中断动作拆成多次执行**(一天连做 5 项改造 ⇒ 铺插件 4 次 + 重启 `dshs` 3 次,每次都让已打开页面手里的 `rev` 过期) | 只有"改一处→验一处"的单点思维,**没有"批处理窗口"概念**(R8 只说"先说明/取得确认",没说"攒批") | → **同一批改动的所有中断动作攒到一个窗口执行**;**窗口内 >1 次重启 = 违规信号**,停下来问"能不能并到一次"。⚠️ 这正是「Failed to load plugins」的直接成因(档案 95) |
@@ -0,0 +1,54 @@
---
name: dsh-env-bootstrap
description: DSH 平台项目的「环境引导 / 搬迁」技能 —— 把**工作区常驻规则**(`CODEBUDDY.md` 的关键章节)带走,并在新电脑 / 新工作区路径下**校验与注入**,同时自检环境相关项(绝对路径、hooks 命令、代码仓、备份目录)。当用户说「换电脑了」「改了工作区路径」「迁移到新环境」「规则会不会丢」「新环境还没配好」时使用。
version: 1.0.4
updated_at: 2026-09-15
agent_created: true
---
# dsh-env-bootstrap — 环境引导(常驻规则的可移植化)
## 0. 为什么存在
项目规则分两层,**能力恰好互补、缺口也恰在这里**:
| 层 | 载体 | 特点 |
|---|---|---|
| **常驻层(权威)** | 工作区 `CODEBUDDY.md`(每会话自动注入 ⇒ **动作前必然生效**) | 它是**工作区文件** ⇒ **换电脑 / 换路径就没了** |
| **可移植层** | 本技能(用户级 `~/.workbuddy/skills/`) | 随技能走,但**不自动注入** |
⇒ 本技能把常驻层的关键章节做成**快照**,并提供 **校验 / 注入 / 环境自检**,让"换环境后规则还在"这件事**可执行、可验证**。
## 1. 命令(脚本 `scripts/resident-rules.py`)
```bash
S="<本技能目录>/scripts/resident-rules.py"
python3 "$S" --env-check # ① 环境自检:目录存在性 + hooks 命令里的绝对路径(失效 = 全机 Write/Edit 被拒)
python3 "$S" --check # ② ★校验:关键规则是否齐备(**默认只报**,rc=1 = 有缺失/漂移)
python3 "$S" --inject --init # ③ 注入:目标无标记块时**追加**(不动既有内容);有则在块内替换
python3 "$S" --snapshot # ④ 规则更新后,由权威 CODEBUDDY.md **重生成**技能内快照(单向)
python3 "$S" --goal <路径> # 可指定别的目标 CODEBUDDY.md
```
## 2. 换环境的正确顺序(**把"规则齐备"放在第一步**)
1. `--env-check` —— 先看路径 / hooks 是否失效(**hooks 绝对路径失配 = fail-closed:该机所有会话的 Write/Edit 全被拒**,2026-09-13 实测)
2. `--check` —— 关键规则缺失?⇒ `--inject --init` 追加标记块 ⇒ **人工去重**(块外原有内容与新块可能重复)⇒ 复跑 `--check` 直到 rc=0
3. 逐项核对快照文末的 **§待核清单**(服务器 / 备份目录 / 代码仓 / 工作区)—— **环境相关项不自动配、也不假装能配**
4. 规则更新时:改**权威** `CODEBUDDY.md` → `--snapshot` 重生成快照(**方向单向**)
## 3. 设计红线(别把它用成"第二真相源")
- **权威方向单向**:`CODEBUDDY.md` 是权威,快照是它的副本;**只允许 `--snapshot` 从权威生成副本**,⛔ 不许反向手改快照。
- **默认只报不改**(与文档库「体检只报不改」同规):`--check` **绝不**改任何文件。
- **注入只动标记块**:`<!-- BEGIN resident-rules … --> … <!-- END resident-rules -->` 之间;**首次不自动注入**(避免同一规则在两处并存)。
- **不假装能自动配环境**:环境相关项只做"存在性检查 + 待核清单"。
- ⚠️ **常驻层不可被"搬走"**:本技能**不替代** `CODEBUDDY.md` —— 规则仍必须常驻在**该环境**的 `CODEBUDDY.md`(否则"动作前必然生效"这条就断了);技能只是**把规则带过去并防丢**。
## 4. 自检(用完之后问自己)
1. 我改的是**权威**还是**快照**?(改快照 = 造漂移源)
2. `--check` 绿了吗?红的那条是**真缺失**还是我刚改错?
3. 环境相关项核对了吗(hooks 路径 / 代码仓 / 备份目录 / 工作区)?
4. 注入后有没有**人工去重**(块外原内容 vs 新块)?
5. 给用户的报告**能被扫吗**?—— 排版按 `dsh-feature-first §5.4`。
@@ -0,0 +1,133 @@
# 常驻规则 · 可移植快照(由 `scripts/resident-rules.py --snapshot` 生成,勿手改)
> **这是「工作区 `CODEBUDDY.md` 关键章节」的副本**,随技能走 ⇒ 换电脑 / 换工作区也能带走。
> ⚠️ **权威方向单向**:`CODEBUDDY.md` 是权威,本文件是它的副本(重生成用 `--snapshot`)。
> ⚠️ **环境相关项**(绝对路径 / IP / hooks 路径 / 工具位置)注入后**必须按新环境核对**:见文末「待核清单」。
## 骨架锚点(校验用;丢了就等于规则没了)
- `A1` — 只问「超过现有判断方法边界」的问题
- `A2` — 禁止用征询句收尾
- `A3` — 四件或七件套必跑
- `R7` — 禁未经确认的批量 / 全仓写入
- `R7-边界` — R7 只适用于「不是我的 lane」
- `R9` — 绝对禁止「人工删锁 / 接管」
- `R11` — 只做正向迭代
- `U27` — 目标不打折,路径取最小代价
- `L1` — 先抢全局执行锁
- `L2` — 锁的生命周期 = 任务的生命周期
---
## 1. 提问判据(唯一一条)
> **只问「超过现有判断方法边界」的问题。**(用户原话)
- **边界内 → 一律自决,不要问**:技术选型 / 实现路径 / 命名与数据结构 / 性能与资源调参 / 部署与同步 / 排查方法 / 版本与依赖 / 兼容与降级 / 方法内的方案取舍 / 文档与档案的技术内容。
- ⚠️ **「部署 / 上线」明确属于上面这一项**(2026-09-13 用户纠正原话:「为什么要等我确认才部署呢,**我看线上效果才知道是否满足需求**」):**不中断在线用户**的上线动作(`scp` 产物到 `/opt/dsh/artifacts` + `ensure-biz-plugins.cjs --all` 换包、静态页改动、候选池投放)**做完即上线,不要问** —— 用户要先看线上效果才能判断需求是否被满足。
- **⚠️ 2026-09-13 用户明令:「这个是开发环境服务器,不用担心中断用户」** ⇒ 上一条的「只有会中断在线用户的动作才走 R8 先知会」**改成**:重启 / 停实例 scope / drain / 改配额或 env / 改 nginx·nft **直接做**,只需**动手前一句话说明**在做什么(可回溯用)。**仍未放开**:不可逆的破坏性操作(删数据 / 迁 DB / 清目录)仍先出清单。
- **边界外 → 必须问**:① 业务目标与优先级(做不做、先做哪个)② 花钱与资源承诺 ③ 对外承诺与合规(备案 / 资质 / 合同)④ 需用户提供的账号凭据或审批 ⑤ 无客观优劣的体验偏好(审美 / 文案 / 默认值)⑥ 影响面超出本平台 ⑦ **红线门禁** ⑧ 方法确实判不准。
- **判据**:这件事有没有**客观可判的优劣**?有 → 自己定;没有 → 问用户。
⚠️ **上抛门槛 = 存在「真取舍」**(2026-09-15 用户明令):「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」⇒ 把候选各写 **优点 + 缺点**:某个**只有优点**(明显更优)或**只有缺点** ⇒ **自己拍掉**;只有**各有优有劣、客观标准分不出高下**才上抛,且**必须逐项列出优点与缺点**。
- ⛔ **不许捆包**:要问红线就**只问那一句**(要不要现在动生产 / 影响谁 / 断多久 / 能否避开);技术方案自己定好、当**已定项**陈述("我按 X 做…已定,可推翻")。**一轮最多一个问题,同类不连问两次。**
- ✅ **回话前自检(发出任何回复前过一遍 · 2026-09-15 加)** —— 起因:**「提问闸门」hook 只能拦 `AskUserQuestion` 工具,而真实的上抛大多发生在正文里**。
**实证**:本工作区日志 `tool=AskUserQuestion` 调用数 = **09-12: 43 / 09-13: 3 / 09-14: 0 / 09-15: 0**(09-14 起该工具基本不用)⇒ **hook 那条路径几乎不被走到 ⇒ 拦不到正文里的征询**。
⛔ **禁止用征询句收尾**:出现「**要我…吗 / 是否要我 / 需要我…吗 / 要不要我 / 请确认 / 你看怎么办**」时,**先按上一条判据重判三问**:
① 命中**真门禁**吗(不可逆破坏性操作 / 边界外六类)?**没命中 → 删掉这句,自己做完,改成陈述句**("我接着做 X");
② 我是不是在**把已经定下来的事再问一遍**?是 → 删;
③ 我要问的这件事,**候选之间是「真取舍」吗**(各有优有劣、客观标准分不出高下)?—— 若某个**只有优点 / 只有缺点** ⇒ **自己拍掉**;是真取舍 → 才允许问,且**一轮只问这一句**、**逐项写出优点与缺点**。
📌 需要用户拍板时:**位置 = 整条回复的最后一节**(⛔ 不许埋在中间)、**按有序段落逐条编号**、**每个候选必须写「优点 / 缺点」两栏**、**候选竖排成段**(A / B / C **各占一行**,⛔ 不横排、⛔ 不做成表格的列)(2026-09-15 用户明令:「**能根据决策方法 自行决策的就自决策继续处理,不能决策的问题和需确认内容放在回复的最后,按照有序段落展示**」+「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」+「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」);**形态 = 陈述句**(问题 + 各候选优缺点 + 我的倾向),⛔ **不是**甩征询句("要不要我继续" / "说一声即可")。
- 📐 **排版按 `dsh-feature-first §5.4`**(可扫读九条 + 形态骨架 + 十三条反模式):**首屏 3 行给判定 · 层级 ≤3 · 每节 ≤7 行 · 加粗只留关键词 · 表格 ≤5 列 · 一条信息只说一次**;**待你拍板项落在最后一节、逐条编号、每个候选带「优点 / 缺点」且竖排成段(不横排、不做成表格的列)**;执行信息 / 报障 / 提问**各有现成骨架,不新造**;细节进「技术附录」,正文只留"能决定下一步"的信息。
- 🎯 **要的是解决问题,不是将就妥协**(2026-09-15 用户明令):面对风险/缺陷**默认目标是解决**;**降级目标 / 延期 / 静默兜底**三种**都不算解决**。只有**客观不可逾越**(技术不可行 / 上游未支持 / 需你提供凭据或窗口)才允许"暂时接受",且必须写明 ① 卡在哪(证据)② 已做到哪一步 ③ **什么条件一出现必须回头解决**。⚠️ 与 §2「最小代价路径」不矛盾:**目标不打折,路径取最小代价**(细节:素材库 **U27 / U26 / A6**)。
- 🟢 **只做正向迭代**(2026-09-15 用户明令,红线 **R11**):任何改动**只要让项目在某一维度净变差**(目标/方向/架构/功能/性能/安全/交互/UI/便利性/扩展性)⇒ **立即停下复盘**;**拿不出正向做法 ⇒ 立即停止、禁止继续执行**(细节:素材库 **U28**)。
---
---
## 3. 红线 R1–R11(任一条命中 → **先停手**;效率论证不构成豁免)
| # | 红线 | 要点 |
|---|---|---|
| **R1** | 不自动升级 dsh | 升级须走独立"测试 → 评估 → 修复"流程 |
| **R2** | 不改官方 dsh 主程序与缓存 | `@deepseek-ai/dsh` **零改动**;扩展只走 profile 层官方插件机制 |
| **R3** | client bundle 禁 `exports.default` | 只导出 `apply` + `inject` |
| **R4** | 不用真实账号测登录 | 用临时 session(`mksess.cjs` 直插),用完即删 |
| **R5** | **权限只准收窄** | 凡**扩大**(新挂载 / 放开遮蔽 / 暴露平台目录或 env / 放宽 nft / 提档位)→ 先出「权限影响评估」并取得确认 |
| **R6** | 先查已有资产再动手 | 可用技能 → 本机 / 项目已有技能与记忆 → "本机已有的能否满足" |
| **R7** | **禁未经确认的批量 / 全仓写入** | **只做被明确要求的事**;额外发现的问题**先报告、后动手**;禁全库遍历改写 / 通配符重写 / 批量 `chmod`·`chown` / **批量换行符转换** / `cp -r` 整目录覆盖 / `git add -A`;**可能影响 >10 文件 → 先出清单 + 确认**;先单点验证;**本机不是沙箱**(会经 scp 传导到生产) |
| **R7-边界** | **R7 只适用于「不是我的 lane」—— 两个方向都别套错**(2026-09-13 用户两处明令) | ① **不适用于「我 lane 内的执行细节」**:部署 / 上线(换包、传产物、改静态页、候选池投放)、重启服务、改配置、跑自己的脚本、改自己的插件源码与产物 ⇒ **别拿 R7 当挡箭牌去问,直接做**,事后一句「我选了什么(可推翻)」(原话:「为什么要等我确认才部署呢」)。② **适用于「平台级 / 全局 / 别人 lane」**:`/var/lib/**`、全局符号链接、systemd 单元、nginx·nft、别人的 profile / 产物 ⇒ **一律只报告、不动手**,**哪怕改它能让自己流程跑通**(原话:「谁让你去改这个的」+「不是自己负责的任务相关文件不要去改」)|
| **R8** | **中断在线用户的生产变更须先知会** | ⚠️ **2026-09-13 用户明令修正:服务器 `47.77.182.89` 是「开发环境服务器」,不用担心中断用户** —— 重启 `dshs` / 停实例 scope / drain / 改实例配额或 env / 改 nginx·nft **均可直接做**,不必再等确认。仅保留两条最低自律:① **动手前一句话说明**在重启/停了什么(便于出问题回溯)② **破坏性且不可逆**的动作(删数据 / 迁 DB / 清目录)仍先报清单。 |
| **R9** | **⛔⛔ 绝对禁止「人工删锁 / 接管」**(用户 2026-09-12 明令:"严格禁止这类操作") | **AI 一律不得**:`rm -rf 交接单/.exec-lock`、删 `交接单/.doing-*`、或以「持有者疑似已死 / 卡住 / 太久没动 / 只在只读分析没产出」为由**单方面接管**。锁**只能由持有者自己释放**(`--release-exec` / `--release`);**`handoff-guard.sh` 输出里的「或确认接管后人工删锁」不构成授权**。抢不到锁时 AI 的**唯一**合规动作 = **停手 + 报告用户** —— **锁的处置权只属于用户本人**(要删也只能用户自己动手)。**理由**:删锁 = 在**无法验证**对方死活的前提下单方面撤销互斥(**无心跳机制,AI 没有任何判据**)→ 一旦对方仍在跑,就**退回「两个会话同时改同一批文件」**,而这正是这把锁存在的理由。 |
| **R10** | **⛔ 绝不以 root(或非该实例 uid)运行 / 触碰用户实例的东西** | **实证(2026-09-14 事故)**:为量内存**用 root 手动起 admin 的 profile** ⇒ 它以 root 写入 `home/storages/workspace.json`(0.1.5 新增的 `@deepseek-ai/dsh-workspace` 状态文件)与 `home/.dsh/mcn-plugin.db` ⇒ **属主变 root** ⇒ 实例进程(uid 114801)`EACCES` ⇒ `plugin tree failed to load` ⇒ **exitCode 1 崩溃循环 ⇒ 页面 404**。**规则**:① 对用户实例的**一切验证 / 冒烟 / 探针必须以该 uid 运行**(`setpriv --reuid <uid> --regid <uid> --clear-groups`)或**照平台姿势进 bwrap 沙箱**;⛔ **禁止 root 直跑 `dsh --profile`**。② 确需临时以 root 跑(读全局配置等)⇒ **收尾必须** `find <home> -user root` 列出 + `-exec chown <uid>:<uid> {} +` 修正。③ 实例「起不来」排查**先看属主 / EACCES**,**不要先怀疑 OOM**(本次先后误判为 OOM,绕了 20 分钟)。**修复手法(实测 1 步恢复)**:`find <home> -user root -exec chown <uid>:<uid> {} +` → 重启平台 → 页面 200。 |
| **R11** | **⛔ 只做正向迭代:命中「劣化风险」→ 立即停下复盘;确实无正向做法 → 立即停止,禁止继续执行**(2026-09-15 用户明令) | **判据(每次决策前过一遍十维)**:这个改动是否让项目**任一维度净变差** —— **目标 / 方向 / 架构 / 功能 / 性能 / 安全 / 交互 / UI / 便利性 / 扩展性**?<br>**命中 ⇒ 立即停下复盘**(不许"先做着看"、不许将就):① 写清**劣化在哪一维、代价多大**(证据 / 量级)② 找出**能保住正向收益的做法**(改小范围 / 换实现 / 分阶段)③ **拿不出正向做法 ⇒ 立即停止、不再执行,只报告**。<br>⛔ **三种伪装禁止**:把劣化说成"必要代价"/用"后续再优化"掩盖已知劣化/把劣化项藏进交付不写。<br>与 **R5**(权限只准收窄)互补 —— R5 管**权限**,R11 管**全维度净收益**;与 **U27**(不将就妥协)同源。 |
---
## 4. 提交边界
**未明确要求 → 不 commit / 不 push / 不同步仓库。** 用户说"提交 / 推送 / 同步"时才做,且**只 add 自己改的文件**。
---
## 5. 规划与执行分离
规划会话**只产出交接单**(`dsh-server-docs/交接单/`,8 段必填:目标 / 只读前置 / 范围 / 决策点 / 步骤 / 验收 / 回滚 / 回报格式),**不 ssh、不改码、不重启、不 scp**;落地交另一个执行会话(**不读规划会话的上下文**)。
---
## 6. 并发纪律(本库多会话并行是常态)
- 共享文件**只用 Edit 做精确片段替换**(失败 = 天然冲突检测),**禁整文件 Write 覆盖**。
- **同一时刻只放一个执行会话**(并行度按**冲突域**定,不按任务数);规划会话在执行期间**只读、不 scp**。
- **三把锁,顺序固定**(本平台多会话并行的唯一防线):
| 序 | 锁 | 命令 | 管什么 |
|---|---|---|---|
| ① | **全局执行锁**(粗) | `bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<你的会话名>"` | **同一时刻只允许一个执行会话**动「文档 / 代码 / 服务器」← **防"5 个会话同时改"靠的就是这一把** |
| ② | 单级占用锁(细) | `bash dsh-server-docs/scripts/handoff-guard.sh --claim <单号> "<会话名>"` | 这个单归谁做(供台账与占用声明) |
| ③ | 服务器侧操作锁 | `bash scripts/op-lock.sh claim <操作名> "<影响面:谁会被断、断多久>"` | 谁正在动**生产**(重启 / drain / 改 env·配额 / 铺插件 / 改 nginx·nft) |
**完工反序释放**:先 `--release <单>` / `op-lock.sh release`,最后 `--release-exec`。
- 🔍 **抢锁必须"校验结果",不能"看输出"**(2026-09-15 实证事故):把 `handoff-guard.sh --claim-exec` 的输出**用管道截尾**(`| grep` / `| tail`)时,**失败提示里也含关键词**(如"…必须 `--release-exec` 才算完成")⇒ `grep -q` 会**假命中** ⇒ 于是"以为抢到了"而**在无锁状态下改库**。
✅ **正确判据(二选一,缺一不可)**:① **检查退出码**(`if bash scripts/handoff-guard.sh --claim-exec "X"; then … ; fi`,不要接管道);② **复读 `交接单/.exec-lock/OWNER` 并断言等于自己的会话名**。
⛔ 事故版形态:`OUT=$(… --claim-exec … | grep 已持…)` —— **grep 吃掉了退出码,也吃掉了"抢不到"这个事实**。
- ⚠️ **「无锁」的正确读法 =「你快去抢」,不是「可以开工」** —— 2026-09-12 实证:两个会话把 guard 输出的「✓ 无全局锁」读成"环境干净"→ **同时改了本库**(无实际损害,属流程失效)。**看到"无锁" ⇒ 下一个动作就是 `--claim-exec`**;**抢到才是开工许可**。
- 强制层(可选启用):`scripts/lock-guard-hook.py` + `~/.workbuddy/settings.json` 的 hooks —— **无锁时直接拒写**(见 `04-调整方案/73`)。
- ⛔ **抢不到锁就是终点,不是待办**:**不得人工删锁、不得接管**(见 **R9**,2026-09-12 用户明令)。唯一合规动作 = 等持有者自己释放,或**报告用户、由用户本人处置**;AI 不得以任何理由替用户判断"那把锁已经可以删"。
- ✅ **锁只约束「写」,不约束「读」**(2026-09-12 用户问清):读文档 / 读代码 / 只读命令(`git status|log|diff`、`journalctl`、`ls`、`grep`、只读 ssh)**随时可做,不需要锁** —— 被挡期间照样可以查清事实再报告。
⚠️ 但**会改本地状态的命令不算"读"**:`git fetch` / `checkout` / `stash` / `reset` / `switch` 等一律要持锁(它们会写 `.git/refs` 或工作树)。
- 🔓 **释放时机 = 整个交付闭环走完,不是"改完文件就放"**:`回填台账 → 四件套校验 → commit → 推送 + 对账 → 归档` 全部结束后才 `--release-exec`(**反序**:先 `--release`,最后 `--release-exec`)。
理由:中间放锁 = 别的会话可能在你 commit 前挤进来,让**你的半成品被它的提交带走**(本库实证过这类事故)。
- ⏸️ **持锁期间若需要等用户拍板(等窗口 / 等选择)→ 先释放锁,再等**:锁是"**正在动手**"的凭证,不是"先占着"。挂着锁空转会把所有会话挡在门外;确认完再重新 `--claim-exec`。
- 🔒 **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令):**抢到锁的任务,只有"执行完成 → 反序释放"才算完成**;⛔ **禁止"抢到锁、做一半、不解锁就结束回合 / 结束会话"** —— 锁是**独占资源**,带锁结束 = 把其他所有会话挡在门外,而本库**无心跳机制**、别人**没有任何判据**能确认你已停 ⇒ 会被迫空等,或被诱去违规接管(R9)。
三条硬性配套:① **抢锁前先把收口步骤列出来**(落地 → 校验 → 推送/对账 → 收尾),别做到一半才发现收不完;② **中途必须停**(等用户拍板 / 等外部窗口)⇒ **先释放锁再停**(上一条);③ **结束语必须对锁状态负责** —— 要么写明"**已释放**",要么**显式点名"锁仍在 `<OWNER>`、未释放、原因、下一步"**(仅限"释放通道不可用"这类极端情形);⛔ **"忘了 / 做不完就走"一律不允许**。
- 推送前复跑对账:**「仅本地」里若有不在你清单里的文件 → 立刻停手**(幽灵文件);**只推自己本次改的文件**。
- 单子里的基线数字**必须带取数时间 + 复核命令**,不写死绝对值(会被并行改动打穿)。
- **`~/.workbuddy/settings.json` 的 `hooks` 段 = 多会话共享配置** —— 多个会话各自加钩子时**只能 Edit 增删条目,禁止整段覆盖**(JSON 顶层键被覆盖会**静默**抹掉别人的钩子)。
2026-09-12 实证:两处独立钩子(提问闸门 / 锁闸门)各写一份配置文档,若各自按文档落盘 → **互相覆盖**;已合并为一段(`PreToolUse` 两条 + `SessionStart` 一条)。
**同一条也适用于用户级 `~/.workbuddy/MEMORY.md`。**
- ⚠️ **钩子命令「会话启动时快照」** —— 改 `~/.workbuddy/settings.json` 里的 hook,**对已在跑的会话无效**,必须**「完全重启」(彻底退出——关窗 ≠ 退出)或新开会话**才加载。**2026-09-13 实测定论**(本会话 06:47 启动 → 06:55 改配置 → 07:05 拆掉临时目录联接后,写操作报的**仍是旧路径**;此前那版「每次调用现读」是被那支临时联接掩盖的**误判**)。
- ⚠️ **脚本路径失配 = fail-closed**:hook 打不开脚本 → 报错 → 该机**所有会话**的 Write/Edit 全被拒(09-13 实际发生,连改 `settings.json` 本身都被拦)⇒ **迁移 / 改名后第一件事 = 核对 hooks 里的绝对路径**。
- **应急兜底(本钩子有意的安全阀)**:钩子**不拦 Bash** ⇒ 路径失配期间可用 shell 写文件过渡(09-13 实际走通)。
---
## 8. 会导致事故的实测事实(**常驻,不许只给指针**)
| 事实 | 不知道会怎样 |
|---|---|
| **实例权限档位是「会话创建时播种」的** —— 既有会话**不跟随平台默认**(平台默认 `danger-full-access`) | 会误判"平台坏了";更危险的是可能**自动去改档位** —— 那等于把受限会话静默提升为完全权限,**安全语义变更必须用户知情** → 只提示 + 建议新开会话 |
| **功能插件「禁用」= `pnpm remove`(真卸载)**,不是"保留包 + disabled" | 任何**硬绑定 provider** 的覆写段在用户禁用后会指向不存在的 provider;`web.searchProvider` **单选且无回落**,多 provider 又未显式配置 → `WEB_PROVIDER_AMBIGUOUS` 报错(平台已用 `syncWebProviderPatch()` 按当前 bundles 重算解决) |
| **实例配额** = `MemoryMax 384 MiB` / `CPUQuota 150%` / `TasksMax 128`;但 **V8 堆上限按宿主物理内存算(960 MiB)而非 cgroup** | ① `systemd` 的 `MemoryCurrent`/`MemoryMax` **单位是字节、不是 KB**(按 KB 算会放大 1024 倍)② 不注入 `NODE_OPTIONS=--max-old-space-size=256` → 实例会先撞**内核 SIGKILL**(无优雅退出、无日志,排查时无从下手) |
| **业务功能插件(第三方 + 自研)只有一种投放方式** = 「**admin 在门户导入候选池 → 用户在实例「功能管理」里自己启用/禁用**」(用户 2026-09-12 明确:三方插件**一律**按这个处理,**不需要铺什么**) | 若图省事改"直铺"(直接往用户 profile 装包 + 写 provider 覆写段),后果有三:① **绕过用户自决** —— 用户看不到、也关不掉;② 直铺覆写段与档案 65 的平台托管段**同属"整体替换 config"语义** → 两段并存**互相覆盖**,产生**难察觉的配置漂移**;③ 与托管段机制重复建设。**无例外** —— 连 AnySearch 最初走的直铺,也已由用户拍板改回候选池(档案 64 §8.3 修正)。⚠️ **例外只限平台基础设施插件**(`portal-entry` / `business-plugins` / `workspace-scoped-picker`):它们仍是平台级直铺、用户无感,见档案 16「插件三层归属」 |
> 细节与当时实测:`04-调整方案/33`(权限档位)· `04-16`/`04-64`/`04-65`(插件与 provider)· `04-58`(内存与配额)。
---
## 待核清单(换环境后逐项核对,脚本不代改)
- [ ] **工作区根** E:/ProgramData/AI技能/aliyun-dsh-server
- [ ] **文档库** D:/github/dsh_shenxian/dsh-server-docs
- [ ] **代码仓** D:/github/dsh_shenxian
- [ ] **备份目录** /opt/dsh/backups
- [ ] **服务器** bt-server(47.77.182.89,SSH 32022)
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""resident-rules.py —— 「常驻规则」的可移植化:快照 / 校验 / 注入 / 环境自检
为什么需要它
────────────
项目常驻规则写在**工作区**的 `CODEBUDDY.md`(每会话自动注入 ⇒ 动作前必然生效)。
但它是**工作区文件**:换电脑 / 换工作区路径 ⇒ 规则直接消失,而"技能"才是**可移植层**。
本脚本 + 同目录 `references/常驻规则-快照.md` 把两者接起来:
CODEBUDDY.md(工作区·权威·动作前生效)
│ --snapshot(抽取 §1/§3/§4/§5/§6/§8 全文)
▼
references/常驻规则-快照.md(技能内·可移植·随技能走)
│ --check / --inject(到新环境的 CODEBUDDY.md)
▼
新环境的 CODEBUDDY.md
设计红线(避免造"第二真相源")
──────────────────────────────
1. **权威方向单向**:`CODEBUDDY.md` 是权威,快照是**它的副本**(`--snapshot` 生成,不手改)。
2. **默认只报不改**:`--check` 只报告差异(退出码 1)—— 与文档库「体检只报不改」同规。
3. **注入只动标记块**:`--inject` 只替换 `<!-- BEGIN resident-rules … --> … <!-- END -->` 之间的内容;
首次注入**不自动建块**(除非 `--init`),避免与人工撰写的内容重复成两处。
4. **区分环境无关 / 环境相关**:环境相关项(绝对路径 / IP / hooks 路径 / 工具位置)
注入后**必须按新环境核对** —— 脚本给「待核清单」,不假装能自动配好。
用法
────
python3 resident-rules.py --snapshot # 由 CODEBUDDY.md 重生成技能内快照
python3 resident-rules.py --check [--goal <CODEBUDDY.md>] # 校验目标环境(默认只报,rc=1 = 有差异)
python3 resident-rules.py --env-check # 环境自检:路径 / hooks / 锁脚本 / 工作区
python3 resident-rules.py --inject [--goal <CODEBUDDY.md>] [--init] # 注入(需已存在标记块)
退出码:0 = 一致/成功;1 = 有差异(未改任何文件);2 = 用法/环境异常
"""
import io
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
SKILL = os.path.dirname(HERE)
SNAP = os.path.join(SKILL, 'references', '常驻规则-快照.md')
DEFAULT_GOAL = r'E:/ProgramData/AI技能/aliyun-dsh-server/CODEBUDDY.md'
# 要随技能走的章节("动作前必须生效"的那些 + 事故级事实)
SECTIONS = ['## 1. 提问判据', '## 3. 红线', '## 4. 提交边界', '## 5. 规划与执行分离',
'## 6. 并发纪律', '## 8. ']
# ★骨架锚点:这几条丢了就等于规则没了 —— 校验时逐条查存在性(短语取自权威文件)
ANCHORS = [
('A1', '只问「超过现有判断方法边界」的问题'),
('A2', '禁止用征询句收尾'),
('A3', '四件或七件套必跑', '占位'), # 由下方 canary 覆盖
('R7', '禁未经确认的批量 / 全仓写入'),
('R7-边界', 'R7 只适用于「不是我的 lane」'),
('R9', '绝对禁止「人工删锁 / 接管」'),
('R11', '只做正向迭代'),
('U27', '目标不打折,路径取最小代价'),
('L1', '先抢全局执行锁'),
('L2', '锁的生命周期 = 任务的生命周期'),
]
MARK_BEGIN = '<!-- BEGIN resident-rules (generated by skills/dsh-env-bootstrap · 勿手改块内) -->'
MARK_END = '<!-- END resident-rules -->'
# 环境相关项:注入/换机后**必须按新环境核对**(不是"自动配好")
ENV_ITEMS = [
('工作区根', r'E:/ProgramData/AI技能/aliyun-dsh-server'),
('文档库', r'D:/github/dsh_shenxian/dsh-server-docs'),
('代码仓', r'D:/github/dsh_shenxian'),
('备份目录', r'/opt/dsh/backups'),
('服务器', 'bt-server(47.77.182.89,SSH 32022)'),
]
def rd(p):
try:
return io.open(p, encoding='utf-8', newline='').read()
except OSError:
return ''
def section_of(text, head):
"""取 `head` 章节到下一个同级 `## ` 之间的内容(含标题行)。"""
i = text.index(head)
m = re.search(r'^## ', text[i + len(head):], re.M)
return text[i:i + len(head) + (m.start() if m else len(text) - i - len(head))].rstrip() + '\n'
def snapshot(src):
t = rd(src)
if not t:
raise SystemExit('ERROR: 读不到源文件 %s' % src)
parts = []
for h in SECTIONS:
if h == '## 3. 红线':
h = '## 3. 红线 R1' # 实际标题含编号,用前缀匹配
i = t.find(h)
if i < 0:
continue
j = t.find('\n## 4.', i)
parts.append(t[i:j if j > 0 else len(t)].rstrip() + '\n')
continue
if t.find(h) < 0:
parts.append('<!-- 源文件里没有 `%s` 章节 -->\n' % h)
continue
parts.append(section_of(t, h))
body = '\n---\n\n'.join(parts)
head = ('# 常驻规则 · 可移植快照(由 `scripts/resident-rules.py --snapshot` 生成,勿手改)\n\n'
'> **这是「工作区 `CODEBUDDY.md` 关键章节」的副本**,随技能走 ⇒ 换电脑 / 换工作区也能带走。\n'
'> ⚠️ **权威方向单向**:`CODEBUDDY.md` 是权威,本文件是它的副本(重生成用 `--snapshot`)。\n'
'> ⚠️ **环境相关项**(绝对路径 / IP / hooks 路径 / 工具位置)注入后**必须按新环境核对**:见文末「待核清单」。\n\n'
'## 骨架锚点(校验用;丢了就等于规则没了)\n\n')
anch = '\n'.join('- `%s` — %s' % (k, v) for k, v, *_ in
[(a[0], a[1]) for a in ANCHORS]) + '\n'
tail = ('\n---\n\n## 待核清单(换环境后逐项核对,脚本不代改)\n\n'
+ '\n'.join('- [ ] **%s** %s' % (k, v) for k, v in ENV_ITEMS) + '\n')
out = head + anch + '\n---\n\n' + body + tail
os.makedirs(os.path.dirname(SNAP), exist_ok=True)
io.open(SNAP, 'w', encoding='utf-8', newline='\n').write(out)
return len(out), len(parts)
def check(goal):
t = rd(goal)
if not t:
print('ERROR: 读不到目标 %s' % goal)
return 2
snap = rd(SNAP)
if not snap:
print('ERROR: 缺少技能内快照(先跑 --snapshot)')
return 2
print('=== 常驻规则校验|目标 = %s ===' % goal)
bad = 0
for a in ANCHORS:
k, phrase = a[0], a[1]
if phrase.startswith('四件或七件套'):
ok = ('七件套' in t) or ('四件套' in t)
else:
ok = phrase in t
print(' %-10s %s %s' % (k, '✓' if ok else '✗ 缺失', phrase[:40]))
bad += 0 if ok else 1
sec_ok = sum(1 for h in ('## 1. 提问判据', '## 3. 红线', '## 4. 提交边界',
'## 5. 规划与执行分离', '## 6. 并发纪律') if h in t)
print(' 章节存在性:%d/5' % sec_ok)
bad += 5 - sec_ok
print('结论:%s' % ('✅ 关键规则齐备' if bad == 0 else '⚠️ 有 %d 处缺失/漂移 ⇒ 用 --inject(或人工补齐)' % bad))
return 1 if bad else 0
def env_check():
print('=== 环境自检(换机后逐项确认)===')
bad = 0
for k, v in ENV_ITEMS:
if k in ('服务器', '备份目录'):
print(' %-8s %-52s (需 ssh 侧核对)' % (k, v))
continue
ex = os.path.exists(v)
print(' %-8s %-52s %s' % (k, v, '✓ 存在' if ex else '✗ 不存在'))
bad += 0 if ex else 1
# hooks 里的绝对路径(工作区搬迁后最常见的坑)
st = r'E:/ProgramData/.workbuddy/settings.json'
if not os.path.exists(st):
st = os.path.expanduser('~/.workbuddy/settings.json')
try:
h = json.loads(rd(st)).get('hooks', {})
except Exception:
h = {}
cmds = [x.get('command', '') for v in h.values() for it in v for x in it.get('hooks', [])
if x.get('type') == 'command']
print(' hooks 命令 %d 条:' % len(cmds))
for c in cmds:
paths = re.findall(r'[A-Za-z]:/[^\s"]+?\.(?:py|sh|exe)', c)
miss = [q for q in paths if not os.path.exists(q.replace('/', os.sep))]
flag = '✗ 路径失效' if miss else '✓'
print(' %s %s' % (flag, c[:100]))
bad += 1 if miss else 0
return 1 if bad else 0
def inject(goal, init=False):
t = rd(goal)
if not t:
print('ERROR: 读不到目标 %s' % goal)
return 2
snap = rd(SNAP)
if not snap:
print('ERROR: 缺少技能内快照(先跑 --snapshot)')
return 2
body = snap.split('## 待核清单', 1)[0]
block = MARK_BEGIN + '\n' + body.strip() + '\n' + MARK_END + '\n'
if MARK_BEGIN in t:
new = re.sub(re.escape(MARK_BEGIN) + r'.*?' + re.escape(MARK_END) + r'\n?',
block, t, flags=re.S)
io.open(goal, 'w', encoding='utf-8', newline='').write(new)
print('✓ 已替换标记块内内容(块外人工内容未动)')
return 0
if not init:
print('⚠️ 目标里没有标记块 ⇒ **不自动注入**(避免与人工撰写的内容重复成两处)。\n'
' 要建立标记块并注入:加 --init(会**追加**到目标文末,不动既有内容)')
return 1
io.open(goal, 'w', encoding='utf-8', newline='').write(t.rstrip('\n') + '\n\n' + block)
print('✓ 已追加标记块(既有内容未动);建议随后人工去重并 `--check`')
return 0
def main(argv):
goal = DEFAULT_GOAL
if '--goal' in argv:
goal = argv[argv.index('--goal') + 1]
if '--snapshot' in argv:
n, k = snapshot(goal)
print('✓ 已由 %s 生成快照:%d 字符 / %d 个章节 → %s' % (goal, n, k, os.path.relpath(SNAP, SKILL)))
return 0
if '--env-check' in argv:
return env_check()
if '--inject' in argv:
return inject(goal, init=('--init' in argv))
if '--check' in argv:
return check(goal)
print(__doc__)
return 2
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,352 @@
---
name: dsh-feature-first
description: dsh 多租户平台(alotbuy.com)的「功能优先」协作协议 —— **用户只提功能需求,AI 自主完成全部技术决策**。当用户提出任何平台功能/交互需求、报障、或说「我要把精力放在功能搭建上」时使用;也用于 AI 自查「这件事该不该拿去问用户」。核心 = 复盘证据(技术类+报障类占 64%)+ 用户的「功能卡」4 问 + **AI 自主决策的 9 类白名单(永不问)** + **只准上抛的 3 类(功能语义分叉 / 红线门禁 / 超出方法边界)** + **决策方法边界定义(边界外必须问的 8 类)** + 上抛语言转换表 + 报障闭环前置 + 交付回执格式 + 方法本身演进不再上抛 + **§3.6 拆包上抛:红线问题不得与技术方案捆在一起问** · **§3.2 红线按「实际影响面」判、不按「动作名字」判** · **§5.1 结论骨架(答「是否已实现 / 为什么不行」)** + **§5.3 铁律 4–5 / §5.4 硬约束 7–9:待确认项 = 回复的最后一节、逐条编号;每个候选各占一段(竖排、不横排)且必须写「优点 / 缺点」;只有优点或只有缺点 ⇒ 自决、不上抛**(2026-09-15 用户明令)。**配套:`dsh-decision-method`(怎么想)· `dsh-change-workflow`(怎么落地)。**
version: 1.7.0
updated_at: 2026-09-15
agent_created: true
---
# dsh-feature-first — 功能优先协作协议
> **一句话**:把「事前请示」改成「**默认自主 + 事后可推翻**」。
> 用户的输入只到**功能层**(谁、在哪、做什么、怎样算成功);**技术层全部由 AI 自主决定并记录**。
---
## 0. 为什么要有这条协议(复盘证据,2026-09-12)
### 0.1 事实:用户的时间被技术问题吃掉了
对 `dsh-server-docs/04-调整方案/` 全部档案按「触发来源」分类(含「触发」字段的 42 份):
| 类别 | 份数 | 占比 | 含义 |
|---|---|---|---|
| **用户的功能 / 交互需求**(用户的主场) | 15 | 36% | 16 / 18 / 19 / 31 / 34 / 36 / 37b / 38b / 53 / 54 / 56① / 57 / 60 / 61 / 62 |
| **技术类**(用户被拉进技术判断,或 AI 做技术排查) | 17 | 40% | 12 / 14 / 22 / 23 / 26 / 27 / 29 / 32 / 33 / 35 / 37a / 38a / 42 / 55 / 58 / 64 / 65 |
| **用户报障**(用户只能看到现象,被迫当测试) | 10 | 24% | 15 / 17 / 21 / 24 / 25 / 30 / 49 / 51 / 56② / 59 |
⇒ **技术类 + 报障类 = 27/42 ≈ 64%** —— 用户 2/3 的注意力花在自己不擅长的领域。
> 复核命令:`cd dsh-server-docs/04-调整方案 && grep -Hn "触发" *.md`(分类可按档案号逐一核验)
> **⚠️ 该比例是快照,会随档案增长变化;引用时须重新取数。**
### 0.2 五个根因(按影响排序)
| # | 根因 | 表现 |
|---|---|---|
| **1** | **技术问题被"决策化"了** | AI 把"我不确定选哪条技术路径"包装成「决策点」上抛:走 CF 代理还是改 DNS、要不要 watchdog、内存能否降到 512M、要不要 enablePatch。用户没有判断依据,只能凭感觉点头 |
| **2** | **需求没有"规格层"** | 用户说「AI 生成的文件看不到、下不了」→ AI 直接跳到技术方案(新增端点 + 注入脚本 + 面板)。中间的"谁在什么场景要看到什么、怎样算成功"没有沉淀 → 每次都要重新谈技术 |
| **3** | **报障驱动** | 平台上线后用户输入大量来自"坏了"而不是"我要什么";报障的根因排查天然是技术沟通 |
| **4** | **没有"技术默认值"** | 项目其实有大量技术原则(红线 R1-R8、只准收窄、最小代价路径、能一行代码就不改平台),但**没有一条"AI 自主技术决策的授权边界"** → 每次都重新分析、重新上抛 |
| **5** | **交付语言是技术语言** | AI 汇报用 commit / 文件 / md5 / 端点;用户只能靠技术语言参与验收 → 反过来推高了技术沟通量 |
---
## 1. 用户的输入:只填「功能卡」4 问
> 用户**不需要**描述技术方案、不需要指定改哪个文件、不需要选技术路径。只回答这 4 件事(缺的 AI 自己补默认值并标注)。
```
1. 谁用? (角色:admin / 普通用户 / 两者)
2. 在哪用? (哪个界面:门户某页 / 实例内设置 / 会话页 / 右下角…)
3. 要做什么? (用户视角的动作与结果,一句话)
4. 怎样算成功? (用户能亲眼看到/亲手验证的验收点)
```
**示例(用户侧的正确写法)**
> 「普通用户要能在设置里看到自己能启停哪些功能插件,勾选后确认,重启后生效。」
**示例(不要这样写)**
> ~~「在 business-plugins 的 settings.section 里加一个 id、调 /api/plugins/mine/apply、重启实例生效」~~
> —— 这是实现,不是需求。
**AI 侧**:拿到功能卡后,缺什么自己定;定完在档案里记「技术选择」段(见 §5)。
---
## 2. AI 自主决策的 9 类白名单(**永不问用户**)
> 命中以下任一类 → **直接定,直接做,只在档案里记**。问了就是违规。
| # | 类别 | 例子 |
|---|---|---|
| 1 | **技术选型** | 用哪个 API / 表 / 库 / 协议 / 存储格式(pnpm vs npm、sqlite vs 文件、HTTP 流 vs WS) |
| 2 | **实现路径** | 改哪一层(编排器 / profile / bwrap / nginx / 前端)、用配置还是改码、复用哪个扩展点 |
| 3 | **命名与结构** | 变量名、路由路径、表名、目录布局、文件拆分 |
| 4 | **性能与资源调参** | 内存上限、并发数、超时、缓存 TTL、退避参数 |
| 5 | **部署与同步流程** | scp 姿势、权限位、行尾、对账步骤、脚本怎么写 |
| 6 | **排查方法** | 用 journalctl 还是埋点、怎么复现、怎么取证 |
| 7 | **版本与依赖** | 选哪个版本的包、预装还是按需拉取、打包方式 |
| 8 | **兼容与降级** | 遇到不支持的平台怎么办(显式抛 `unsupported` 而不是假装支持) |
| 9 | **文档与档案的技术内容** | 编号、模板、章节、脚本实现 |
**判断口诀**:**"用户能不能从功能视角判断这个选项的好坏?"**
- 能 → 可以上抛(见 §3)
- 不能 → **这就是 AI 的工作,不要问**
---
## 3. 只准上抛的 3 类(+ 语言转换表)
> **唯一判据(用户 2026-09-12 原话):只问「超过现有判断方法边界」的问题。**
> 边界内 → 一律自决;边界外(§3.1–§3.4)→ 必须问。**没有第三条路。**
### 3.1 (a) 功能语义分叉 —— 选项之间存在**用户能感知的差别**
例:技能随插件包投放 vs 走平台共享层(差别 = 用户"要不要分开管理");新开会话 vs 迁移老会话(差别 = 用户"要不要重开一个");提示 vs 自动改档位(差别 = "AI 会不会悄悄改我的设置")。
### 3.2 (b) 红线门禁 —— 必须用户点头
- **扩大**权限/可见面(R5)
- 会**中断在线用户**(R8)
- **批量 / 全仓写入**(R7,>10 文件)
- ⚠️ **判据:按「实际影响面」判,不按「动作名字」判**(2026-09-13 用户纠正)—— 看到「部署 / 上线 / 生产 / 铺包」就上抛是**过度套用**:
**不中断在线用户的上线**(传产物 / 换 profile 包 / 改静态页 / 候选池投放)**属边界内的执行细节 ⇒ 做完即上线,不上抛**;只有**真会中断**的(重启服务 / 停实例 / drain / 改配额·env / 改 nginx·nft)才上抛。详见 `dsh-decision-method` **U20 / X9**。
### 3.3 上抛语言转换表(**强制**)
| ❌ 技术语言(禁止) | ✅ 用户能感知的语言(必须) |
|---|---|
| 「要不要启用 `enablePatch`?」 | 「要不要让用户**自己装插件**,还是只由管理员统一装?」 |
| 「`DSH_PERMISSION_MODE` 设成哪个档位?」 | 「AI 在你的会话里**能不能直接执行命令**,还是每次都问你一遍?」 |
| 「新会话 vs 改写存量会话种子事件」 | 「**新开一个会话**就好,还是要我去改你**已有的**会话设置(改完你正在用的会话会变)」 |
| 「`NODE_OPTIONS` 限堆 / `MemoryMax` 降到 384」 | 「每个用户能用的内存**小一点更安全**,但用户跑大任务时余地也小一点」 |
| 「插件走内投技能还是平台共享技能层」 | 「技能**跟着插件一起装**,还是**单独管理**?」 |
> **规则**:上抛时**不允许出现**包名、环境变量、文件路径、commit、API 路径、代码标识符。出现即是没转换。
### 3.4 (c) 超出决策方法边界的事 —— **必须问**(用户 2026-09-12 明确)
> **用户原话**:「有超过现有决策方法边界的事情,还是需要问的。」
> 即:**方法覆盖得到 → 自己定;覆盖不到 → 必须问**。这条是 §2 的**例外出口**,也是闸门的 fail-open 依据。
> ⚠️ 判据是「**有没有客观可判的优劣**」:有 → 方法能判,自己定;没有 → 那是用户的取向,必须问。
| # | 边界外的类别 | 例子 | 为什么方法判不了 |
|---|---|---|---|
| 1 | **业务目标与优先级** | 先做哪个功能、这个功能要不要做、值不值得投入 | 方法只能判"**怎么做得最优**",判不了"**该不该做**" |
| 2 | **成本与资源承诺** | 买云资源 / 开第三方账号 / 花钱买额度 | 涉及用户的钱,不是技术优劣问题 |
| 3 | **对外承诺与合规** | ICP 备案、资质、合同、对外 SLA、对外品牌文案 | 法律与商业后果,超出工程判断 |
| 4 | **需要用户提供的第三方凭据 / 审批** | API Key、DNS 权限、企业审批、账号授权 | 只有用户有 |
| 5 | **用户体验偏好(无客观优劣)** | 审美与文案语气、默认值取向、提示语措辞 | 没有"更优",只有用户"更喜欢" |
| 6 | **影响面超出本平台** | 会波及其他系统 / 他人数据 / 不可逆的对外影响 | 影响半径超出我能判断的范围 |
| 7 | **红线门禁**(R5 扩大 / R7 批量>10 / R8 中断在线用户) | — | 安全与影响面变更**必须**用户知情同意 |
| 8 | **方法确实判不准** | 两边都无依据、事实不足以判断 | **宁可问,不要卡死** |
**上抛方式**:这 8 类**照实说人话**说明「你为什么需要他决定」,不要包装成技术选项。
### 3.5 方法本身的演进 —— 不再上抛(用户 2026-09-12 明确)
> **用户原话**:「以后决策方法的问题就不需要再问了。」
- **方法的日常应用不问**:边界内的一切判断,按 §2 + `dsh-decision-method §4.4` 自己定。
- **方法本身的修订也不问**:发现方法有缺口 / 有反例 / 需要加规则时,**直接改、直接记录**(改完在交付回执里说一句"我更新了方法:因为 X"),不要拿"要不要加这条规则""这么写好不好"去问用户。
- **唯一例外**:方法修订若**扩大**了 AI 的自主权或收窄了用户的门禁 → 属边界外的第 1/7 类,**必须问**。
### 3.6 ⛔ 拆包上抛:红线问题**不得**与技术方案捆在一起问(2026-09-12 实证)
> **实证**(会话「任务执行2」14:39):AI 把「**要不要现在重启服务**(R8,该问)」和「**用 A 还是 B 实现**(技术项,不该问)」**捆成一个提问**,用户被迫先读懂两套技术方案才能回答那个红线问题
> → 用户的体感依然是"**又在让我确认技术问题**"。**这才是"设置了却没用"的真正形态**:不是问得太多,而是**把该问的和不该问的混在一次提问里**。
**三条规则**:
1. **剥出红线问题单独问**,且只问用户能判断的维度:**要不要现在动生产 / 影响谁 / 断多久 / 能否避开**。
2. **技术形态自己定**,作为**已定项**写进回复("我按 B 做,因为…;可推翻"),**不要做成选项让用户选**。
3. **一轮最多一个问题**;同一个问题**不要连问两次**(实证:11:16 与 11:42 两次问"用哪个账号验收",第二次只是细化 → 本可合并成一次,或直接自定)。
**该会话 6 次上抛的逐条判定(反例对照)**:
| 时间 | 问的什么 | 判定 |
|---|---|---|
| 10:34 | 21 处 Windows 专属指引怎么处理(一并改 / 只改单子 / 先出清单) | ❌ **纯技术项**(范围 + 实现路径)——该自己定 |
| 10:21 | 行尾 CRLF 怎么处理 | ✅ 该问(命中 **R7**:批量换行符转换需授权) |
| 11:16 / 11:42 | 验收用哪个实例 / 账号 | ✅ 该问(影响面)· ❌ **连问两次**,可合并 |
| 13:32 | admin 崩溃怎么修 | ✅ 该问(R8 + 跨会话)· ❌ 选项里混了"我拆 / 转给对方 / 只给命令"三种**技术路径** |
| 14:39 | 选 A/B 方案 **+** 要不要重启 | ✅ 红线该问 · ❌ 技术方案不该问 → **典型捆包** |
**正确写法(照这个格式写)**:
```
我先按 B 做(平台侧自动摘除坏插件,覆盖所有装法)——**已定,可推翻**。
但它要重启 dshs:
· 影响谁:当前所有在线用户(刚实测 guest 实例在跑)
· 断多久:数秒;实例"访问即拉起",会话数据不丢
· 能否避开:可以等空闲;或并入 T04 那批改动只重启一次
**请定:现在就重启,还是等窗口?**
```
---
## 4. 报障闭环前置(让用户**不必**报障)
报障类占 24%,大部分可以从源头消掉。凡涉及"用户会撞上的状态",**必须同时交付**:
| # | 要求 | 已落地先例 |
|---|---|---|
| 1 | **错误页给人话 + 下一步**,禁止裸 JSON / 英文原文 | 档案 25(`not_running` 不再吐 JSON) |
| 2 | **等待必有可见反馈**(导航路径 + XHR 路径都要有) | 档案 59(3 秒后亮覆盖层) |
| 3 | **状态可自查**(能力清单 / 我的文件 / 档位提示) | 档案 56 |
| 4 | **能自愈就不报错**(401 透明重放、回收后自动恢复) | 档案 24 / 45 / 49 / 51 |
| 5 | **缓存/后台任务的状态可见**("更新于 X 前"、可手动重拉) | 档案 62 |
> 判断口诀:**「用户遇到这个情况,会不会只能来问我?」** 会 → 先把反馈做出来。
---
## 5. 答复与交付格式(功能性语言优先)
### 5.1 结论骨架(回答「是否已实现 / 能不能 / 为什么不行」类提问)
> **2026-09-13 用户纠正原话**:「**需要告诉我的是 是否已实现,如果未实现:为什么不能,需要我拍板可以问我**」
> 触发场景:用户问「是否已实现」,AI 却用「我自己的失误(一并交代)+ 探针怎么被污染 + 版本流水 + 下一步三步计划」作答 —— **要的结论被埋在第 5 段之后**,用户只能再追问一句。
**固定四节,顺序不许换;没有的节整节删掉,不要留空标题:**
> ⚠️ **「需要你拍板」必须是整条回复的最后一节**(2026-09-15 用户明令:「**要把需要我确认或决策的内容放在 最后,别隐藏在回复内容中间** | **按照有序段落展示**」)—— 它后面**不许再有任何节**,且必须**逐条编号**(有序段落),不写成散文。理由:夹在中间 ⇒ 用户扫不到、漏答。
> ✅ **配套的反向要求(同一句原话前半段)**:「**能根据决策方法 自行决策的就自决策继续处理**」⇒ **能自决的不要停下来问**,直接做完并陈述;**只有不能自决的**(真门禁 / 需你给凭据或窗口)才收进这一节。
> 🎯 **上抛门槛 = 存在"真取舍"**(2026-09-15 用户明令:「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」):把候选各写 **优点 + 缺点** —— 某个**只有优点**(明显更优)或**只有缺点** ⇒ **不需要用户判断**,自己拍掉再陈述;只有**各有优有劣、且客观标准分不出高下**才算真取舍,才上抛,且**必须逐项列出优点与缺点**(只写"差别在哪"不算)。
> 📐 **候选方案必须"竖排成段"**(2026-09-15 用户明令:「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」):**A / B / C 各占一行(各自成段)**;⛔ **不许**写成 `A:… · B:…` 一行横排,⛔ **也不许**把候选做成**表格的列**。段内「优点…;缺点…」连写即可 —— 不必每个字段再拆行,否则撞 §5.4 硬约束 3「每节 ≤7 行」。
```markdown
## 判定
❌ 未实现 / ⚠️ 部分可用 / ✅ 已实现 —— <一句话>
| 目标 | 状态 |
|---|---|
| <用户列的第 1 项> | ✅/❌ + 半句依据 |
| <第 2 项> | … |
## 为什么不行(只在有 ❌ 时写;最多 3 层,结论层零技术标识)
- 已经排除的:<今天已经修完、不再是原因的> —— 一句话带过
- 当前唯一卡点:<一句人话>
- 为什么难(可选):<1–2 句;不确定性要写进**结论句**,例:「还不能说做不到,只能说这一步还没试」>
## 我接着做
- 下一步 <X> —— **陈述句,不是征询句**
## 需要你拍板(**最后一节**;真需要才写,不需要 → 整节删掉;**逐条编号**)
**1. <问题一句话>**
**A** —— 优点:…;缺点:…
**B** —— 优点:…;缺点:…
我倾向 **A**(一句话理由,可推翻)
**2. <下一件>** —— 同上
```
### 5.2 交付回执(已完成功能的交付)
AI 交付时**先说功能,技术细节折叠在后**:
```markdown
## 做了什么(功能)
<2-3 句:现在多了一个什么能力,在哪儿>
## 你现在能看到
- <具体位置> 出现了 <什么>;点它会发生 <什么>
- 验证方式:<用户亲手可做的一步>
## 不用你决策的技术选择(已定,可随时推翻)
- <易感知的一条,一句话>(若你希望反过来,说一声即可)
…
## 技术附录(可选读)
<文件 / commit / md5 / 端点 / 档案号>
```
> 第 3 段是这条协议的关键:**把"事前请示"改成"事后可推翻"** —— 用户获得了知情权与推翻权,但不需要在读之前做判断。
---
### 5.3 五条铁律(都来自本工作区的真实失手)
| # | 铁律 | 反例(实证) |
|---|---|---|
| 1 | **回答主位 = 用户问的那件事**;AI 的进度 / 失误 / 计划**不得占前两节** | 用户问「是否已实现」,AI 先写「五、我自己的失误(一并交代)」+ 版本流水 → 用户被迫再追问一句才拿到结论 |
| 2 | **结论层零技术标识**(版本号 / commit / 包名 / 内部函数名 / 路径 / 探针名)—— 最多放「技术附录」 | `0.2.19 / 0.2.20 / 0.2.23`、`TDZ`、`chr(10)`、`requestOverUnixSocket` 全在主线,用户读不出结论 |
| 3 | **禁止征询式收尾**:「要我接着做吗 / 说一声即可 / 你看怎么弄」—— 下一步**已定**且不命中门禁(现行门禁 = **不可逆破坏性操作**,见 `CODEBUDDY.md §3 R8`)⇒ **直接做**,用陈述句交代。⚠️ 本条禁的是**形态**;**位置**要求见铁律 4 | 「要我现在接着做,说一声即可」—— 把本该自己拍的执行细节又推回用户(同 **X9** 一族) |
| 4 | **能自决的继续做;不能自决的收到最后一节** —— 能按决策方法自决 ⇒ **自决 + 继续处理**(不为"要不要继续"而停);不能自决(真门禁 / 需凭据·窗口)⇒ **收进整条回复的最后一节,按有序段落逐条编号**(每条 = 问题 + 选项**优缺点** + 我的倾向),⛔ 不许夹在中间,也不许散在正文里问 | 2026-09-15 用户原话:「**能根据决策方法 自行决策的就自决策继续处理,不能决策的问题和需确认内容放在回复的最后,按照有序段落展示**」—— 待拍板项夹在「进度 + 下一步」之间 ⇒ 用户扫不到、漏答 |
| 5 | **上抛前先过「取舍筛」**:把候选各写 **优点 + 缺点** —— ① 某个**只有优点**(明显更优)或**只有缺点** ⇒ **不需要用户判断**,自己拍掉再陈述;② 只有**各有优有劣、客观标准分不出高下**(真取舍)才上抛;③ 上抛时**必须逐项列出优点与缺点**(只写"差别在哪"不算);④ 候选**竖排成段**(A / B / C 各占一行),⛔ 不横排、不做成表格的列 | 2026-09-15 用户原话两段:「**需要我确认的方案需要说明优点和缺点,现在没法判断,假如只有优点或只有缺点那不需要我判断**」+「**每个需要我决策的问题的潜在解决方案 A B C 也按照段落式排版,别横着排列**」—— 此前只写"可感知差别"且**横排**,用户**没法判断** |
> **自己失误的交代**:只在两种情况下写 —— ① 它**改变了结论**(例:某次改动引入了新问题);② 用户**问根因**。否则放最后一节一行,或先不提。
---
### 5.4 排版规范(让长回答**可扫读** —— 结构清晰 / 重点突出 / 细节完整)
> 目标:**30 秒扫到结论,2 分钟看全细节**。适用范围 = **每一轮回复**(执行信息 / 报障答复 / 提问 / 交付回执都算)。
> 判据:排版不是为了好看,是为了**能不能被扫** —— 所以下面每条都**可自检**(数得出来)。
**A. 九条硬约束**
| # | 约束 | 自检怎么数 |
|---|---|---|
| 1 | **首屏 3 行内给判定**(✅/⚠️/❌ + 一句) | 前 3 行有没有判定 |
| 2 | **层级 ≤ 3 级**(`##` → `###` → 列表),**不出 `####`** | 有没有第 4 级 |
| 3 | **每节 ≤ 7 行**;连续 **>12 行无结构** = 文字墙 | 有没有墙 |
| 4 | **加粗只留跳读关键词**:每节 ≤2 处、**不整句加粗** | 数加粗处数 |
| 5 | **表格 ≤ 5 列**;单元格不塞整句 | 数列宽 |
| 6 | **一条信息只出现一次**(别标题/正文/表格各写一遍) | 抽查重复 |
| 7 | **能自决的继续做;不能自决的收进最后一节、逐条编号**(有序段落;埋在中间 = 用户漏看) | 翻到最后一节看是不是拍板项;数它有没有编号 |
| 8 | **上抛项必须带「优点 / 缺点」两栏**(只有优点或只有缺点 ⇒ 本不该上抛) | 每个候选是否优缺点各至少一条 |
| 9 | **候选竖排成段**(A / B / C **各占一行**)—— ⛔ 不横排、⛔ 不做成表格的列 | 有没有 `A:… · B:…` 一行塞多个候选 |
**B. 按回答类型套现成骨架(**不新造**)**
| 回答类型 | 用哪个骨架 |
|---|---|
| 执行信息(做了什么 / 结果如何) | **§5.2 交付回执**:做了什么 → 你能看到 → 不用你决策的技术选择 → 技术附录 |
| 是否已实现 / 能不能 / 为什么不行 | **§5.1 结论骨架**:判定 → 为什么不行 → 我接着做 → **需要你拍板(末节)** |
| 报障 / 排查结果 | 判定(根因一句)→ 证据(命令 + 输出,代码块 **≤10 行**)→ 处置 → 未闭环 |
| **向用户提问** | `**问题**`(一句)→ `**为什么问你**`(命中哪条门禁)→ `**选项**`(**每个候选各占一段:竖排**,各 ≤3 行,**每个都必须写「优点 / 缺点」**,推荐项置首标"(推荐)") |
| 长清单 / 对比 | **表格**(不要长 bullet 串) |
**C. 十三条反模式(见到就改)**
1. ❌ 大段无空行文字(>12 行)→ 拆节或转表
2. ❌ 嵌套列表超过 2 层 → 降为表格或加粗小标题
3. ❌ 结论埋在段落中间 → 提到该节**首句**
4. ❌ 整句 / 整段加粗 → 只留关键词
5. ❌ 表格 >5 列、单元格里塞整句 → 拆表 / 缩短
6. ❌ 同一信息重复三遍(标题 + 正文 + 表格)→ 留一处
7. ❌ 用"如下所述 / 综上"指代不清 → 直接写"见 §X"或重述一句
8. ❌ emoji 堆砌 → **只用于状态**(✅⚠️❌🔄)与**分级**(P0/P1)
9. ❌ 术语 / 路径 / 版本号混进结论层 → 移入「技术附录」(§5.3 铁律 2)
10. ❌ 标题层级跳跃(`##` 直接到 `####`)→ 逐级
11. ❌ **待你拍板的内容夹在中间**(后面还有别的节)→ **挪到最后一节**;❌ 这些内容写成散文一段 → 改成**有序编号条目**(§5.3 铁律 4 · 2026-09-15 用户明令)
12. ❌ 只写"两者差别在哪"却**不写优缺点** → 补齐「优点 / 缺点」两栏;❌ 把**只有优点**(或只有缺点)的候选拿来问 → **自己拍掉**(§5.3 铁律 5)
13. ❌ 候选方案**横排**(`A:… · B:…` 一行并列,或把候选做成表格的列)→ **每个候选各占一段(竖排)**(§5.3 铁律 5 ④ · 2026-09-15 用户明令)
**D. "细节完整"≠"全塞正文"**:细节进「技术附录」/ 代码块 / 表格附列;**正文只留"能决定下一步"的信息**。
---
## 6. AI 自检清单(每次动手前 / 交付前)
**动手前**
1. 我手上有一张**功能卡**吗(谁/在哪/做什么/怎样算成功)?没有 → 先问这 4 条,**不要问技术**。
2. 我准备上抛的每一件事,**用户能从功能视角判断吗**?不能 → 撤掉,自己定。
3. 我上抛的文案里,有没有包名 / 环境变量 / 路径 / commit?有 → 翻译成"能感知的差别"。
4. 有没有命中红线(R5 扩大 / R7 批量 / R8 中断)?**按实际影响面判,不按动作名字判**(X9)—— 命中 → 必须上抛,并说明**影响谁、断多久**;**没命中就别拿「这是生产操作」当理由停下**(不中断的上线直接做完)。
**交付前**
5. 我有没有按 §5 的格式给"做了什么 / 你能看到什么"?还是又只给了技术清单?
6. 用户会不会因为**缺少某个反馈**又来报障?(对照 §4 五条)
7. 技术选择我**记进档案**了吗(§2 的 9 类,做完要留痕,否则下次重新吵)。
8. 用户问的是「是否 / 能不能 / 为什么不行」吗?—— 是 → 用 §5.1 结论骨架(**判定在前,过程在后**)。
9. 结论层有没有技术标识(版本号 / commit / 包名 / 内部函数名 / 路径)?有 → 挪进「技术附录」。
10. 我有没有用「要我接着做吗 / 说一声即可」收尾?—— 有 → 改成陈述句,并**直接去做**(除非命中门禁 = 不可逆破坏性操作)。
11. 这条回复**能被扫吗**?—— 首屏 3 行给判定 / 层级 ≤3 / 每节 ≤7 行 / 加粗 ≤2 处每节 / 表格 ≤5 列 / 一条信息只说一次 / **待拍板项在末节**(§5.4;执行信息·报障·提问**各有骨架**)
12. **需要用户确认 / 决策的内容,在整条回复的最后一节吗**?—— 它后面若还有别的节 ⇒ **挪到最后**(2026-09-15 用户明令);且必须**逐条编号**(有序段落),不是散文一段;形态是**陈述句**,不是征询句。**反向也查一遍**:能自决的事,我是不是停下来问了?
13. 我要上抛的每一项,**优缺点都写了吗**?—— 若某个候选**只有优点**或**只有缺点** ⇒ **不该问**,自己拍掉再陈述(§5.3 铁律 5)。
14. 候选**竖排成段**了吗(A / B / C **各占一行**)?—— 横排成 `A:… · B:…` 或塞进表格的列 ⇒ **改成竖排**(§5.4 硬约束 9 · 2026-09-15 用户明令)。
---
## 7. 与其他约定/技能的关系
- **红线优先级最高**:R1-R8 命中一律先停手 —— 本协议不构成豁免,只约束"该不该问"。
- **`dsh-decision-method`**:管"怎么想、怎么定"(判定矩阵、十问、验收分级)。本协议管"**谁定什么、用什么语言问**"。
- **`dsh-change-workflow`**:管"怎么落地"(六阶段、档案模板、并行调度)。
- **`06-工作台UI规范.md`**:前端强制基线,冲突以其实测 Token 为准。
- **单一来源**:本协议即唯一来源(技能必须本地加载才生效);文档库 `INDEX.md` 只放**指针**,不复制全文。
@@ -0,0 +1,224 @@
---
name: dsh-instance-diagnose
description: DSH 多租户平台(alotbuy.com / 47.77.182.89)单个用户实例的「故障诊断」技能,重点是内存 / OOM / 实例崩溃重启。当出现「实例打不开」「会话突然报错/中断」「跑着跑着断了」「响应慢」「怀疑内存不够」时触发。核心:先分「服务级 / 实例级 / 会话级」三层定位,再用 cgroup 内存三件套定量归因,最后在隔离 cgroup 里复现——**绝不在生产实例上做压力测试**。
version: 1.0.0
updated_at: 2026-09-12
last_change: 首版。由 2026-09-12 guest 实例 OOM 排查沉淀(两个会话同一秒被打断 → 内核 OOM 杀 node → exitCode 137),含 cgroup v1 路径、两条死亡路径区分、隔离复现配方与 6 条实测踩坑。
agent_created: true
---
# dsh-instance-diagnose — DSH 实例故障诊断
## 何时用
用户报「实例打不开 / 会话突然报错 / 聊到一半断了 / 很慢 / 内存不够」,或你看到 `crash-restart` 日志。**报障第一步永远是先读 journal 拿真实失败请求**,不要先猜。
## 平台事实(硬编码,勿猜)
| 项 | 值 |
|---|---|
| 服务器 | SSH 别名 `bt-server`(端口 32022,root) |
| 用户数据根 | `/var/lib/dshs/users/<uuid>/`(**不是** `/opt/dsh/users`,那里只有 main) |
| 已知账号 | guest = `4092b965-2f68-4977-9989-68b3966f7df0`(系统 uid **100002**)|admin = `cce6d1cd-b376-4304-80f0-0e1c58c9ffde`(uid 114801) |
| 实例配额 | ⚠️ **2026-09-14 起:基础 MIN、最多浮动到 MAX**(档案 **96**,用户要求:**不受插件开关影响**):cgroup `MemoryHigh = MIN_MEM_MB = 448 MiB`(**软限/基础**,超过即回收·限速)+ `MemoryMax = MAX_MEM_MB = 1024 MiB`(**硬限/上界**,越界 OOM);**不再读 profile 的 bundles**;`heapMbFor()` = 配额 − 96,**cap 256**。其余 `CPUQuota 150%` / `TasksMax 128` 未变。实测(2026-09-14):两 scope 均 `MemoryHigh=448M` + `MemoryMax=1024M`(与各自插件集合无关)。⚠️ **`dsh-univer-office` 这类重插件(gateway ≈390MB + 基座)512 装不下 ⇒ 先抬 MIN**。⚠️ `/etc/dshs.env` 里的 `DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160` **已不是生效值**(代码侧 `withHeap()` 会摘掉 `--max-old-space-size` 再按配额补回),查看实际值请直接读 `/proc/<pid>/environ` 与 `systemctl show <scope> -p MemoryMax` |
| 宿主 | 物理内存仅 **1.83 GB**(1915896 kB);swap 1 GB |
| 会话文件 | `<用户根>/home/sessions/--var-lib-...-ws-<工作区>--/<session-id>/session.jsonl.zstd`(**多帧 zstd**,按 magic `28 b5 2f fd` 切分逐帧解压) |
> ⚠️ 会话目录名**不是** `home/sessions/<session>`,中间还有一层带名字的 workspace 转义目录。
## 三层定位(按顺序做,每层都能单独结案)
### 第 1 层 · 服务级
```bash
systemctl status dshs --no-pager | head -20 # 重启过没
journalctl -u dshs --since today --no-pager | grep -iE "crash-restart|instance-restart|instance-stable"
```
- `instance-restart` 带 **`exitCode 137`** ⇒ **内核 SIGKILL(128+9),九成是 OOM**
- `exitCode 1` + `ModuleLoader.import` ⇒ 插件加载崩(如 `duplicate loader entry id`)
- 无 exitCode ⇒ 信号终止,看紧随其后的服务重启
### 第 2 层 · 实例级(内存三件套)
**先找 scope 名**(**每次重启 hash 会变**,别写死):
```bash
ls -d /sys/fs/cgroup/memory/system.slice/dsh-*.scope
```
> ⚠️ **本服务器是 cgroup v1**,路径是 `/sys/fs/cgroup/memory/system.slice/<scope>/`,
> 文件叫 `memory.usage_in_bytes` / `memory.max_usage_in_bytes` / `memory.limit_in_bytes`。
> cgroup v2 才是 `memory.current` / `memory.peak` —— **写 v2 路径会静默读到空**。
```bash
P=/sys/fs/cgroup/memory/system.slice/<scope>
cat $P/memory.usage_in_bytes # 当前(字节!不是 KB)
cat $P/memory.max_usage_in_bytes # 历史峰值 —— 等于 limit 就是「曾精确打满」
cat $P/memory.stat # ★ 关键:分 rss 与 cache
cat /proc/<pid>/smaps_rollup # Anonymous / Pss_Anon
```
**判据**:
- `rss` 占绝对多数、`cache` 很小 ⇒ **几乎全不可回收,内核 OOM killer 无路可退**
- `rss` 接近 `limit` 且 `max_usage == limit` ⇒ 配额**零余量**,任何波动就是终点
- `rss_huge` 大 ⇒ 透明大页放大占用
> ⚠️ **`ps` 的 RSS ≠ cgroup 计费**:实测 `ps` 报 437 MiB 而 cgroup 只用 381 MiB(共享页不计入)。
### 第 3 层 · 会话级(谁在吃内存)
```bash
find <用户根>/home/sessions -name session.jsonl.zstd -printf "%TY-%Tm-%Td %TH:%TM %10s %h\n" | sort -r | head -12
```
拉回本地用 `dsh-server-docs/scripts/sess-analyze.mjs` 解(**注意该脚本同目录的 `sess-list-presets.mjs` 首行曾缺 `/**` 起始符**,如报 `SyntaxError: Unexpected token '*'` 先补)。
**别忘了对照**:多实例时横向比 `dsh-instance-mem.log`(**时间戳是 UTC,+8 才是本地时间**)。无插件的实例峰值 vs 有插件的实例峰值 = 插件的净增量。
## 两条死亡路径(症状不同,别混)
| 路径 | 触发 | 日志指纹 | systemd 结果 |
|---|---|---|---|
| **A · V8 堆限** | 堆冲到 `--max-old-space-size` | `FATAL ERROR: Reached heap limit` / `Ineffective mark-compacts` | `code=dumped/status=ABRT` |
| **B · cgroup 限** | RSS 打满 `MemoryMax` | `kernel: oom-kill:constraint=CONSTRAINT_MEMCG, task=node` | `code=killed/status=9/KILL` → 平台 `exitCode 137` |
**两条都拿不到可读的应用层日志**——这正是用户觉得「莫名其妙就断了」的原因。
## 隔离复现(**唯一允许的压测方式**)
⛔ **压测走隔离环境**:在**生产实例**上压满配额 = 当场 SIGKILL,会连带把实例带进 crash-loop / 熔断(自找干扰)。⚠️ 注意 **2026-09-13 用户已明确「服务器是开发环境,不用担心中断用户」**(R8 已放宽)—— 但**别因此就去污染实例**:隔离复现能拿到同样的数据而**不留副作用**,仍是首选;只有在需要复现"平台侧联动"(如自动回滚、熔断计数)时才动真实例。
```bash
systemd-run --wait --pipe --collect --unit=memsim-x \
-p MemoryHigh=448M \
-p MemoryMax=1024M \
node /tmp/memsim/probe.mjs
```
- 独立 cgroup ⇒ **撞墙只杀模拟进程**,生产不受任何影响(宿主 available 需 > 400 MiB)
- 跑完 `rm -rf /tmp/memsim`,`systemctl list-units "memsim*"` 确认无残留
**复现路径 B(137)的配方**:必须用**纯堆外分配**才能走到 cgroup 限——
```js
const b = Buffer.allocUnsafe(4 * 1024 * 1024); b.fill(1); bufs.push(b)
```
⚠️ 若改用 JS 对象数组,会**先撞 V8 堆限(路径 A)**,永远复现不出 137。这是本轮踩到的坑。
**复现路径 A 的配方**:正常 push JS 对象即可,同时打印 `process.memoryUsage()` 看崩在哪个 `heapUsed`。
## 踩坑清单(全部实测)
1. **cgroup v1 vs v2 路径不同** —— 写错不报错,静默读到空值。
2. **`memory.*_bytes` 单位是字节**,不是 KB(差 1024 倍)。
3. **`dsh-instance-mem.log` 时间戳是 UTC**,+8 才对得上本地时间。
4. **scope 名带随机 hash**,每次重启都变,必须现查。
5. **`ps` RSS ≠ cgroup usage**(共享页不计入 cgroup)。
6. **会话目录多一层 workspace 转义目录**,别按 `home/sessions/<id>` 找。
7. **`systemd-run` 起服务不继承调用者 env** ⇒ 压测必须 `--setenv=NODE_OPTIONS=...`,否则堆限根本没生效。**自证手段**:脚本里打印 `require('node:v8').getHeapStatistics().heap_size_limit`。
8. **同质字符串会被引擎优化**:`'x'.repeat(N) + i` 走 cons string 惰性拼接(不复制前缀)→ 实测「投喂 23 GB 只涨 140 MB」的假数据。**内存类模拟实验极易骗人,优先用真实数据统计 + smaps 实测**。
9. **`.cjs` 不支持顶层 await** ⇒ 用 `await import()` 的探针脚本要存成 `.mjs`。
## 定量归因的经验值(2026-09-12 guest 实例实测)
| 组成 | 量 | 说明 |
|---|---|---|
| V8 老生代堆 | ≤`--max-old-space-size` | **这是「许可」不是「上限」**,V8 会主动把水位推满以减少 GC |
| RSS / heapUsed 膨胀 | **约 3.4×** | 实测 heapUsed 37 MiB 时 RSS 已 125 MiB(预留未用 + 回收未归还给 OS) |
| `dsh-univer-office` | **+65 MiB** | 磁盘 168 MB,含 29 MB + 10 MB 原生绑定 |
| 页缓存(node_modules mmap) | ~18 MiB | **唯一可回收的部分** |
| V8 不管但计入配额 | ~50 MiB | Buffer / 原生库 / 模块映射 |
### 内存去向实测(2026-09-12 · 空载实例 smaps 拆解)
**空载实例(零会话)已占 285 MiB**:`V8 堆/匿名 155.9` + `[heap] 58.1` + `文件映射 69.1`(其中 **node 本体 59.8**)+ `JIT 2.2`。
⚠️ **`[heap]`(malloc 区)不受 `--max-old-space-size` 约束** ⇒ 压堆限**不会**让总量线性下降。
### 会话数据几乎不占内存(重要反直觉)
实测:**整个会话(2546 事件 / 11 轮)的全部字符串只有 0.9 MB**(最大单串 40820 字符 ⇒ 工具有截断)。
⇒ **「减会话长度 / 少用 web_fetch」对内存几乎无用**;占用主体是**代码与插件加载**。
### 插件内存成本(逐个 `import` 实测)
| 插件 | rss 增量 | 备注 |
|---|---|---|
| `dsh-univer-office` | **+65.1 MiB** | 单文件 bundle 5400 行 / 6 MB,**顶层静态 import** 拉起重型依赖(连 `@puppeteer/browsers` 都在),全文件仅 2 处动态 import |
| `libsql` | +7.8 MiB | 原生绑定 |
| 平台自研 ×3(portal-entry / workspace-scoped-picker / business-plugins) | **0.0 MiB** | 自研插件写法是轻的 |
| `puppeteer-core` | **0.0 MiB** | 只 `import` 主入口、不启动浏览器 ⇒ **懒加载确实有效** |
⇒ **成本由「装了什么」决定,不是「装了几个」** —— 一个重型插件 ≈ 无穷多个轻量插件。
⇒ 治理优先级:**卸重型插件 > 让插件懒加载 > 折腾堆参数**。
⚠️ 插件的 `pnpm remove`(禁用)**必须重启实例**才真释放 —— Node 模块缓存不卸载。
**诊断结论的落地口径**:算「V8 堆上限 + 插件 + 堆外 + 缓存」总和是否 ≥ `MemoryMax`。若 ≥,就是**配额本身零余量**(配置问题,不是 bug)——此时可调项只有:① 压 `--max-old-space-size`(代价:更易撞路径 A)② 提高配额(受宿主物理内存限制)③ 减少单会话负载。
## 已知与本平台不兼容的插件
### `dsh-univer-office`(2026-09-12 定性:**架构级不兼容,配置不可修**)
两条**独立**的阻碍,缺一都打不开:
| 阻碍 | 机理 | 证据 |
|---|---|---|
| **Host → Gateway** | 插件启动 bundled Gateway(默认 `127.0.0.1:9080`),实例内 node 要连它做健康检查;而平台 nft `dsh_egress` 对 `skuid 100000-199999 → 127.0.0.0/8` 是 `reject with tcp reset` ⇒ 永远连不上 | `univer_new` 恒报 `bundled Gateway did not become ready within 10000ms`;实例内 `curl 127.0.0.1:908x` → `000` / Connection refused |
| **Browser → Viewer** | **源码硬编码** `gateway = http://127.0.0.1:${port}`、`viewerUrl = ${gateway}/?file=...`,client 拿它当 **iframe src** ⇒ 浏览器去**用户自己电脑**的 127.0.0.1 找 Viewer,那里没有服务 | `grep -o "viewerUrl: [^,]*" lib/index.js`;`grep -oE "http://127\.0\.0\.1:[^\`\"']*"` |
⇒ 它的 Viewer **假设「浏览器与实例同机」(本地部署场景)**,与托管多租户平台根本不兼容。**解封 loopback 也没用**——第二条拦在浏览器侧。
⇒ 只能改插件(viewerUrl 改走平台代理的相对路径)。**治理结论:候选池应下架 / 用户应禁用**(顺带省 **65 MiB**)。
**替代路径**:让 agent 用 python(openpyxl + python-docx + matplotlib)直接产出 xlsx/docx,已验证可行(2026-09-12)。
⇒ **评估标准已立档**:`04-调整方案/75`(**托管友好性 H1–H4** + **资源成本**;自研插件改造规范 **R-a~R-e**)。今后候选池导入与自研插件验收按该档的检查项走 —— 核心判据一句话:**「插件的一切对外交互,是否都能走平台已有的那一条入口」**(浏览器侧只用相对路径;实例侧不依赖 loopback 网络服务)。
## 注入层 / 浮层的**真机验证配方**(2026-09-13 实测,4 轮才摸清)
**为什么不能用 curl 验**:`src/supervisor/proxy.ts` 的注释写得很明白 —— **「curl 不带 Accept-Encoding,故此前验证是假阳性」**。注入只在「客户端接受 HTML → 代理把上游 `accept-encoding` 改写成 `identity` → 上游回未压缩 HTML」时发生;curl 一不留神就绕过这个判定,于是**"有标记"不代表脚本能在浏览器里跑**,而**"没标记"也可能是 curl 自己造成的**。⇒ **判定注入层是否活着,必须用真浏览器。**
**前置(R4 允许的临时会话,别用真实账号)**:
```bash
# 服务器上:建一个 10 分钟自过期的 guest 会话(ip=127.0.0.1 / ua=poc-curl2)
TOKEN=$(ssh bt-server 'cd /opt/dshs && /usr/local/bin/node mksess-guest.cjs')
# 用完立刻删(否则留下真实可用的会话行)
ssh bt-server "cd /opt/dshs && /usr/local/bin/node -e \"const c=require('crypto');const D=require('/opt/dshs/node_modules/better-sqlite3');const db=new D('/var/lib/dshs/dshs.db');console.log(db.prepare('DELETE FROM sessions WHERE token_hash=?').run(c.createHash('sha256').update('$TOKEN').digest('hex')).changes);db.close();\""
```
**工具**:`playwright-core` + `channel:'chrome'`(装在 `E:\ProgramData\.workbuddy\binaries\node\workspace`;须 `createRequire` 指向该目录,并在该目录内跑)。把 `sid` cookie 写进 context(`domain:'.alotbuy.com'`, `secure:true`, `sameSite:'None'`),再 `goto https://<user>.alotbuy.com/`。
**判定用的哨兵与元素(直接 `page.evaluate` 读)**:
| 判据 | 说明 |
|---|---|
| `window.__dshRecover === 1` | **recovery.js 全文执行完毕的哨兵**(脚本第 23 行设)—— **最强证据**,比找 DOM 元素可靠 |
| `window.fetch.toString()` 不含 `[native code]` | 证明监控层已装载(脚本包装了 `fetch` / `EventSource` / `WebSocket`) |
| `#__dshAssistBar` 存在 | assist.js 跑起来了(它 `mount()` 时建这个容器) |
| `#__dshRecover` + 文案 + `#__dshRetryBtn` | 浮层级 |
| `document.scripts` 里含 `__dsh` 的 `<script>` 数 = 2 | 两个注入脚本都下发了 |
**4 个坑(每个都让我误判过一次)**:
1. **`page.route` 拦不住 WebSocket** ⇒ 想用「拦 `/api/*`」或 `ctx.setOffline(true)` 造断流**逼不出浮层**(dsh 的主链路是 WS/SSE)。`setOffline` 也不会立即掐断已建立的 WS。
2. **`probe()` 有 15 秒节流**(`lastProbe`),而心跳每 25 秒跑一次 ⇒ **心跳会把节流窗口占掉**,你手动 `dispatchEvent(new Event('focus'))` 往往被**静默丢弃**。要么按心跳节奏等(`status` 恒 `running:false` → 连续两次软失败 ≈ 50 s),要么确保距上次探针 ≥15 s。
3. **`recover()` 成功后 0.7 秒内 `location.replace()` / `location.reload()`** ⇒ 浮层只亮 0.7 s。**任何 ≥2 秒的轮询都会全踩空**,然后你会得出"浮层没出现"的**错误结论**。
4. **`#__dshAssistBar` 本身是容器**,里面才是「📁 我的文件」「🧭 能力」两个 button —— **点容器无效**,要用 `page.click('#__dshAssistBar button:nth-child(1)')`。
**✅ 正解(纯客户端、零生产副作用)**:拦 `status` 仿真未运行 + **把 `/api/dsh/enter` 挂住不回**,浮层就会**停住不下跳**,从容观测:
```js
await page.route('**/api/dsh/status*', r => r.fulfill({ status:200, contentType:'application/json', body:'{"running":false}' }))
await page.route('**/api/dsh/enter*', () => { /* 故意永不回包 → 浮层停住 */ })
// 之后每 2 秒采样 #__dshRecover 的可见性与文案即可
```
2026-09-13 用这套验证拿到的实测结果:文案「工作区已休眠,正在唤醒…(**已等待 N 秒**)」**倒计时逐秒递增**、`DSH · AUTO RECOVERY`、AI 核心视觉 + 进度条;助手面板点开后正确列出工作区目录。**全程零页面错误。**
## 相关
- 排查完整案例:`04-调整方案/55`(会话档位)、`04-调整方案/16`(崩溃循环)
- 实例配额与内存优化:`04-调整方案/58`
- 会话档位是「会话创建时播种」的,老会话不跟随平台默认:`04-调整方案/33`
@@ -0,0 +1,133 @@
---
name: dsh-knowledge-upkeep
description: dsh 平台文档库 / 项目知识的**维护与纠偏方法**。当发现「文档与现状不符」「同一事实多处打架」「知识越积越碎」「AI 忘了某条规则」「要收敛或重构知识库结构」时使用;也用于定期体检。核心 = 六层知识结构(L0-L5)+ **分层判据(实体 vs 指针)** + Lint 四件套 + **漂移处理 SOP** + **自动化的边界(检测可自动,改写不可自动)** + 今天踩过的 5 个反例。**配套:`dsh-feature-first`(谁定什么)· `dsh-decision-method`(怎么定得对)· `dsh-change-workflow`(怎么落地)。**
version: 1.0.0
updated_at: 2026-09-12
agent_created: true
---
# dsh-knowledge-upkeep — 知识库维护方法
> **一句话**:知识库不会自己变好,**只会慢慢变错**。本技能把"纠错"从临时动作变成**可复跑的方法**。
> 素材来源:2026-09-12 全库校验实证(20 项违背 → 0)。
---
## 0. 为什么需要它(实证,不是理论)
| 现象 | 实测 |
|---|---|
| **同一事实被抄很多份** | 现行域名出现在 **21 个文件 / 123 处**;档案"下一号"写在 4 处且**互相打架**(72 / 69 / 53 / 20) |
| **过时值长期存活** | 旧配额活在 15 个文件;实例 MemoryMax 曾同时存在两个"现役值" |
| **权威源头本身是错的** | `BRIEF.md`(首读现状卡)自己带着旧值 → AI 老老实实读了它,读到就是错的 |
| **校验工具查不到** | 原 `docs-audit.py` 只查**结构**(编号/悬空/重复),**不查事实** → 漂移不可见 |
| **规模已超"全量塞入"上限** | 库 = 93 md / 555,533 字符 ≈ **388,873 tokens**;Karpathy 模式实证 **~100 篇就崩**(模型开始略读并给出自信的错误答案)|
---
## 1. 知识结构:六层 + 单一来源
| 层 | 内容 | 唯一权威 | 变更频率 | 送达方式 |
|---|---|---|---|---|
| **L0 不变量** | 架构与机制(为什么这样设计) | `01-规划与架构` + 早期档案 | 年 | 按需 |
| **L1 现行值** | 现在到底是什么(域名/配额/端口/路径/阈值) | **`BRIEF.md`** | 周 | 首读 |
| **L2 规则** | 必须遵守(提问判据/红线/提交边界) | **`CODEBUDDY.md`** | 很少 | **自动注入** |
| **L3 方法** | 怎么想、怎么做 | 3 个 dsh 技能 | 月 | 按需触发 |
| **L4 状态** | 现在在哪(待办、落差) | `交接单/` + `.workbuddy/memory/MEMORY.md` | 天 | **自动注入** |
| **L5 历史** | 怎么变成现在这样 | `04-调整方案/` + `archive/` | **只增不改** | 按需 |
**三条铁律**
1. **每个事实只有一个权威** —— 其余文件**只许写指针,不许复制数字**。
2. **L5 冻结** —— 历史档案里的旧值**不回改**(写的时候是对的,回改破坏历史);需要修正时**在文末追加「修正(YYYY-MM-DD)」小节**,并改 L1 的现值。
3. **校验必须能查"事实一致性"** —— 否则前两条必然失守。
---
## 2. 分层判据:**什么必须实体,什么可以只给指针**
> **"不做会违规、会出事"的 → 实体保留;"看了更准但不看也不违规"的 → 才给指针。**
| 处理 | 内容 |
|---|---|
| **实体保留**(压缩不许删语义) | 提问判据 · 红线 R1–R8 · 提交边界 · 规划/执行分离 · 并发纪律 · **会导致事故的实测事实** · 环境要点 |
| **只给指针** | 平台现状与历史细节 · UI 规范全文 · 档案模板细则 · 某功能的实现内幕 |
⚠️ **指针必须绑定可识别的动作**,否则"去查"不会发生:
`当你要写前端页面 → 先读 06-UI规范`,而不是 `详见 06-UI规范`。
---
## 3. Lint 四件套(可复跑,退出码可接 CI)
```bash
python3 scripts/docs-audit.py # 结构:编号冲突 / 标题号不符 / 悬空引用
python3 scripts/docs-manifest.py # 刷新机读清单 docs-manifest.json(含被引次数/tier)
bash scripts/docs-sync-check.sh # 双端对账(本机 ↔ /opt/dsh/docs)
python3 scripts/docs-consistency.py # 事实:写死取值 + 跨页取值冲突
```
**`docs-consistency.py` 的设计要点**(写它时踩过的坑,别重踩):
- 只查**高置信**模式。首版把「旧域名」「旧配额」当违规 → **几乎全是误报**(库里都是"旧域已 301" / "512M→384M" 的合法表述)。
- **引号感知**:引号内的匹配 = **引用历史**,不是断言,不算违规。
- 判据 = 「**承诺现行**的文件不许含已废止/写死/矛盾的取值」;
**历史豁免**:`04-调整方案/**`、`archive/**`、`01-规划与架构`、`02-运维手册`。
---
## 4. 漂移处理 SOP(发现 → 收敛,五步)
```
① 发现 → 四件套任一项退出码非 0
② 定性 → 真漂移 / 合法历史表述 / 校验器误报 ← **这一步不能跳**
③ 定权威 → 这个事实的**唯一权威**是哪一层哪个文件(见 §1)
④ 收敛 → 改权威文件;其余副本改为指针或删除;写死值改为"复跑取号"
⑤ 复跑+同步 → 四件套全绿 → scp → 复跑 docs-sync-check.sh
```
**② 定性是分水岭**——2026-09-12 首跑报 20 项,逐条看上下文后:
- 真漂移 **≈6 项**(下一号三处矛盾、MemoryMax 两处取值不一致、`maidou`、旧路径、SSH 端口)
- **合法历史 14 项**("旧域已 301" / "512M→384M" 的迁移表述)
⇒ 若不做定性就批量改,会**破坏历史档案**并制造新错误。
---
## 5. 自动化的边界(**重要**)
| 动作 | 可否自动 | 理由 |
|---|---|---|
| **检测**(跑四件套、出报告) | ✅ **可以,且应该** | 只读、零风险、退出码可判定 |
| **刷新派生件**(`docs-manifest.json`) | ✅ 可以 | 纯派生,无人工语义 |
| **改写正文 / 批量替换** | ❌ **不可以** | ① 触 **R7**(批量写入须确认)② **认识论漂移**:LLM 改知识库后,错误会成为后续输入并**复利放大**(LLM Wiki 社区已实证)③ 研究员明确指出:**git diff + 人工审阅才是真正的安全网** |
| **删历史档案里的旧值** | ❌ 不可以 | 违反铁律 2(L5 冻结) |
⇒ **推荐形态**:自动任务**只做"体检 + 出报告"**,**发现违背时通知人**,由人或新会话按 §4 SOP 收敛。
---
## 6. 反例(今天真实踩过,别重犯)
| # | 反例 | 后果 | 规避 |
|---|---|---|---|
| 1 | 校验规则太宽(拿"旧域名"当违规) | 20 项里 14 项误报 → 校验器被忽视 | 只留高置信模式;拿不准就不查,改人工 |
| 2 | 改了**技能工作副本**却忘了**归档副本** | 校验器扫的是归档副本 → 改了等于没改 | 技能两处位置必须同步(md5 一致) |
| 3 | 备份文件 `*.bak-*` 留在**文档库内** | 污染 `docs-sync-check`(算成"仅本地") | 备份放库外,或收尾删掉 |
| 4 | scp 时把 `scripts/x.py` 也推到**根目录** | 服务器多一份同内容副本 → 对账报"仅服务器" | scp 目标路径逐个核对 |
| 5 | 把"下一号"写死在入口文档 | 并行改动 3 次打穿(83→85→87) | 一律"复跑取号" |
| 6 | **只看根目录就说"本机没有这个文件"** | 实际在 `scripts/` 下 → 结论完全相反(今天两次) | 报"不存在/缺失"前先 `find` 全库 |
| 7 | **把问题抛给用户前没确认它还在** | 服务器多余文件已被并行会话清掉 → 问了个**已消失**的问题 | 抛出前重新查一次;状态会被别人改变 |
| 8 | 没顺着 grep 找权威档案就猜文件用途 | 直接读关联档案的 TL;DR 一句话就清楚,比猜快得多 | `grep -rn <文件名> --include="*.md"` → 读那条档案的头部 |
---
## 7. 自检清单(每次动知识库前后)
**动手前**
1. 这个事实的**唯一权威**在哪一层?我是不是正准备在别处复制它?
2. 我要改的是**承诺现行**还是**历史**文件?历史的 → 不回改,追加"修正"小节。
3. 涉及 >10 文件?→ 先出清单(R7)。
**收尾**
4. 四件套**全绿**了吗?
5. 两处技能副本 md5 一致吗?推服务器了吗?
6. 临时/备份产物清了吗?
7. 复数入口都改成"复跑取号"了吗?
@@ -0,0 +1,528 @@
---
name: dsh-opensource-release
description: DSH 多租户托管平台的「开源导出与版本迭代」技能 —— 把私有代码仓导出成可公开的开源副本(脱敏 / 去插件 / 分层授权 / 重写说明文档),并在后续版本里安全地重跑导出、登记版本。当用户说「开源一份」「导出到 GitHub」「发新版本」「改一下开源那份的脱敏/授权/说明」「开源那份同步一下」时触发。核心:**源仓库只读** + **阻断性探针 0 命中**才准放行 + **OVERLAY 手工撰写层**不得被重建抹掉。
version: 1.6.0
updated_at: 2026-09-15
last_change: 1.6.0(2026-09-15):用户立 **K8s 长期策略** —— 「后续获取开发项目代码时**跳过** K8s 相关部分,**就用当前清除/修改后的版本**」⇒ 新增 **R-O14**(源仓那侧视为**废弃分支**;判定标准 = 构建 `blocking hits: 0`),并**修掉会误导未来会话的过期事实**:① §2「保留」列表原写 `deploy/`(11) 与 `poc/`(16) **要保留** —— 与 K8s 政策**直接冲突**(会让未来会话把它们加回来)⇒ 改为「K8s 相关一律不带」+ 指向 `_K8s排除台账.md` / `_k8s_scan.py`;② §0 的 `dsh-web-platform` → **`dsh-users-platform`**、中文母本 → **英文为主(`*.zh-CN.md`)**、首发行 → **v1.1.0 / 153 文件 / 历史已重置为单条提交 `a327649`**、补计数现状(OVERLAY 26 · REQUIRED 54 · INCLUDE_DIRS 6 · EXCLUDE_FILES 9 · DROP_SCRIPTS 8);③ 标注 `K8S_SEMANTIC_RULES`(清「不含 `k8s` 字样但语义已不成立」的描述)与 `_apply_k8s_semantics.py`(本机 `--force` 被安全删除层拦下时的**热应用**替代路径)。1.5.2(2026-09-13):`assets/` 漏收录 + 空括号残渣两处真问题。
agent_created: true
---
# dsh-opensource-release — 开源导出与版本迭代
## 何时用
- 要把 `dsh_shenxian`(私有部署版)导出成可公开的仓库副本;
- 要**发新版本**(v1.0.1 / v1.1.0 …)→ 重跑导出 + 登记版本表;
- 要改开源那份的**脱敏口径 / 保留范围 / 授权结构 / 说明文档**;
- 有人问「开源那份怎么维护 / 怎么保证不泄密」。
**不适用**:日常平台改造(用 `dsh-change-workflow`)、知识库维护(用 `dsh-knowledge-upkeep`)。
---
## 0.5 🔴 事故清单(真发生过 —— **开工前 30 秒读完**)
| # | 事故(真实) | 正确做法 | 详见 |
|---|---|---|---|
| 1 | **误放别的会话的全局执行锁** —— 抢锁失败**没看输出** + `claim`/`release` 写在同一条命令 ⇒ 末尾的 `--release-exec` 是 `rm -rf` 语义,删掉了 `R1-注入层-1104` 的锁 | 抢锁**单独一条命令**并**当场看输出**;看到占用者不是自己 ⇒ 停手;放锁前 `cat 交接单/.exec-lock/OWNER` 确认首行是自己;**误放**则按 `handoff-guard.sh:43` 格式原样重建**并告知用户** | §8 坑 16 |
| 2 | **导出基线在漂** —— 导出脚本复制的是**工作树**,而多会话在并行改源码仓 ⇒ 导出混入**别人未提交的改动**,还会冒出新的需脱敏标识(`'dsh-plugin-mcn-suite'` 触发探针) | 发布前**按 commit 取**(`git archive <commit>`)=冻结版本;否则**每次重建都必须重跑探针**并复核 README 描述与实际一致 | §10 |
| 3 | **盲替 `_build_export.py` 自身** —— 脚本里同时有「源模式」与「目标值」,批量替换把**源模式**改掉 ⇒ 改名规则**静默失效** | 改脚本只用**精确 `Edit`**;改完**必须重建 + `grep` 复查** | §8 坑 9 |
| 4 | **`Dockerfile` / `Dockerfile.dsh` 整份跳过脱敏** —— `is_text()` 按扩展名判断 | `is_text()` 已加特判;**新增无扩展名文件**要复核是否进了脱敏 | §8 坑 10 |
| 5 | **`package.json` 手改被重建覆盖** —— 它属「源派生」而不是 OVERLAY | 项目自有字段(version/description/repository/license)必须写成 **GLOBAL 规则** | §8 坑 11 |
| 6 | **废弃的中间候选名静默残留** —— 探针只探最老的名字,`_overlay` 里的中间名(`dsh-hosting`)漏了 72 处 | **所有曾用名**都进 `LEAK_PROBES` | §8 坑 14 |
| 7 | **说明文档文案连改 7 轮**(替别人宣传 / 开头讲基线 / 议论式表述 / 授权在最前 / 把未验证的当可用 / 致谢太长) | 严格照 **R-O9–R-O12** 写;**写完自审一遍**再交付 | R-O9–R-O12 |
| 8 | **脚本里用键名当标题**(`marks[k][0]` 拿到的是键 `"A"` 不是标题)⇒ 结构改写打歪,误删 `PoC` 的一个字母 | 结构改写用**完整标题字符串**做锚点;改完**读回原文复核**关键块 | 本表 #7 的同一节 |
| 9 | **改工作根时漏改脚本常量 ⇒ 旧目录被"重建复活"**(2026-09-13 迁移到 `dsh-laijing-github` 时:先搬目录、后改 `OUT`,中间跑了一次重建 ⇒ 旧位置被重新建出 135 个文件,且**因旧位置没有 `_overlay` 而缺了 6 个手工层文件**;`_overlay` 本身侥幸没被洗掉) | **顺序必须是:① 改脚本常量(`OUT`+`EX`)→ ② 再搬/删目录 → ③ 重建验证**。迁完**必查旧目录没有复活**(`ls`),并核对 `find <repo> -type f \| wc -l` 与手工层 6 个文件是否都在 | §8 坑 17 |
| 10 | **白名单收录静默漏项:`assets/` 没进 `INCLUDE_DIRS`**(2026-09-13 用户问"确认都同步了吗"时查出)—— 导出的仓库缺 `assets/inject/{recovery,assist}.js`:`proxy.ts` 的 `loadInject` 会 **fail-fast 抛错**(平台起不来)、仓库自带 `scripts/verify-inject.cjs` 也会**判失败**;`package.json` 的 `files` 同样缺 `assets`(npm/git 安装也会缺) | 已补 `INCLUDE_DIRS`、`package.json` files 规则,并**新增 `REQUIRED_EXPORT` 清单(**23 项**)**:缺任何一项 ⇒ **构建判失败**(`return 1`)。以后新增"运行时要读的文件"必须同步加进该清单 | §3 · §8 坑 18 |
| **10** | 🔴 **"顺手清理"的正则把 ASCII 标点也吃进去 ⇒ 直接改坏源码**(2026-09-13 去「档案 NN」时:清理规则写成 `[((]\s*[))]` / `[;;,,]\s*[))]`,**字符类里混了 ASCII `(` `)` `,` `;`** ⇒ 源码里所有 `foo()` 被删成 `foo`,`whitelist.ts` / `security-scan.ts` 当场语法错(tsc 报 *Invalid character* / *Unterminated string literal*)。**而当时 leak 探针全过**) | ⛔ **清理类正则只准碰全角标点(()·、,:;),绝不可把 ASCII 语法符号写进字符类**;<br>✅ **改完必跑 `_verify_tsc.mjs`,`exit 0 + no diagnostics` 是"没改坏代码"的唯一证据** —— **探针全过 ≠ 代码还活着**,两者查的是完全不同的东西;<br>另:`(\s*/\s*` 这类"吃掉前导斜杠"的规则会毁掉 `(/api/x)` 路径,一律不要写 | 本表新条目 · 台账 §八 D |
| **11** | 🔴 **同一个名字既当"脱敏目标"又当"公开值" ⇒ 自相矛盾**(2026-09-13 填 `PUBLISH_*` 时:`maogeigei` 同时在 `GLOBAL`(替换为占位符)与 `LEAK_PROBES`(判泄露)里 ⇒ ① 刚填好的公开值被规则**改回占位符**,② 探针报 `blocking hits: 4`。**"占位符残留 0"是假象**) | ⛔ **任何进入 `PUBLISH_*` 的值,先 `grep -n "<该值>" _build_export.py` 确认它不在 `GLOBAL`/`LEAK_PROBES`/`INFO_PROBES` 里**;升格为公开身份后要**同时**从 GLOBAL **与** 探针移除(只删一处 = 另一种错);<br>公开联系方式的兜底应靠**更具体的探针**(如私有仓库域名 `work.alotbuy`),不要靠账号名 | 台账 §八 D2 |
| **12** | ⚠️ **`--dry-run` 报"假成功"**(2026-09-13 真机预演时:`run()` 会把命令加 `[dry-run]` 前缀跳过执行,**但结果提示语是硬编码的** ⇒ 预演满屏 `✓ 构建完成` / `✓ 服务已启动` / `✓ 部署完成`;更糟的是**打印了一个假的初始管理员密码** —— `bootstrap-admin` 根本没跑,用户会照着登录失败。另:未设域名时文案出现空缺「把 与 *. 的 DNS A 记录」) | **dry-run 必须"只读":既不落盘,也不得宣称成功**。所有**结果类提示**(不只是动作)都要有 `DRY_RUN` 分支;**凡是"执行后才产生的值"(密码/ID/路径)在 dry-run 下必须标为"未生成"**;文案里的变量要有 `${VAR:-默认}` 兜底。<br>**验收方式**:`--dry-run` 的输出里**不允许出现任何 `✓`**,只允许 `[dry-run]` | 台账 §八 E |
| **13** | 🔴 **`_build_export.py --force` 在本机跑不动**(2026-09-14 实测:`shutil.rmtree(DST)` 被 WorkBuddy 的「安全删除层」shim 拦下 ⇒ `SAFE_DELETE_FAIL_CLOSED`;⚠️ 而且 `--force` 会**连导出物里的本地 `.git` 一起删** —— 本轮实测提交历史被吃掉后由会话重新 `git init` 建单条提交) | ① 新规则要**抽成具名列表**(如 `K8S_SEMANTIC_RULES`)再 `REGEX_RULES += …`,不要直接内联 —— 具名才能被热应用工具复用;<br>② 本机改规则后用 `<导出根>\_apply_k8s_semantics.py --write` **就地热应用**(不删任何东西,结果与整目录重建**逐字节一致**,且幂等:复跑命中 0);<br>③ 确实要整目录重建:先 `mv dsh-users-platform/.git <导出根>/_keep_git_tmp`,重建完 `mv` 回来 | 台账 §五·补 |
| **14** | 🔴 **「K8s 残留」只按 `k8s` 字面量清 ⇒ 清不干净**(2026-09-14:字面量已清零,仍有 **10 处注释**在讲 `Pod` / `file sidecar` —— 描述的是**已被移除的 K8s 形态**,属悬空描述) | 判据是**「这段描述在单机形态下还成立吗」**,不是「有没有 `k8s` 字样」:`Pod`(K8s Pod ≠ DSH 子进程)· `sidecar`(已移除的 per-user file sidecar)· `a Linux Pod` 都要清;<br>⛔ **噪音不要清**:`--profile headless`(dsh 自己的 profile 名)· `headless-univer`(第三方插件包名)· `manifest`(npm 包清单,不是 K8s manifest)· `egress`(nftables 出网护栏,本项目**保留**能力);<br>复核用 `<导出根>\_k8s_comment_scan.py [--wide]` —— 它按「**注释 / 代码**」分类输出,可直接核对「只清注释」这件事 | 台账 §五·补 |
| **15** | 🔴 **只删「构建步骤」、没删「使用者」⇒ CI 必红**(2026-09-15 实测:撤下 `Dockerfile.dsh` 后,构建 `dsh:ci` 镜像的那条步骤删了,但 **3 条使用者仍在** —— `Smoke — dsh resolves runtime plugin` / `Trivy — dsh` / `Push dsh to ACR` ⇒ 首次推送 CI 必红) | 判据 =「**产物不在,引用它的步骤也不该在**」:删任何产物(镜像 / 文件 / 模块 / 脚本)时,**必须把它的「使用者」一并处理**(本次已把 3 条步骤整块删除 + 把 `dsh:ci` 加进 `LEAK_PROBES`);<br>⚠️ 复核**务必用 `os.walk` 脚本**(`_k8s_comment_scan.py`),`bash grep -r` 会漏隐藏目录 —— 见 #16 | 影响说明 §7 |
| **16** | 🔴 **`bash grep -r` 不遍历隐藏目录 ⇒ 静默漏掉 `.github/`**(2026-09-15 实测:据此一度误判「CI 没问题」,靠 Read 才看到 `dsh:ci` 仍在) | 全仓复核用**自带脚本**(走 `os.walk`)或给**显式路径**;<br>本机另注:bash 的 `PATH` 会被 shim 重置(`dirname`/`grep`/`awk` 全 `command not found`)⇒ 先 `export PATH=<PortableGit>/usr/bin:<…>/mingw64/bin:/c/Windows/System32:/c/Windows:$PATH`,`python` / `node` 一律用**绝对路径** | 影响说明 §8 |
---
## 0. 事实(硬编码,勿猜)
| 项 | 值 |
|---|---|
| **中文名 / English name** | **DSH 用户平台** / **DSH Users Platform**(2026-09-14 由 `dsh-web-platform` 定稿改名) |
| **文档语言(2026-09-14 定稿,覆盖 R-O13)** | **英文为主**:主文档 `*.md` 为英文 + 顶部中文入口;中文全文在 **`*.zh-CN.md`**;**`manual/` 8 篇 ×2 语言**;架构图也分语言(`diagrams/architecture{,.zh-CN}.svg`) |
| **内容来源(用户 2026-09-13 定的重点卖点)** | **全部代码与文档由 AI 生成** —— 模型 **DeepSeek V4 / V4.1 flash**(**不写工具名**)。README 里有专节 `## AI 生成`(**位于「目录」之后、「亮点」之前**),Hero 区还有**一行声明 + 2 个徽章**(`code & docs-AI-generated`、`DeepSeek V4 / V4.1 flash`)—— **这三处一个都不能少** |
| ~~**文档语言**~~ | ⛔ **本行已作废** —— 2026-09-14 起改为**英文为主**,见上表「文档语言(2026-09-14 定稿)」行;<br>**R-O13 的「中文母本 + `*.en.md`」口径同时作废**,现状是 `*.zh-CN.md` 副本 |
| **技术标识(仓库·包·服务·env 前缀)** | **`dsh-users-platform`** | `DSH_USERS_PLATFORM_*` | `/var/lib/dsh-users-platform`(中文名「DSH 用户平台」/ English「DSH Users Platform」) |
| **曾用名(全部必须在 `LEAK_PROBES` 里)** | `dshs` · `dsh-multitenant` · `dsh-hosting` · **`dsh-web-platform`** · `DSH_WEB_PLATFORM_*` · `/var/lib/dsh-web-platform`;`taimiao` 未落地也一并加入 |
| **前名(已废弃,现为阻断探针项)** | `dsh-multitenant` / `DSH_MULTITENANT_*` / `/var/lib/dsh-multitenant` —— 2026-09-13 20:2x 由用户定名 `dsh-web-platform` 取代;`LEAK_PROBES` 已收录,**出现即判泄露** |
| **源仓库(只读!)** | `D:\github\dsh_shenxian` |
| **工作根(GitHub 开源专用文件夹)** | `E:\ProgramData\AI技能\dsh-laijing-github\`(2026-09-13 由 `aliyun-dsh-server\_开源导出_20260913\` 整体迁入;**本项目的独立开源工作区**,不再是 `aliyun-dsh-server` 的子目录) |
| **仓库根(可直接 git init)** | `<导出根>\dsh-web-platform\` |
| 构建脚本 | `<导出根>\_build_export.py`(**默认拒跑**,须 `--force`) |
| 手工撰写层快照 | `<导出根>\_overlay\`(脚本自动维护) |
| 类型检查 | `<导出根>\_verify_tsc.mjs`(建 junction → tsc → `rmdirSync` 拆) |
| 给人看的台账 | `<导出根>\_导出说明与脱敏台账.md`(**不随仓库上传**;§七 = 改名记录) |
| 手工撰写层(`OVERLAY`:重建时自动快照→恢复,**不得被抹掉**) | **现为 3 份**:`README.md` · **`PLUGIN-PORTING.md`** · `install.sh`(原另有 `LICENSE` / `LICENSE-UPSTREAM-MIT.txt` / `THIRD-PARTY-NOTICES.md`,**2026-09-13 已按用户要求移除** —— 见 §5) |
| 当前版本 | **v1.1.0 / 2026-09-14**,**153 文件**;历史已按用户要求**重置为单条提交**(`a327649`,强推覆盖)| 首发 v1.0.0 / 2026-09-13 |
| 计数现状(2026-09-15) | `OVERLAY` **26** | `REQUIRED_EXPORT` **54** | `INCLUDE_DIRS` **6** | `EXCLUDE_FILES` **9** | `DROP_SCRIPTS` **8** |
| 上游基线(三方) | `上游骨架仓库(已按要求不再具名)` → **MIT**(GitHub 仓库 + DSH 插件目录已收录) |
| 运行期上游 | `@deepseek-ai/dsh`(DeepSeek Harness)→ **MIT** |
| 本机 Python | `/e/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe` |
> ⚠️ **工作根 = `E:\ProgramData\AI技能\dsh-laijing-github\`**(独立 GitHub 开源文件夹)。**不要再改名或迁移** —— 这个名字被 `_build_export.py` 的 `OUT`、`_verify_tsc.mjs` 的 `EX`、本技能、项目 MEMORY 同时引用。真要迁:先改这几处,再**复跑重建 + tsc**,并检查旧目录**没有被重建脚本重新建出来**(迁完曾因漏改 `OUT` 而复活一次)。
---
## 1. 硬规则 R-O1–R-O13(= 用户原始要求 + 实际踩过的坑,任何会话**不得放宽**)
| # | 规则 | 判据 |
|---|---|---|
| **R-O1** | **源仓库只读** | 所有改写只落在导出副本。任何 `git`/写操作碰 `D:\github\dsh_shenxian` = 违规 |
| **R-O2** | **去掉本机 / 服务器信息** | 域名 / IP / 账号 / 绝对路径 / 私有仓库地址 → **占位符或通用化**;收尾探针 **0 命中** |
| **R-O3** | **去掉所有已投放的插件** | 插件包目录 + 插件投放脚本 + 构建产物(`*.tgz`)全部不带;**代码注释里的插件名也要中性化** —— ⚠️ **唯一例外见下表**:`dsh-univer-office` 经用户 2026-09-13 特许保留 |
| **R-O4** | **不带任何项目文档与 skill** | `docs/`、档案类 md、`skills/`、`.workbuddy/`、`lib/`、**`.git`** 一律不带;只留 4 个新写文档(见 §6) |
| **R-O5** | ~~授权:个人/非商业免费 + 商业收费~~ → **2026-09-13 用户定:授权类内容全部移除、不再处理、不再上抛** | 见 §5;**但「上游 MIT 不得被附加限制」是事实,保留** |
外加**操作纪律与文档口径(R-O6–R-O12)**,每条都对应 §0.5 里的真实事故:
- **R-O6** 未经明确要求 **不做 `git init` / commit / push / scp**(同项目 §4)。
- **R-O7** 导出的**手工撰写层**(README / LICENSE / NOTICE / PLUGIN-PORTING / install.sh)是资产,**不得被重建抹掉** —— 靠 `_overlay` 自动快照恢复。
- **R-O8 · 命名** —— **绝不沿用三方项目名**(详下)。
- **R-O9 · 能力宣称** —— **只写「已验证的」**:有代码 + 有单测 + 有 PoC **都不等于可用**;未验证的标「实验性 · 未验证」(详下)。
- **R-O10 · 出处** —— 只放**文末**,且**只写一行致谢**;不写骨架枚举、不写自我表扬、不写改名理由(详下)。
- **R-O11 · README 内容口径** —— 开头写**重心**不写门槛 · **面向读者**不写议论 · 每条亮点**带机制与数值** · 亮点**先读码再写**(详下)。
- **R-O12 · README 结构顺序** —— 认知漏斗 Hook→Onboarding→Content→Trust→Meta · **授权压轴** · Hero 前 50 行有可视块 · TOC 锚点机检(详下)。
- **R-O13 · 双语文档** —— **中文是母本**;英文版**在同步 GitHub 之前**才生成(`*.en.md`),两份顶部再加语言切换行;法律文本不翻译(详下)。
### R-O8 · 命名规则(2026-09-13 立,因踩过)
> 上游 `dshs` **是 上游作者(已按要求不再具名) 的三方项目**(GitHub 仓库 + **已被 DSH 插件目录收录**,含 L1–L5 验证记录)。
> 沿用它当发布名 = **冒名 / 指代混淆**,还会让上游作者的工作被误认作本项目产出。
**发布前必须做三件事**:
| # | 动作 | 判据 |
|---|---|---|
| 1 | **起自己的名**,并**实测未被占用**(npm `registry.npmjs.org/<name>` + 插件目录 `dshbase.com/plugins/<name>`,两处都 404 才可用) | 本项目命名口径(用户 2026-09-13 定):中文 **DSH Web 平台** | English **DSH Web Platform** | 技术标识 **`dsh-web-platform`**(短横连写用于仓库/包/服务/env 前缀)。<br>⚠️ **已排除的两个候选**:**`dsh-hive`**(第三方插件 `llluchy/dsh-hive` 已占用);**`dsh-hosting`**(本会话中间候选,已被用户口径取代 ⇒ **仓库里不得残留**,已列入阻断性探针) |
| 2 | **改完全部自指标识**:包名 / bin / cordis plugin id / env 前缀(36 种)/ 数据根 / systemd 单元 / nginx conf / 库文件名 / 镜像与 k8s / 导出目录名 | 全仓 `grep` 旧名,**只剩出处引用**才算干净 |
| 3 | **保留出处并正式致敬 —— 但只放在文末**(用户 2026-09-13 明确:"在最后提一下引用了谁就行,不要上来就重点讲用了谁") | ① `LICENSE-UPSTREAM-MIT.txt` **逐字节不动**(MIT 强制);② README **不得在开头单独设「名称与渊源 / 基于 XX」章节** —— 出处只出现在**文末**的「第三方组件与致谢」里,**一句话带过**;③ 法律归属由 `LICENSE` 第一层说明 + `THIRD-PARTY-NOTICES.md` §4 承载(这两处必须完整,不受"轻描淡写"影响) |
⚠️ **改名时最容易误伤的两处**:① 把「致敬上游」的引用一起改掉(= 抹掉出处,法律与道义双重问题);② 把脚本里作**源模式**的旧名改掉(见 §8 坑 9)。
### R-O9 · 只写「已验证的」能力
> 用户原话:「模式 B · Kubernetes **去掉这个,根本没验证**,应该是**待开发验证**」。
| 规则 | 做法 |
|---|---|
| **宣称必须可复现** | 只把**真正跑过端到端验收**的路径写成「可用」。**有代码 + 有单元测试 + 有 PoC 记录,都不等于可用** |
| **未验证的照实标注,不删代码** | 保留代码与清单,但标注 **「实验性 · 未验证」**,并与可用路径**在同一张表里用状态行对照**,不要让读者自己猜 |
| **删掉「已完整落地 / Phase 0–4」式表述** | 这类词最容易把"写完了"说成"验证过了"(2026-09-13 从「部署形态」章节删掉的就是它) |
| **配置表 / 脚本文案同步标注** | 未验证路径的 env 变量统一加「(模式 X · 未验证)」前缀;`install.sh` 的报错/提示文案要与 README 口径一致 |
| **FAQ 给一句直答** | 加一条「X 现在能用吗?」→ 直接回答「**还不能**」,并说清**缺哪一步**(如「从未做过端到端部署验收」) |
| **归属措辞也要改** | 出处/致谢里描述对方贡献时,把「双部署**形态**」降级为「双部署**框架**」,避免暗示未验证的路径是可选项 |
| **落点清单(照抄)** | README:部署形态表 + 快速开始 + 配置表 + 功能详解 + 安全模型 + 亮点 + 目录结构 + FAQ;`install.sh` 文案;`LICENSE` 与 `THIRD-PARTY-NOTICES.md` 的归属措辞 |
---
---
### R-O10 · 出处的位置与措辞
> 用户原话:「在最后提一下引用了谁就行,**不要上来就重点讲用了谁**,感觉是在宣传别人的项目,已经很多地方深度改造过了。」
| 规则 | 说明 |
|---|---|
| **位置** | 只在**文末**(README 的「第三方组件与致谢」+ `LICENSE` 第一层 + `THIRD-PARTY-NOTICES.md` §4)。**不得**在开头/前部单设「名称与渊源」「基于 XX」章节 |
| **主语** | 正文一律**以本项目为主语**。写「本项目做了…」,**不要**写成「上游提供了骨架,我们在此基础上…」这种自我降格的框架 |
| **措辞** | 用「**起步时参考了** X(作者,许可证)—— **感谢作者开源**」;**不要**用「仅仅接着往前走了一步」「离开它就没有这个项目」这类过度抬举,**也不要**补「我们做了大量深度改造」这类**自我表扬**(见下表「致谢只写一行」) |
| **边界** | 「轻描淡写」**只作用于 README 正文**;MIT 要求的版权与许可声明(`LICENSE-UPSTREAM-MIT.txt`)与授权分层说明**一处都不能省** |
| **JS/注释里的名字** | 代码注释里也**不主动**出现上游名(它们已经被 R-O8 第 2 步改成新名;只剩出处语境才允许) |
| **致谢只写一行**(2026-09-13 第七次纠正) | 出处行 = **「起步时参考了 X(作者,许可证)—— 感谢作者开源」**,一行结束。**不要**写:① 骨架范围的枚举(那是 `LICENSE` 第一层与 `THIRD-PARTY-NOTICES.md` §4 的职责,**重复即冗余**);② 「此后我们做了大量深度改造」(**自我表扬**,致谢不是讲功绩的地方);③ 「项目名从它的 X 换成我们的 Y(避免指代混淆…)」(**内部事务**,读者不关心,改名说明留在 NOTICE 的「更名说明」行即可)。用户原话:「**有必要讲这么多吗,好好想想**」 |
| **别解释我们为什么这么写** | 「—— 这是 MIT 的硬性要求,必须保留」「因此 README 与 install.sh 一律以模式 A 为准」这类**关于文档自身**的话术删掉;正文只陈述事实与规则本体 |
| **指针行不重复** | 「机制见亮点」这类导航指针**全文留 1–2 处**(Hero + 功能详解)即可,Content 各节末尾各来一句 = 冗余 |
---
### R-O11 · README 内容口径
> 🧭 **体例 = 说明书,不是技术文档**(2026-09-13 20:4x 用户定):「readme 是说明不是技术文档 用语需要简明扼要 排版要方便观看」。
> ⇒ 只写**怎么用 / 有什么 / 限制**;机制级细节(链名、flag、常量、函数语义)留给代码注释与 `PLUGIN-PORTING.md`;
> ⇒ **不写内部变更日志式长表**(如「v1.0.0 的六组改造」);表格单元格要短、可扫读;多用列表少用长段落。
> 用户原话:「部署到一台服务器,多个用户注册并经管理员审核,各自获得一套相互隔离 —— **这个只是项目的基础不是重点**,重点是多租户各自进程的安全隔离、状态恢复、插件和技能的管理。**多看看项目找出这个项目的亮点**」「对了多想想 **不要弄了半天亮点都体现不出来**」。
| 规则 | 做法 |
|---|---|
| **开头 = 亮点,不是门槛** | 开篇**不要**写「部署到一台服务器,多个用户注册…」这类「能跑」描述(那是基线)。改成**一句定位 + 三条主线速览**:① 进程级安全隔离 ② 故障自愈与状态恢复 ③ 插件与技能的受控管理 |
| **亮点必须先于功能清单** | README 的第一个正文章节就是**亮点**(`## 亮点`),且**要早于**「快速开始 / 功能详解」。功能清单只做**索引**,并在开头声明「机制见亮点」 |
| **每条亮点必须带机制** | 禁止「真隔离」「会自愈」这类空词。每条要写**具体机制 + 关键数值/常量 + 为什么这么设计**(例:端口守卫要在 OUTPUT 链按**客户端 uid** 匹配、且 `-I OUTPUT 1` 插链首,否则 ufw/conntrack 的 ACCEPT 会先吃包;宿主不支持就 fail loud) |
| **先读代码再写亮点** | 亮点**必须来自实际读码**(`src/**` 的头注与关键函数就写得很好),不要凭 README 旧文或印象复述。读法:`find src -name '*.ts' \| xargs wc -l \| sort -rn` 找最重的模块 → 读头注 → 再挑 3-5 个「反直觉且踩过坑」的细节 |
| **「难在哪」心里有数(但别写进 README)** | 每条主线要能回答「为什么这不容易」(租户之间**真的**隔得住 / 崩溃后**无感**恢复 / 装了**不出事**)—— 用于**决定亮点写什么**;⚠️ 但「多租户演示十分钟就能写出来」这类**对比式议论本身不许出现在 README**(见下表「面向读者」) |
| **补一节工程纵深** | 用「双后端同接口 / 关键决策是纯函数 / `npm test` 覆盖 + 注入脚本运行时校验」这类事实回答「凭什么信这些机制」 |
| **面向读者,不是面向作者**(2026-09-13 第三次纠正) | README 是给**使用者 / 贡献者**看的。禁止出现「**重点不是 X,而是 Y**」「X **十分钟就能写出来**」这类**议论式 / 对比式**表述 —— 那是在跟作者对话,读者不关心。直接陈述「**项目做什么 + 重心在哪**」即可 |
| **小节标题别加议论** | 标题只写名词短语(`### 一、进程级安全隔离`),**不要**写成 `(不是「同机不同目录」)` `(「能装」和「装了不出事」是两件事)` 这种反问/抖机灵。对比与理由放到**表格单元格**里当技术说明 |
| **别重复定位句** | 标题下的一句 tagline 与 badges 之后的描述段**不要重复同一个句式**(「把 DSH 变成…」说了两遍)。tagline 一句话,描述段讲**用户视角的事实 + 三条重心** |
---
---
### R-O12 · README 结构顺序
**依据**:`readme-craft`(SkillHub 技能,蒸馏 awesome-readme / Standard README / Art of README / Make a README / GitHub Docs / thoughtbot)+ `standard-readme`。核心是**认知漏斗**:**Hook → Onboarding → Content → Trust → Meta**,**不得倒序**。
**本项目定稿顺序(Hero + 16 节)**
```
标题 → 一行描述 → 徽章(**5 个**:Node · Version · built on DSH · AI-generated · DeepSeek V4/V4.1 flash;License 徽章已随授权类内容一同移除)
→ Hero(三条主线 + **一行「全部代码与文档由 AI 生成」** + 拓扑图)→ 快速开始片段
目录 → **AI 生成** → 亮点 → 快速开始 → 功能详解 → 架构 → 安全模型 → 部署形态 → 配置
→ 控制面 API → 插件移植指南 → 开发 → 常见问题 → 目录结构 → 版本与迭代 → 贡献
→ 第三方组件与致谢 → 授权与商业使用(必须最后)→ 授权摘要收尾(全文唯一一处摘要)
```
> ⚠️ `## AI 生成` 是**第 1 个正文章节**(用户 2026-09-13 明确「还有个重点要加载前面」)—— 它是这个项目的**核心差异化**:整站代码与文档由 AI 写成。**别把它挪到后面,也别只留徽章不留正文**。
| # | 硬规则 | 理由 |
|---|---|---|
| 1 | **授权章节放最后** | 6 个权威来源共识 + readme-craft 的 Trust 评分项就是「License 在最后?」 |
| 2 | **授权信息不要出现在前排正文**(2026-09-13 用户纠正):靠**顶部 License 徽章**承载(本项目 = `license-personal free \| commercial paid`,并链接到末节)+ **文末**「授权与商业使用」章节尾的一行摘要。**不要在徽章下方另加一句授权 blockquote** —— 用户明确「说明文档还是在前面」不接受 |
| 3 | **Hero 区(前 50 行)必须有可视块** | 审计项 H3 占 7 分(拓扑图 / 截图 / GIF 任选) |
| 4 | 一行描述 **< 120 字符**,且与 `package.json` 的 `description` **开头一致** | 审计项 H2 占 8 分,单项最高 |
| 5 | 超 100 行**必须有 TOC**,且**锚点逐条可解析** | 自查法:把标题按 GitHub 规则转锚点(小写 / 去标点 / 空格→`-`)后与 TOC 比对 |
| 6 | 功能用**表格/列表**不写散文;**清单型章节要声明「机制见亮点」** | 避免 C1 扣分与 S3 信息重复 |
| 7 | **API 概览必须从代码抓真实路由**(`grep -oE "app\.(get\|post\|put\|delete)\("`),别凭印象写 | 审计项 C3;写错路由比不写更糟 |
| 8 | 章节重排要用**脚本按标题切分再拼装**,重排后**复跑 TOC 校验** | 手工搬章节极易丢内容 |
| 9 | **表格单元格里不要写裸 `\|`**(如 `GET\|POST`)—— 会截断表格,改写「(`POST` 同形)」 | 格式错误扣 P2 |
| 10 | 发布后补 **CI 徽章** 与**真实联系方式**;上线前不要加 CI 徽章(占位符 = 死链) | T3 / T4 是唯二「因尚未发布」而扣分的项 |
**审计口径**(readme-craft 22 项 / 100 分):Hook 25 · Onboarding 25 · Content 20 · Trust 15 · Structure 10 · Polish 5;等级 **S**≥90 / **A**≥80 / **B**≥70 / **C**≥60 / **D**<60。
**本项目首轮自审 = 93/100(S)**,扣分仅:CI 徽章 0/3 · 维护者信息 0/3(`<CONTACT_EMAIL>` 未填)· S3 信息重复 2/3。
---
### R-O13 · 双语文档(2026-09-13 用户定:**中文优先,英文在同步 GitHub 时生成**)
> 用户原话:「文档别忘了**中英文双语**,后续**优先写中文**,需要同步到 GitHub 时再更新英语。」
| 项 | 口径 |
|---|---|
| **母本** | **中文**(`README.md` / `PLUGIN-PORTING.md`)。**日常只维护中文**;英文文件**不要求随时同步**,避免双份维护成本 |
| **英文文件名** | `README.en.md`、`PLUGIN-PORTING.en.md`(与中文同目录,后缀 `.en.md`) |
| **生成时机** | **发布 / 推送 GitHub 之前那一步**(SOP 的 5.5)—— 也就是 §7 验证之后、§6 交付之前 |
| **语言切换行** | 两份文件**顶部都要有**:中文版写 `[中文](README.md) · [English](README.en.md)`;英文版写 `[English](README.en.md) · [中文](README.md)`。⚠️ **英文文件存在之前不要加**(否则是死链,扣 P1) |
| **翻译时严格保持** | 代码块、命令、路径、env 名、包名、URL、图(ASCII/Mermaid)、表格结构与「版本与迭代」表内容 —— **逐字保留**;占位符(`<YOUR_GITHUB_ACCOUNT>` 等)**同步替换** |
| **不翻译(保持原文)** | `LICENSE-UPSTREAM-MIT.txt`(MIT 英文原文,**逐字节**);`LICENSE` 保持**中文正文 + 英文摘要**(现状即如此);`THIRD-PARTY-NOTICES.md` 中文即可(包名与许可标识本就是英文) |
| **校验** | 英文版同样要跑 **TOC 锚点校验**(按英文标题算锚点)与**相对链接存在性**;两版的「版本与迭代」表**行数与版本号必须一致** |
| **别忘** | 这是**发布前唯一会因为"没做"而显得半成品**的项 —— 写进 SOP 与 §10 待办,接手先看 |
---
### R-O14 · 🚫 K8s 相关内容:**源仓那侧视为废弃分支**(2026-09-14 定稿;2026-09-15 用户升级为**长期策略**)
> 用户原话(2026-09-15):「记住这部分和 K8S 相关的代码 **后续获取开发项目代码时 跳过就用当前清除或修改后的版本**」
| 规则 | 判据 |
|---|---|
| **① 不回流** | 源仓的 K8s 资源 / 文档 / 注释 / 语义描述 / K8s 专属配置,**不得**因源仓更新而重新进入导出物 |
| **② 不重复实现** | 导出层已有的清除与改写就是**唯一口径**,不要每次重新发明 |
| **③ 改了源仓也不跟** | 源仓若再改 K8s 那段代码,**以导出层版本为准**(源仓那侧视为**废弃分支**) |
| **判定标准** | 构建输出 **`blocking hits: 0`**;不为 0 = 回流了 ⇒ 按台账 §四 处置 |
| **权威清单** | **`_K8s排除台账.md`**(本工作根)—— 含「下次取新版本怎么做」的步骤 + 判读表 + 噪音清单 |
| **扫描器** | **`_k8s_scan.py`**(`--source` 扫源仓 / 无参数扫导出物;**EXIT=1 = 有未覆盖项**) |
**三重强制手段**(缺一会静默失效):
`INCLUDE_DIRS`/`INCLUDE_FILES` 白名单 → `EXCLUDE_FILES`/`DROP_SCRIPTS` → `REGEX_RULES` ⑦⑧⑨ + **`K8S_SEMANTIC_RULES`** + `LEAK_PROBES`。
**已清到 0(源码与注释)**:62 → 0;含 K8s 专属配置项(`config.ts` 7 字段 + 3 常量 + `parseCidrs()`)、
`DeployMode` 收窄为 `'local'`、全部注释与**语义描述**(`Pod`/`file sidecar`/`ConfigMap`)。
⚠️ **连带必做**(否则 `tsc` 报错):`proxy.ts` 的 `deployMode === 'k8s'` 实参改常量 `false`;`web/server.ts` 的 fail-loud 守卫删除。
⏳ **唯一剩项**:`@kubernetes/client-node` 依赖 + lock(8 处)—— 导出层删不了(须重生成 lock,⛔ 不手改 lock)。
⚠️ **但这不等于「去源仓 `npm uninstall` 就行」**(2026-09-15 更正):**导出物**里它 0 import(3 个 import 它的模块被 `EXCLUDE_FILES` 剔了),可**源仓仍保留 K8s 后端**(`src/supervisor/{k8s-spawner,leader,reconcile}.ts` 都 import 它)⇒ **在源仓直接删会打断源仓 `tsc`**。
**正确顺序**:源仓先下线 K8s 后端 → 再 `npm uninstall` 重生成 lock → 最后重跑导出。三条路线见 `_K8s清理影响评估与回归说明_20260915.md §9`。
---
## 1.5 🔴🔴🔴 用户当场纠正过的硬口径(2026-09-14 凌晨:连纠 8 次)!!!
> **这不是"建议",是硬口径 —— 一条不遵守就要返工。** 全部来自真实纠正,逐条附用户原话。
| !!! | 规则 | 判据 / 用户原话 |
|---|---|---|
| **!!! 1 !!!** | **没验证的能力:不写(不是标注「未验证」,是整条删)** | 「**没验证的不要写意思就是不要写 !!!**」,点名删「双部署后端同一接口(K8s 后端未验证)」。⇒ README **不得出现**「未验证 / 实验性 / 模式 B / K8s 后端 / Postgres」等字样 —— 已**全清**(代码可留,**文档不提**)|
| **!!! 2 !!!** | **不写废话**:括号里的体贴话、推销话 | 「(想自己掌控每一步)**不要写这种没用的废话**」;「商业授权…联系 xxx **不用写这个废话**」。**判据:括号里若没有机制 / 数值 / 真实行为 / UI 提示 = 废话,一律删**。已删:`手动部署(想自己掌控每一步)`、`(建议先审一遍)`、整段「商业授权」+ 邮箱、章节名「授权与商业使用」→「**授权**」|
| **!!! 3 !!!** | **归类必须准:DeepSeek Harness 是「基座」,不是「第三方组件」** | 「**这个不叫三方组件…所有开发都是基于这个来的**」⇒ ① 删掉「第三方组件与致谢」整节;② 在「架构」里讲清 **DSH = DeepSeek AI 开源的 agent harness**(everything-is-a-plugin / Cordis 驱动 / 默认 `npx @deepseek-ai/dsh web` → `127.0.0.1:3080` **本机单用户** Web UI);③ 补上 **developer preview ⇒ 官方明说会破坏性变更**(它才是"为什么要有兼容性预检 + 版本冻结"的钩子)|
| **!!! 4 !!!** | **读码找亮点:亮点必须来自实际读码,且区分「已验证」** | 「在分析一遍项目看看还有哪些亮点」⇒ 按 R-O11 逐个读 `src/**` 头注;**没验证过的不写**(本次挖到的"模型管理两层""迁移账本同形"等因**未实测**而**未采纳**)|
| **!!! 5 !!!** | **未持锁 ≠ 不能改导出物;但「重建」会删掉本地 `.git`** | ① 本工作根**不在**锁保护范围(钩子只护「文档库 / 代码仓」)—— **别再拿锁当理由拒改导出物**(本次被纠正);② ⚠️ **`_build_export.py --force` 的 `rmtree(DST)` 会连仓库里本地 `.git` 一起删**(本次实测)⇒ 顺序必须 **重建 → 六件套 → `git init`/commit → 推送**,**提交后不得再重建** |
| **!!! 6 !!!** | **安装成功才准提交 / 推送(用户定的闸门)** | 「**必须按照说明文档 安装成功才能提交**」⇒ **平台本体跑通**(install 成功 + 登录 200 + 管理台 / 桌面 API 200)是**下限**;`--dry-run` 通过、组件级验证、探针全绿 **都不算通过** |
| **!!! 7 !!!** | **敏感信息一律不进仓库**(测试 IP / 账号密码 / 实例 ID / 服务器信息) | 本次把**真实测试 IP** 写进 README 示例被当场抓到 ⇒ 改用 **RFC 5737 文档保留地址**(如 `203.0.113.10.nip.io`);并把 `106.54.21.172` / `ins-3q6k1p8t` / 测试账号密码**四项加进 `LEAK_PROBES`** |
| **!!! 8 !!!** | **提「注意事项」前先对齐本技能 §1 的 R-O1–R-O13** | 「感觉都不是我最早说的注意事项,**你看看 skill**」⇒ 用户要的是**他原始定的硬规则**,**不是**实现坑;计划/清单按 R-O 组织 |
| **!!! 9 !!!** | **章节顺序服务于读者;参考性章节别放太后;排版要整体看** | 「目录结构 是不是放的太靠后了,**在整体看看你的排版呢**」⇒ ①「**目录结构**」从 Meta 区**上移到「架构」之后**(逻辑结构 → 物理结构,读者顺着一路读);② 排版检查要**整体过**:分隔线用法、标题层级、表格/列表一致性、长句拆分、括号废话(见 !!! 2 !!!)。⚠️ 与 R-O12 的社区标准顺序冲突时,**以用户当场指令为准**并说明理由 |
### 改文档时的**固定动作**(本次反复用到,照抄)
1. **两份同步**:`_overlay\<文件>` 与 `dsh-users-platform\<文件>` **必须逐字节一致**(改一份 = 改两份,改完 `md5` 比一次);
2. **改完必验**:`md5` 相同 + **TOC 锚点机检无悬空** + **全篇关键词扫描**(`模式 B / K8s / 未验证 / 实验性 / WorkBuddy / 旧名 / 敏感串` 应**全 0**);
3. **提交**:`git -c core.autocrlf=false add -A` → `commit --amend --no-edit`(**首发阶段**保持单条发布提交);**提交后不得再重建**;
4. **改本技能**:两副本(活跃 + 文档库归档)**md5 必须一致**,同步前**先抢全局执行锁**。
## 2. 保留 / 移除清单(delta · 改口径先改这里)
### 保留
```
assets/ 注入脚本(2:inject/recovery.js · inject/assist.js)—— ⚠️ **proxy.ts 的 loadInject 按包根解析 ⇒ 缺了平台启动即抛错**,必须随包
src/ 控制面 TS 源码(**已剔除 6 个 K8s 后端模块**,见下)
web/ 静态页
scripts/ 运维与冒烟脚本(**已剔除 5 个 K8s / 已排除插件相关脚本**)
test/ 单测(**已剔除 2 个 K8s 模块的测试**)
.github/workflows/build.yml
package.json package-lock.json tsconfig.json cordis.patch.yml
Dockerfile .dockerignore .gitignore(重建)
mksess.cjs ensure-role-profile-patch.cjs(临时会话 / profile 补丁)
```
> 🔴 **K8s 相关一律不带**(2026-09-14 用户定稿,2026-09-15 升级为**长期策略**):
> ⛔ **不再保留** `deploy/`(11 个 K8s 清单)· `poc/`(4 个 K8s 可行性验证件)· `Dockerfile.dsh`(每用户 Pod 镜像)·
> 6 个 K8s 后端模块(`k8s-spawner` `k8s-user-fs` `leader` `reconcile` `tcp-bridge` `web/file-service`)·
> 2 个 K8s 测试 · `smoke-file-service.mjs` · CI 里的 dsh 镜像步骤 · `config.ts` 的 K8s 专属配置项 ·
> 以及**全部 k8s 注释与语义描述**。
> ✅ **保留** `src/db/pg.ts`(Postgres = 共享 HA,**不是** K8s 专属)。
> 📌 **权威清单与处置办法**:**`_K8s排除台账.md`**(本工作根下)—— 含「下次取新版本怎么做」的步骤与判读表;
> 扫描器 **`_k8s_scan.py`**(`--source` 扫源仓 / 不给参数扫导出物);判定标准 = 构建 **`blocking hits: 0`**。
### 移除
| 路径 | 理由 |
|---|---|
| `poc/business-plugins/` | **已投放插件**(功能管理分区插件,含全部 `.tgz`) |
| `poc/workspace-scoped-picker/` | **已投放插件**(目录选择器) |
| `scripts/ensure-anysearch-admin.cjs`<br>`scripts/ensure-anysearch-pool.mjs` | 特定插件投放脚本 |
| `scripts/ensure-biz-plugins.cjs`<br>`scripts/ensure-portal-entry.cjs`<br>`scripts/install-workspace-picker.sh`<br>`scripts/provision-new-users.sh` | 插件 / 本平台专属投放与开通脚本 |
| `docs/`(7 篇) | R-O4:项目文档 |
| `STANDARD.md` · `poc/README.md` · `poc/*/README.md` | R-O4:文档 |
| `.workbuddy/` · `lib/` · `node_modules/` · `data/` | 本地状态与构建产物 |
| **`.git`** | ⛔ 历史里含全部内部信息 ⇒ **必须全新仓库** |
| 全部 `*.tgz` | 构建产物 |
### 本就不在代码仓(别去找)
**MCN 工作台(`dsh-plugin-mcn-suite`)、douyin-accounts、vision-router、各 skill 本体(含 MCN 相关 skill)** 都不在 `dsh_shenxian` 里 —— 它们在其他仓库 / 实例 profile。收尾探针会复核 `mcn` / `douyin` / `抖音` / `vision-router` **0 命中**。
### ⚠️ 特许保留项(**不要在脱敏里动它**)
| 项 | 处置 | 依据 |
|---|---|---|
| **`dsh-univer-office`**(插件名 + 实例侧 unix-socket 适配) | **保留原名、保留代码**:`src/supervisor/orchestrator.ts` 的 `DSH_INSTANCE_UNIVER_SOCKET` / `UNIVER_DSH_GATEWAY_SOCKET` 段**不得删**;`src/web/routes/whitelist.ts` 注释里点名 `dsh-univer-office`;探针 `LEAK_PROBES` 里**不得**出现 `dsh-univer` / `UNIVER` | 用户 2026-09-13:「univer 可以保留这个插件 专门讲讲如何把开源插件改造为可在多租户平台运行」 |
| ↳ 为什么值得保留 | 它是 `PLUGIN-PORTING.md`(插件移植指南)的**唯一实证样本** —— 一个「静态预检通过但在托管平台完全不可用」的真实案例,含两层根因、3 条设计决策、2 个平台侧机制坑 | 同上 |
### 待决项(用户没拍板前维持现状)
- `poc/business-plugins` + `poc/workspace-scoped-picker`:现按**严格口径移除**(「所有已投放的插件」)。若判定它们属「平台自带界面」而非投放插件 → 加回,并同步改本节 + 探针。
---
## 3. 脱敏映射表(**唯一口径**,改只改这里 + 脚本里的 `GLOBAL`)
| 类别 | 原值 | 替换为 | 命中 |
|---|---|---|---|
| 平台域名 | `dsh.alotbuy.com` / `*.alotbuy.com` / `alotbuy` | `<baseDomain>` | 3 文件 |
| ↳ **特例** | `web/wake.html` 的 `safeNext()` 正则 | 改成**运行时从 `location.hostname` 推导注册域**(`split('.')` 去最左一段),彻底去硬编码 | — |
| 服务器公网 IP | `47.77.182.89` | `<SERVER_PUBLIC_IP>` | 1 |
| 宿主内网 IP | `172.18.16.212` | `<HOST_LAN_IP>` | 1 |
| 服务器代码路径 | `/opt/dshs` | `<INSTALL_DIR>` | 8 |
| ↳ **特例** | `require('/opt/dshs/node_modules/better-sqlite3')` | `require('better-sqlite3')`(等价且更规范) | — |
| 服务器平台目录 | `/opt/dsh/{state,artifacts,backups}` | `/var/lib/dsh-web-platform/{state,artifacts,backups}`(与上游文档一致的数据根) | 6 |
| **内部档案号** | 全角/ASCII 括号里的 `档案 NN`(含 `档案 81 · R1`、`(档案 20)`、`backoff (档案 20)`) | **整块删**(号无对外意义);残渣(空括号 `()` / ` ().`)由后续规则清掉 | 183 |
| **悬空文档引用** | `docs/k8s.md §5.2` / `docs/blueprint.md` / `docs/domain-config.md` | 映射到 README 真实小节(`README「部署形态」` / `README「架构」` / `README「配置」`);`§号` 一并去掉 | 42 |
| ↳ **实现位置** | 以上两项由 `_build_export.py` 的 **`REGEX_RULES`** 做(不是 `GLOBAL`);⛔ **清理规则只准碰全角标点**,唯一放开的两条 ASCII 规则:① `(档案 NN)`(模式里必须有档案号 ⇒ 不会误伤 `foo()`)② ` ().`+句末标点(`(): void` 后面是 `:` ⇒ 不命中)。**每次改 REGEX_RULES 必须重建 + `tsc` 复核**(第一次踩过:ASCII 括号进字符类把 `foo()` 删了,tsc 报 Invalid character) | — |
| ↳ tar 相对写法 | `opt/dsh*`(无前导斜杠) | `var/lib/dsh-web-platform*` | — |
| 账号 / 私有仓库 | `maogeigei`、`[email protected]:...dsh_shenxian_doc.git` | `<YOUR_GITHUB_ACCOUNT>`;文档库引用随 README 重写整体移除 | 1 |
| 云厂商标识 | 「阿里云内网 DNS」「BT-Panel 58888/8765」「sshd 22/32022」 | 「云厂商内网 DNS」「面板」「sshd」 | 1 |
| 插件具体名 | `anysearch` / `@anysearch/anysearch-dsh` / `@liustack/modlens` / `mcn-workstation` | 「该插件」/「某第三方插件」→ 再**整行重写**成中性描述 | 6 |
| ↳ **例外(保留原名)** | **`dsh-univer-office`** 与其实例侧适配(`DSH_INSTANCE_UNIVER_SOCKET` / `UNIVER_DSH_GATEWAY_SOCKET`) | **不脱敏、不删代码**(用户 2026-09-13 明确「可以保留这个插件」);它同时是 `PLUGIN-PORTING.md` 的实证样本 ⇒ 其 env 名、注释、`DELETE_LINES` 条目**都不得再动** | 3 |
| **项目改名** | `dshs` 全部自指标识 | `dsh-web-platform`(规则顺序:`dshs.db` → `dshs.db` → `/var/lib/dshs` → `DSHS_` → `dshs`) | 149 |
| ↳ **必须保留** | `上游骨架仓库(已按要求不再具名)`、`上游 \`dshs\`` | **原样不动**(出处/致谢);`LICENSE-UPSTREAM-MIT.txt` 逐字节不动 | — |
**发布前必须替换的占位符**(写在台账里):
| 占位符 | 出现位置 |
|---|---|
| `<YOUR_GITHUB_ACCOUNT>` | `README.md`、`package.json` → repository |
| ~~`<CONTACT_EMAIL>`~~ | 原为 `README.md` 授权章节 / `LICENSE` —— **授权章节与 LICENSE 已于 2026-09-13 移除**,该占位符随之消失(见 §5) |
| `<COPYRIGHT_HOLDER>` | `LICENSE` |
| `<SERVER_PUBLIC_IP>` / `<HOST_LAN_IP>` | `scripts/install-egress-guard.sh`(部署时按自己服务器填) |
| `<INSTALL_DIR>` / `<baseDomain>` | 仅注释,可留 |
> `install.sh` 里的 `dsh.example.com` / `[email protected]` 是文档示例,符合惯例,**不改**。
---
## 4. 保留未动(刻意)
- **183 处「档案 NN」内部编号引用**(仅代码注释):属内部档案号,**不是**本机/服务器信息。已列入「可选后续」;要清需用户点头(做一次纯注释替换)。
- 上游 `docs/` 内容虽为 MIT 可带走,但 R-O4「不带任何文档」⇒ 不带。
---
## 5. 授权结构(**2026-09-13 15:1x 已变更:授权类文件全部移除**)
> 🔴 **现状(以本条为准,覆盖下文所有旧描述)**:用户 2026-09-13 明确 —— **「你的任务是改造和优化,这些内容全部删除」** ⇒ 导出仓里的
> **`LICENSE` / `LICENSE-UPSTREAM-MIT.txt` / `THIRD-PARTY-NOTICES.md` 三份已全部移除**(同时从 `OVERLAY` 与 `REQUIRED_EXPORT` 摘除、`_overlay/` 快照同步删除、README 的 License 徽章 / 目录项 / 「授权与商业使用」整节删掉、`package.json` 的 `license` 回到上游原值 `MIT`)。
> **原文备份在 `../_授权归档_发布时再放回/`**,恢复步骤见 `_导出说明与脱敏台账.md` §F。
> ⛔ **不要再把授权/许可当议题去问用户**(不属本技能范围,不列为待办、不上抛)。
> ⛔ **不要再把授权/许可当议题去问用户**(不属本技能范围,不列为待办、不上抛)。**下文凡提到这三份文件的段落一律以此条为准**(保留原文只为记录历史,不是要求)。
> ⚠️ **但下面这条「MIT 硬约束」是客观事实,任何会话都不许删**(删了会误导后人写出违规声明)。
> ⚠️ **硬法律前提(事实,必须记住)**:**上游 `dshs`(上游作者(已按要求不再具名))是 MIT。MIT 不允许对上游代码附加限制** ⇒ 若日后要恢复「个人免费 / 商用收费」,**只能分层**:上游留 MIT,只对**本项目新增部分**收费;把整个仓库标成「商用需授权」= **违反 MIT**。
**若日后要对外发布**(当前仓库里**没有**任何授权文件),必须先放回那三份并恢复 `OVERLAY` / `REQUIRED_EXPORT`。理由:
1. 无 `LICENSE` ⇒ 默认「保留所有权利」⇒ **任何人都不能合法使用**;
2. 缺上游 MIT 声明 ⇒ **侵犯 上游作者(已按要求不再具名) 的著作权**(MIT 明文要求保留版权与许可声明);
3. 缺第三方 NOTICES ⇒ 多款依赖(MIT/Apache-2.0 等)同样要求保留声明。
⚠️ **不要再把「授权结构」当议题去问用户**(2026-09-13 用户明确:*"你的任务是改造和优化,这些内容全部删除"*)—— **不属本项目范围,不列为待办、不上抛**。保持**现状分层**即可(保守合规:上游永远 MIT、只对本项目新增部分声明)。
> 即便日后发现 `上游作者(已按要求不再具名)` 就是用户本人(可整体简化为单一授权),也**不是本技能要推进的事** —— 只在用户**主动**提出时按本表处理。**但上一条「MIT 不允许对上游代码附加限制」是客观事实,任何会话都不许删掉它**(删了就会误导后人写出违规声明)。
每次发版要动的地方:`README.md` 授权章节(若条款变)、`LICENSE`(版本/日期)、`THIRD-PARTY-NOTICES.md`(依赖版本与分布)。
---
## 6. 迭代发布 SOP
```sh
# 0) 先抢全局执行锁(会写文档库/或要动导出物时)
cd "/d/github/dsh_shenxian/dsh-server-docs"
ME="<会话名>" bash scripts/handoff-guard.sh --claim-exec "<会话名>"
# 1) 源仓库确认基线(只读)
export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin:/c/Windows/System32:$PATH"
git -C "D:/github/dsh_shenxian" status --short --branch
git -C "D:/github/dsh_shenxian" log --oneline -10
# 2) 若本次要改脱敏/保留口径 → 先改 _build_export.py 的 GLOBAL / INCLUDE_* / DROP_SCRIPTS / OVERLAY
# 3) 重建(手工撰写层会自动快照→恢复)
cd "/e/ProgramData/AI技能/dsh-laijing-github"
"/e/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" _build_export.py --force
# 4) 改版本(三个地方一起改,否则不一致)
# package.json 的 version | README.md「版本与迭代」表新增一行 | LICENSE 的版本/日期(条款没变就不用)
# 5) 六项验证(§7)——**全绿才准交付**
# 5.5) 【**发布到 GitHub 前必做**】补齐英文版(见 R-O13)——中文是母本,英文此刻才生成
# README.en.md + PLUGIN-PORTING.en.md + 两份 README 顶部加语言切换行
# 6) 交付/推送(**R-O6:未经明确要求不做**)
```
**版本号约定**:遵循语义化版本。写 README 版本表时**同时写「类型」列**(首个公开发布 / 特性 / 修复 / 内部迭代)。
**登记格式**(README「版本与迭代」表):
```
| **v1.1.0** | 2026-10-xx | 特性 | 一句话摘要(对外能看懂,不写内部档案号)。 |
```
---
## 7. 验证六件套(缺一不可)
| # | 验证 | 命令 / 判据 |
|---|---|---|
| ① | **阻断性探针 0 命中** | 脚本内置 `LEAK_PROBES` 与 `INFO_PROBES` 两档;**`blocking hits: 0`** 才放行。`INFO_PROBES`(`档案` / `交接单`)是**有意保留**的注释引用 |
| ② | 未改动文件**逐字节一致** | 抽样 `cmp -s <源> <导出>` → `IDENTICAL` |
| ③ | **行尾符不被改写** | `grep -c $'\r'` 两边相等(CRLF 源必须仍是 CRLF) |
| ④ | **能编译** | 见下「tsc 验证法」→ `tsc -p tsconfig.json --noEmit` 退出码 **0** |
| ⑤ | `install.sh` 语法 | `bash -n install.sh` + `bash install.sh --help` |
| ⑥ | **已移除项核对** | 逐一 `[ -e ]` 确认 `docs` / 两个插件目录 / `STANDARD.md` / `lib` / `node_modules` / `.workbuddy` 均不存在 |
**tsc 验证法**(导出物没有 `node_modules`,靠**临时联接**;用脚本而不是手敲,**并且绝不能用 recursive 删除**):
```sh
# 脚本已备好:<导出根>\_verify_tsc.mjs(建 junction → 跑 tsc → rmdirSync 拆联接)
"/e/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe" "<导出根>\_verify_tsc.mjs"
# 判据:tsc exit = 0 | junction removed = true
# 收尾再确认「导出物无 node_modules 残留」+「源仓库 node_modules 条目数未变」
```
⚠️ **绝不要用 `rm -rf` / `rmSync(…, {recursive:true})` 删这个联接** —— 在 Windows 上会**顺着联接删掉源仓库的 `node_modules`**。只用 `rmdirSync`(只摘链、不进目标)。
---
## 8. 实测坑(全部踩过,别再踩)
1. **`text` 被重新赋值导致替换未落盘** —— 脱敏函数里 `text = text.replace(...)` 之后再用 `if text2 != text` 判「是否需要写盘」⇒ 全局替换**永远不落盘**(只落行级改动)。**必须留 `orig = text` 做比较基准。**
2. **行级重写的 key 必须写「全局替换之后的文本」** —— 例:原行 `(档案 70 的 anysearch 事故…)`,key 要写 `(档案 70 的 该插件 事故…)`。用替换前的原文当 key ⇒ 静默不命中,留下半吊子句子。
3. **行级重写会丢缩进** —— 按 `strip()` 匹配就必须把**原行的 indent 补回去**(多行替换值还要给后续行也加 indent),否则 TS 缩进错乱。
4. **全局替换顺序敏感** —— `wake.html` 的专用规则必须在笼统的 `alotbuy → <baseDomain>` **之前**,否则专用规则永远不命中(会被先替换成 `<baseDomain>.com` 这种半成品)。
5. **重建会抹掉手写文件** —— 所以有 `OVERLAY`(先快照 `_overlay/` 后恢复)+ `--force` 安全闸。**验证过一次**:重建后 5/5 个 overlay 文件逐字节一致。
6. **`.git` 绝不能带过去** —— 历史里含全部内部信息;必须是**全新仓库**。
7. **Windows 下 `install.sh` 没有执行位** —— README 一律写 `sudo bash install.sh`;要执行位就 `chmod +x` 后再提交。
8. **别信「探针没命中」的直觉** —— 探针必须**在重建之后、还原 overlay 之后**跑(overlay 里也可能带泄漏)。
9. ⛔ **绝不对 `_build_export.py` 本身做批量替换** —— 脚本里同时存在「源模式」与「目标值」(例如规则 `("dshs", "dsh-web-platform")`),盲替会把**源模式**一起改掉 ⇒ **改名规则静默失效**(2026-09-13 实际误伤 4 处)。改脚本只用精确 `Edit`,改完**必须重建一次并用 grep 复查**。
10. **`Dockerfile` / `Dockerfile.dsh` 曾被整份跳过脱敏** —— `is_text()` 按扩展名判断,二者无扩展名(或 `.dsh` 不在白名单)⇒ 漏掉。已在 `is_text()` 里加特判。
11. **`package.json` 属「源派生」而非 OVERLAY** —— 手改 version / description / repository / license 会在下次 `--force` **被覆盖**。这类项目自有字段必须写成 **GLOBAL 规则**。
12. **`dshs.db` 是无前缀写法** —— `src/config.ts` 里默认库文件名不带 `dsh-` 前缀,只替换 `dshs` 会漏掉它;要单独一条规则。
13. **改名要连导出目录名一起改**,并同步两个辅助脚本里的绝对路径(`_build_export.py` 的 `DST`、`_verify_tsc.mjs` 的 `EX`),改完**重跑一次 tsc** 验证路径仍对。
14. **废弃的旧候选名要进阻断性探针** —— 改名改了两轮时(`dshs` → 中间候选 → 终选),若只探最老的那个名,**中间名会静默残留**(`_overlay` 里最容易中招)。做法:把**所有曾用名**都加进 `LEAK_PROBES`。
15. **改 overlay 的自指标识可以做批量替换**(overlay 里只有本项目自己的标识,上游出处名是另一个字符串)—— 但**必须先用 grep 确认「旧名出现处全是自指」**再替换;`_build_export.py` 本身**绝不**能这么干(见坑 9)。
16. 🔴 **`--claim-exec` 的输出必须当场看;claim 与 release 绝不能写进同一条命令**(2026-09-13 **实际闯祸**)—— 把 claim 结果重定向到文件、只显示 `OWNER` 首行 ⇒ **没发现抢锁失败**;命令末尾的 `--release-exec` 是 `rm -rf` 语义,于是**把另一个会话(`R1-注入层-1104`)的锁删掉了**。正确姿势:
- 抢锁**单独一条命令**,且**当场看它的输出**(`✓ 已持全局执行锁` vs `✗ 抢锁失败`);
- 看到「占用者」不是自己 ⇒ **立即停手**,不碰文档库/代码库;
- **放锁前再 `cat 交接单/.exec-lock/OWNER` 确认首行是自己**;
- 万一误放:脚本实现是 `rm -rf "$LOCKEXEC"`、**不留档** ⇒ 按 `handoff-guard.sh` 第 43 行的格式原样重建(`OWNER` / `开始:MM-DD HH:MM` / `在做:…`,值取自抢锁失败时的输出),再用 `bash scripts/handoff-guard.sh` 的信息模式验证「占用者」已回到原会话,并**在回复里明确告知用户**;若该会话其实已结束,请用户自行撤锁(处置权只属于用户)。
17. 🔴 **迁移工作根:先改脚本常量,再搬目录**(2026-09-13 实际踩)—— 顺序颠倒(先 `mv` 后改 `OUT`)时,任何一次重建都会**在旧位置把整个仓库重新建出来**(那次建出 135 个文件),而且因为旧位置**没有 `_overlay`**,**6 个手工层文件(README / LICENSE / NOTICE / PLUGIN-PORTING / install.sh / LICENSE-UPSTREAM-MIT)全部缺席** —— 若此时旧目录被当成交付物,就是一次「文档凭空消失」的事故。
**正确顺序**:① 改 `_build_export.py` 的 `OUT` **和** `_verify_tsc.mjs` 的 `EX` → ② 复制/搬运目录 → ③ 在新位置重建 → ④ 核对 `文件数` 与**手工层 6/6** + 跑 `tsc` → ⑤ **`ls` 确认旧位置不存在**(没被重建复活)→ ⑥ 同步本技能与项目 MEMORY 里的路径引用。
⚠️ 附带教训:`_build_export.py` 的安全闸与 OVERLAY 机制**都依赖 `OUT` 指向真实工作根**;`OUT` 指错时它**不会报错**,而是**默默在别处新建一套**。
18. 🔴 **`INCLUDE_DIRS` / `INCLUDE_FILES` 是白名单:漏了不报错,只是少文件**(2026-09-13 实际踩:`assets/` 漏收录 ⇒ 导出的仓库缺注入脚本,平台会 fail-fast、自带校验脚本也会失败)。**两条防线**:
- **`REQUIRED_EXPORT` 清单**(脚本内,26 项):构建时逐个 `os.path.exists`,**缺一即 `return 1`**;新增"运行时要读的文件"必须加进去。
- **用被导出仓库自带的校验脚本验收** —— `node scripts/verify-inject.cjs`(读 `assets/inject/*.js` + 检查 `proxy.ts` 走 `loadInject`)。这比"我看了一眼目录"可靠得多。
⚠️ 同类风险点:`package.json` 的 `files`(npm/git 安装按它过滤,与 `INCLUDE_*` 是**两套名单**,都要补)。
---
## 9. 命令速查
```sh
# 重建
cd "/e/ProgramData/AI技能/dsh-laijing-github"
"/e/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" _build_export.py --force
# 只跑探针/看结构(不重建)
grep -rIlF "alotbuy" dsh-web-platform/ ; find dsh-web-platform -type f | wc -l
# 看某文件与源的差异
diff -u "D:/github/dsh_shenxian/<f>" "dsh-web-platform/<f>"
# 行尾对照
grep -c $'\r' "<源 f>" ; grep -c $'\r' "dsh-web-platform/<f>"
```
---
## 10. 已知待办 / 漂移(接手先看)
> ✅ **2026-09-13 15:1x 全部清零** —— 以下原待办均已处理:
> · `install.sh` 已在服务器隔离目录跑通 `--dry-run`(并修 4 个缺陷)| · 悬空 `docs/*.md` 42→0| · 「档案 NN」197→0| · 7 处占位符已按 `PUBLISH_*` 填| · `poc/` 两个插件的加回结论 = **保持移除**。
> ⛔ **「授权条款 / 法律意见 / `上游作者(已按要求不再具名)` 是否本人」已从本项目移除** —— 用户明确「你的任务是改造和优化,这些内容全部删除」⇒ **不再列为待办、不再上抛**(事实层面的 MIT 约束见 §5,那条不许删)。
**当前仍需注意的(非待办,是风险提示)**:
- ⚠️ **首装从未在干净机器上真跑过**:`--dry-run` 已验证全流程,但**真实安装会写 `/etc`、建 systemd unit、起服务** ⇒ 首次真装请在一台干净机器上做。
- ⚠️ **导出基线会漂**:导出脚本复制的是**工作树**(多会话并行在改源码仓)⇒ 发布前按 `git archive <commit>` 冻结,或**每次重建必重跑探针 + `_verify_tsc.mjs`**。
- ✅ **项目名已定稿**(用户 2026-09-13):中文 **DSH Web 平台** | English **DSH Web Platform** | 技术标识 **`dsh-web-platform`**(已在 npm 与插件目录实测未占用)。若日后要换名,按 **R-O8** 重走「查占用 → 改全量标识 → 保留出处致敬」,并把**新废弃的旧名加进探针**。
- ✅ **本技能的归档副本 + `INDEX.md` 登记已补**(2026-09-13):`dsh-server-docs/skills/dsh-opensource-release/SKILL.md`,两副本 md5 一致;INDEX §一/§二 已登记;四件套全绿。
⚠️ **本技能每次改动后都要同步归档副本**(md5 必须一致),同步前**先抢全局执行锁**;纪律 = 先单独抢锁**当场看输出** → 验 `OWNER` 是自己 → `cp` → 两副本 md5 一致 → 复跑四件套 → **再验 `OWNER`** → `--release-exec`。
- ⏳ **英文版待生成(发布前必做,见 R-O13)**:`README.en.md` + `PLUGIN-PORTING.en.md` + 两份顶部的语言切换行。**当前刻意不做**(用户口径:中文优先,同步 GitHub 时再更新英语)—— ⚠️ 但**切换行要等英文文件存在后再加**,否则是死链。
- 🔴 **导出基线会漂(2026-09-13 11:2x 实测)**:导出脚本复制的是**工作树**,而**多个会话在并行改源码仓** —— 当时 `D:\github\dsh_shenxian` 的 HEAD 仍是 `da3e0f9`(⚠️ 2026-09-13 20:2x 实测已变为 **`43976fe`「初始提交」**,且**仅 1 条提交**),但**工作树有 20 个未提交文件**(`orchestrator.ts` / `crash-policy.ts` / `proxy.ts` / `spawner.ts` / `config.ts` / `web/*.html` / `test/*` …),导出里因此混入了**别人未完成的改动**;同时新增了需脱敏的标识(`'dsh-plugin-mcn-suite'`)触发探针。**发布前必须二选一**:① 把导出改成**按 commit 取**(`git archive <commit>`,才是"冻结版本");② 或明确接受"含未提交工作"并**每次重建后重跑探针**(新增插件名/内部名会随别人提交冒出来)。
- ⚠️(知识库侧,非本技能职责):本机 `.workbuddy/skills/dsh-instance-diagnose/` **尚无归档副本**;`INDEX.md` 只登记了 3 个 skill(`dsh-knowledge-upkeep` 未登记)。属既有漂移,**未擅自修**。
---
## 相关
- 台账(唯一事实源):`_导出说明与脱敏台账.md`(在本工作根下)
- 构建脚本:`_build_export.py` | 类型检查:`_verify_tsc.mjs` | 手工层快照:`_overlay\`(三者都在本工作根下)
- **插件移植指南**(随仓库发布):`dsh-web-platform\PLUGIN-PORTING.md` —— 讲「把开源插件改造成多租户平台可用」,含六个失败模式 H1–H6 与五条规范 R-a~R-e,样本 = `dsh-univer-office`
- 内部权威源(**只读参考,不外带**):`dsh-server-docs/04-调整方案/75-*.md`(托管友好性 + 资源成本两维度)· `76-*.md`(univer 改造全过程)· `71-*.md`(兼容性预检)
- 技能:`dsh-change-workflow`(改动流程与红线)· `dsh-knowledge-upkeep`(文档库维护)· `dsh-instance-diagnose`(实例故障)
- 项目事实:`dsh-server-docs/BRIEF.md`(现行事实)· `dsh-server-docs/CODEBUDDY.md`(动作前规则)
@@ -0,0 +1,120 @@
---
name: dsh-plugin-diagnose
description: DSH 多租户平台(alotbuy.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。
version: 1.0.0
updated_at: 2026-09-13
last_change: 首版。由 2026-09-13 一整天 guest 实例 univer 插件四轮排查沉淀(worker socket 垫片被打包顺序废掉 / 宿主 glibc 2.32 < 绑定要求 2.35 / 网关提交 changeset 也建投影致其崩溃 / client 半边因一条不可满足的 inject 永久挂起),含 4 类真因、3 套取证配方与 8 条实测踩坑。
agent_created: true
---
# DSH 业务插件故障诊断
> 适用对象:**候选池里投放的业务插件**(`dsh-univer-office`、`dsh-plugin-mcn-suite`、`@dsh-local/*` 等)。
> 实例本身的故障(打不开 / OOM / 崩溃重启)用 `dsh-instance-diagnose`,不是本技能。
## 0. 第一原则:**静默失败要当默认假设**
业务插件最容易「坏得没声音」:host 工具照跑、日志干净、界面就是没有东西。
所以排查顺序永远是「**先证明某一层到底有没有活着**」,而不是先读代码猜。
## 1. 三层归属(先定层,再动手)
| 层 | 怎么判「它活着」 | 死了什么样 |
|---|---|---|
| **host 半边**(`lib/index.js`) | 插件的 DSH 工具能调通(`univer_*` / 业务工具返回 `ok:true`) | 工具直接报「未知工具」 |
| **client 半边**(`lib/client.js`) | 页面 `__DSH_BOOT__` 里**有**该插件行,且槽位探针能看到它的占位者 | **有无都没痕迹**:无卡片/无 dock/无服务、无报错 |
| **网关 / 原生绑定** | 网关进程在、socket 能连、`/[健康路径]` 200 | 进程崩、连接被 reset、`.node` 加载报错 |
## 2. 客户端半边**静默挂死**(最隐蔽的一类,2026-09-13 实测)
**症状**:host 工具全好,浏览器里**一个 UI 元素都没有**,Console 无报错。
**机理**:dsh 客户端加载器(`@deepseek-ai/dsh-client-modules`)对每个插件行解析 `package.json` 的
`dsh.client` → `inject`(**包名**列表);**cordis 的 `inject waiting`** 决定 fiber 何时 `apply()`。
⇒ **只要有一条 inject 永远不可满足,`apply()` 永不执行**(连注册都没有,静默)。
**判据:`inject` 差集**(一把尺子,通用):
1. 取实例页面(**不碰用户浏览器**,用临时会话走平台代理):
```bash
SID=$(node /opt/dshs/mksess-guest.cjs) # 需对应用户;用完必须删会话
curl -s -H "Host: <用户名>.alotbuy.com" -H "Cookie: sid=$SID" http://127.0.0.1:3080/ -o page.html
```
2. 从页面里取**实际下发的客户端插件集合**(权威清单,别用 manifest 正则——会漏行):
```python
import re; s=open('page.html',encoding='utf-8',errors='ignore').read()
served={p.split('/client.js')[0] for u in re.findall(r'/plugins/\?\?[^"\'&]+',s)
for p in u.split('??',1)[1].split(',')}
```
3. 解析 `__DSH_BOOT__` 里目标插件的行 → 取它的 `inject` → **求差集**:
```python
rows=re.findall(r'\{"id":"([^"]+)","url":"[^"]*","rev":"[^"]*","inject":\[(.*?)\]\}', s)
need=re.findall(r'"([^"]+)"', dict(rows)['<插件包名>'])
print([x for x in need if x not in served]) # 非空 = 该客户端 fiber 永久挂起
```
4. **差集非空 = 确诊**。两个常见根因:
- **inject 了被平台角色补丁禁用的官方包**。平台对普通用户禁:
`@deepseek-ai/dsh-client-ui-settings-models` / `-settings-plugins` / `-settings-plugin-inventory` /
`@deepseek-ai/dsh-client-ui-cordis` / `dsh-client-hmr` / `dsh-host-directory-picker-auto`
(`ensure-role-profile-patch.cjs`,档案 15;**admin profile 不禁**——A/B 对照一眼能看出来)。
- **inject 了根本不存在的包名**(如 `@deepseek-ai/dsh-client-runtime`)。
⇒ 直接 `ls /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/ | grep <名字>` 核一下。
5. **修法**:从插件 `package.json` 的 `dsh.client.inject` **移除**该包(或改成真实存在的)。
⚠️ **别混两个 inject**:`package.json` 的 `dsh.client.inject` 是**包名**(模块级依赖);
客户端源码里的 `export const inject = ['slots','locale','conversation']` 是**服务名**。前者不可满足会**整体不 apply**,后者只影响个别 `ctx.inject` 块。
**预期管理(必须一起告知用户)**:
- 插件激活**之前**产生的**旧回合不会回溯渲染**(预览卡片由 conversation turn 定义驱动)⇒ 要**新跑一轮**才看得见。
- 槽位探针里 `active:false` 常常**不是故障**:很多组件在「没内容可显示」时 `return null`
(如 univer dock:`if (operation.worktreeId === null) return null`),同槽里恒有内容的项才是 `true`。
- 某类槽(chain 类,如 `conversation.chat.turnTail`)的探针输出**不带名称字段** ⇒ 「按名字找不到」是**探针局限**;
改用**注册时写的常量**(如 `priority: -10`)去对号。
- **不是所有插件都注册 `tool.*.toolview`** ⇒ 「工具卡片没变样」≠ 插件坏了。
## 3. 原生绑定 / glibc 门禁
**第一步永远先直测**(别读代码猜):
```bash
ldd --version | head -1 # 宿主 glibc(本平台 = 2.32,Alibaba Cloud Linux 3)
node -e "try{require('<那个 .node>');console.log('OK')}catch(e){console.log(e.message.split('\n')[0])}"
```
**两类绑定,对策不同**:
| 类型 | 例子 | 对策 |
|---|---|---|
| **有 JS/上游回退开关** | `engine-formula-rust-binding`(`useRustEngine:false` 走 JS 引擎) | **资产侧垫片**:包一层同名导出,**绑定装不上才**强制关掉(可用则原样交上游 ⇒ 宿主升级 glibc 后自动恢复) |
| **无回退** | `exchange-node-binding`(Office 导入导出) | **只能在宿主/镜像层解决**(换 glibc ≥ 2.35 的基底)⇒ 上报用户决策,别硬凑 |
**关键警示(2026-09-13 踩过)**:
- 同一条 glibc 门禁可能被**多个进程**撞到 —— 同一插件在 **worker** 与 **gateway** 里可能**都要**建投影;
**只给 worker 打垫片不够**:网关崩掉的表现是「**写入报错但数据已落盘**」(提交时崩 → 客户端读不到响应 → 连接 reset ⇒ 假错误 ⇒ 有重复写入风险)。
- 打垫片要**同时**处理 ESM(`import.meta.url` 可用)与 **CJS**(esbuild 把 `import.meta` 降级成 `{}` ⇒
`createRequire(undefined)` **启动即崩**);CJS 产物里让垫片显式 import 该包的 **CJS 入口**。
## 4. 「改了却没生效」的三类(按可能性排序)
1. **打包顺序把垫片废掉**:模块级 `const` 被降级为 `var` 整体提升 ⇒ 垫片跑到时是 `undefined`,
比较恒 false ⇒ **静默回退原实现**。判据:在**产物**里插一行探针打印该常量。
**修法**:垫片内用**函数内字面量**,不依赖任何模块级常量。
2. **插件没真正换上新包**:`mine/apply` 对**已启用**插件是 **noop** ⇒ 必须**停用 → 启用**两步;
任务终态是 **`success`**(不是 `done`,轮询别只判 done,否则空转)。改完**核 md5**(实装产物 == 你构建的)。
3. **客户端包按 `rev` 缓存**:客户端半边改动后**必须硬刷新**页面;顺带记住**硬刷新会取消在途的
`platform: client` 工具查询**(由页面回答),而 **host 侧调用不受影响** —— 别把它当链路故障。
## 5. 验证纪律(每条结论都要有可复现的命令)
- **结构自检 ≠ 能打开**:`zipfile.testzip()` 只证明「能解压」。文档类产物要上**严格解析器**
(`python-docx` / `openpyxl` / `python-pptx`;装进隔离 venv)。本次正是靠它抓到 `w:tbl` 缺必需子元素 `w:tblGrid`。
- **改完必须在真机跑一次**,并尽量用**租户 uid**(`setpriv --reuid=<uid>`)+ 真实 socket/真实数据。
- 找不到「日志」时别下结论:业务插件的 stdout **不落 journald**,实例 journal 只有 systemd 启停两行。
- **`grep` 会骗人**:esbuild 的 CJS 导出用 getter(`__toCommonJS` / `apply: () => apply`),
`grep "exports.apply"` 会给你**假阴性** —— 要按打包器形态去找。
## 6. 平台侧事实(省得反复查)
- 插件投放:admin `POST /api/plugins/business`(`{filename, file:<base64>, trust?}`)→ 池 `/var/lib/dshs/business-plugins/`;
再 `mine/apply` 启用。临时会话:`node /opt/dshs/mksess{,-guest}.cjs`,**用完必须**
`DELETE FROM sessions WHERE user_agent='poc-curl2'`。
- 技能是**显式清单**注册(`src/host/skills/plugin.ts` 的 `DEFINITIONS`),**不是扫目录** ⇒ 加技能要**同时**改清单并重建 `lib`。
- 起自带网关做复现时:`NODE_PATH` 要带 `…/node_modules/.pnpm/node_modules`,否则 `libsql`/`ws` 等外部依赖解析不到,会被误判成插件坏了。
- 平台「我的文件」面板 = **浏览 + 下载,没有预览**;**只有 `.univer` 能在线预览**,Office 文件在浏览器里没有原生渲染。