Files
dsh_shenxian/dsh-server-docs/skills/dsh-change-workflow/SKILL.md
T

1038 lines
114 KiB
Markdown
Raw Normal View History

---
name: dsh-change-workflow
description: dsh 多租户平�(ai1net.com / dshs,�务器 47.77.182.89)功能改造与优化任务的完整工作�。当用户�出平�需求�改造�优化�缺陷修��功能调研,或�求"按既有�程执行/沉淀/整�处�机制"时触�。核心:需求识别→调研→规划→开�→验�→归档清� 六阶段 + 红线机制(�自动�级 dsh / �改官方主程� / client bundle � exports.default / �用真实账�测登录 / **R7 �未�确认的批�·全仓写入** / **R8 会中断在线用户的生产�更须先知会**)+ 备份先行 + append-only 日志 + 表格�交付 + 文档「�务器唯一� + docs-status �目录�步�。
version: 2.9.2-playwright-banned
updated_at: 2026-09-15
last_change: ã€�2026-09-12 新增「三把é”�ã€�è�½åœ°æœºåˆ¶ + 更正过期指å�‘】把「多任务并行调度å��è®®ã€�从**设计**è¡¥æˆ�**å�¯å¼ºåˆ¶æ‰§è¡Œçš„判æ�®** —— 全局执行é”� `交接å�•/.exec-lock`(å�Œä¸€æ—¶åˆ»å�ªå…�许一个执行会è¯�)+ å�•级å� ç”¨é”� `.doing-<å�•å�·>` + **æœ�务器侧æ“�作é”� `/opt/dsh/state/.op-lock/`**(管生产æ€�:é‡�å�¯ / drain / 改实例 env·quota / 批é‡�铺æ�’ä»¶ / 改 nginx·nft·è¯�书,因「æœ�务器æ€�å�˜æ›´çœ‹ä¸�出æ�¥ã€�故必须显å¼�加é”�);工装 `scripts/handoff-guard.sh`(新增ã€�1d】分支)与 `scripts/op-lock.sh`。å�Œæ—¶**更正产出闭环里的过期指å�‘**:原文写 `docs-status/文档库状æ€�备注.md` + `bash scripts/docs-status-sync.sh --pull`,**该机制已ä¸�存在** → 改为 `INDEX.md §二` 登记 + 收尾四件套(audit → manifest → sync-check → `PUSH=1 handoff-guard`)。出处:当日 3 次并行事故 + `04-调整方案/69`。此å‰�】**R7 ç¦�止未ç»�确认的批é‡� / 全仓写入**(由æ�¥ï¼šä¸º"让 scp 出去的文件行尾干净",用脚本把 **147 个文件** CRLF→LF —— 当期å�ªè¢«è¦�求改一个 UI 字符串;被用户指为「容易把æœ�务器æ�žå´©ã€�,且éš�å�Ž `cp -r` 确实把**未改动的** 2 个文件覆盖到了æœ�务器)+ **R8 会中断在线用户的生产å�˜æ›´é¡»å…ˆçŸ¥ä¼š**(由æ�¥ï¼šæ¡£æ¡ˆ 58/59 连续两次é‡�å�¯ï¼Œç›´æŽ¥å¼•å�‘用户"会è¯�连接异常"报障)。ã€�2026-09-11 晚,用户当é�¢çº æ­£å�Žå¤§ä¿®ã€‘â‘  **阶段 0 新增第 0 æ�¡ã€Œå¼€å·¥å‰�置检查ã€�并标明"本技能最é‡�è¦�的一步"**(装工具/写自动化/查机制å‰�先看:å�¯ç”¨æŠ€èƒ½åˆ—表 → 本机/项目已有技能与记忆 → "本机已有的能å�¦æ»¡è¶³")+ 红线 **R6「先查已有资产,å†�动手ã€�**;② **æµ�览器自动化定型**:用 `browser-harness`(CDP 9223,Windows 调用方å¼� + helper 清å�•),**ç¦�å†�装 agent-browser / Playwright**;⛔ **é“�律:browser-harness 附ç�€çš„æ˜¯ã€Œç”¨æˆ·æ­£åœ¨ç”¨çš„ Chromeã€�→ 动手å‰�å…ˆ `list_tabs()`,ä¸�是全新空æµ�览器就立刻å�œæ‰‹**(本轮真实事故:在其æµ�览器里登录测试å�· → 覆盖用户 `.alotbuy.com` çš„ `sid`);dsh composer 是 Lexical(`data-lexical-editor`),`type_text`/`fill_input` å�‡æœªç”Ÿæ•ˆï¼›â‘¢ **更正「空闲回收ã€�认知**:回收**从未生效**(TTL 默认 7 天 / cap 4 / æ¯�请求 touch),但 **"回收没生效"≠"实例ä¸�会中断"** —— 中断真凶 = **æœ�务é‡�å�¯ï¼ˆ09-11 è¾¾ 48 次)→ 旧实例å�˜å­¤å„¿ → å�¦èµ·æ–°å®žä¾‹ → 旧页é�¢ 401** + **实例崩溃自动é‡�å�¯ï¼ˆè¿‘ 4 天 69 次;admin 根因 `duplicate loader entry id: permission`)**;附三数核对手法;④ 阶段 1 新增「**用户报障第一步:先读 journal 拿真实失败请求**ã€�(三分钟定ä½�,判æ�®é€ŸæŸ¥ 401/502/crash);⑤ 阶段 4 新增「**验è¯�用例必须å�Œæ—¶è¦†ç›–「有请求体ã€�与「无请求体ã€�**ã€�(档案 51 事å�Žä¿®æ­£å®žè¯�:å�ªæµ‹ POST 会整æ�¡æ¼�掉 GET/SSE)+ 「模拟æµ�览器语义è¦�测到底ã€�(å�ªå¸¦æœ€åˆ�æ—§ cookieã€�显å¼�断言 Set-Cookie 回写);⑥ R4 è¡¥**专用测试账å�·æ¨¡æ�¿**(`role='active'`ã€�`approved_by` 需真实 admin idï¼›`users` 表无 `status` 列)。此å‰�新增「**本环境 TLS 走中间人代ç�† → Python 脚本必须 SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt**ã€�(两份 CA bundle 的区别 + é›¶å¹³å�°æ”¹åŠ¨æ­£è§£ + 为何ä¸�èµ° R5 注入 env)+「è�Œè´£è¾¹ç•Œï¼šæŠ€èƒ½æŠ•放=admin 的事,平å�°å�ªæ��供机制与把关ã€�;此å‰� R5 追加「**安装类æ“�作=扩大,必须先出安装确认清å�•(七项)**;技能/æ�’ä»¶ä¾�赖体检å�ªæŠ¥å‘Š+拦截ã€�ç»�ä¸�代装ã€�ï¼›å�¦æŠŠã€Œå¤�æ�‚引å�·ä¸€å¾‹æœ¬åœ°å†™æ–‡ä»¶â†’scp(ç¦� ssh 内 inline node -e/sed)ã€�从建议å�‡ä¸º**硬性è¦�求**(本轮å�Œç±»é”™è¯¯è¿žçН 4 次)。此å‰�补环境事实「正确域å�� alotbuy.com(dsh.alotbuy.com 是旧域 301,抓页é�¢å�ªä¼šæ‹¿åˆ° 301 HTML)ã€�+ 档案 45 å­˜é‡�会è¯�æ��示è�½ç‚¹ï¼ˆlogin.html);此å‰�新增「**基础è¿�行时版本冻结**ã€�节(档案 44:唯一ç¼�éš
agent_created: true
---
# dsh-change-workflow — dsh 平�改造工作�
dsh 多租户平�(�务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改造的标准处�方法。把平��维历�中��验�过的处�方�固化为�执行�程:**先识别边界 → ��级调研 → 方案对比确认 → �步开� → 全链路验� → 归档清�**。
## 平�速查(硬编�事实,勿猜)
> ⚠� **正确域� = `ai1net.com`**(2026-09-19 由 `alotbuy.com` �入);`alotbuy.com` 是**旧域��已�为 nginx 301 过渡装置**
> ——对它抓页��会拿到 301 HTML,**别误判为"改动没生效"**(2026-09-11 踩过)。
> 平�页由门户直出(`127.0.0.1:3080`),生效副本�有 `/opt/dshs/web/` 一份。
> 存�会��示(档案 45)�在 `web/login.html`:告知「shell 频��求审批 → **新开一个会�**���。
>
> ⚠� **`*.ai1net.com`(��个用户�域)都由�一个 vhost 先 `proxy_pass 127.0.0.1:3080`(门户)**,
> �由门户的 `src/supervisor/proxy.ts` 转�到实例 → **想改"�域访问行为"�改 `proxy.ts`,
> nginx 的 `error_page` / `location` 到�了实例层**(档案 49 实测)。
> 实例空闲回收�首次访问的链路(档案 49):**导航**(GET+`Accept: text/html`)→ 立刻 302 到
> `web/wake.html`(门户��,动画 + 自动调 `/api/dsh/enter` 跳回);**XHR/API** → **等实例就绪�
> 继续转�**(�是回错误/动画 —— 让请求最终�功,dsh 自身 loading ��馈)。
>
> ⚠� **实例的�览器凭� cookie �带�机�缀**:`dsh-auth-<�机�缀>=v1.<base64>.<sig>`(HttpOnly/Secure)。
> **实例�建�这个�缀会�**(�是"过期",是"�字都�对")→ 已打开的页�手里那份对新实例**永远无效**。
> 档案 51 的修法在 `proxy.ts`:**�导航请求�实例侧 401 → �当�实例 token → `GET /?token=` �新 cookie
> → 覆盖 cookie 头�明�放�一请求 → �应追加 `Set-Cookie` 回写�览器**(此�� SSE 自动�连直接�功)。
> �放闸门是 `canReplay = mayHaveBody ? bodyBuf !== undefined : true` —— **GET/HEAD 无请求体,一样�能�放**。
>
> ⚠� **"空闲回收"实际从未生效(2026-09-11 核实,别�拿它解释现象)**:
> ① **TTL** `instanceIdleTtlSeconds` 默认 = **7 天**(`src/config.ts:167` `60*60*24*7`),线上 env 未设 → 等于�会触�;
> ② **LRU 上�** `maxIdleInstances` 默认 = **4**,�有**第 5 个用户**拉起实例时�淘汰最久未活跃者(现有 2 用户永远达�到);
> ③ `reapOnce()` �看 `lastActive`,而 `proxy.ts::resolveSubdomainAccess()` **�个�域请求都 `touch()`**,
> dsh �端还有常驻 SSE `/plugins/events` → **页�开�就永远"活跃"**,TTL 规则��能命中。
> **历�实�**:全历� `grep -c "idle-reap"` = **1**(`09-09 12:24 stop … idle>20s`,是档案 08 拿 TTL=20s �验�那次)。
> ### ⛔ **别把"回收没生效"外推�"实例�会中断"**(2026-09-11 我犯过这个错,被用户当�纠正)
>
> 「页�等一会就模型断开�必须刷新�的真凶是**中断**,与"空闲回收"是两�事。实测统计:
>
> | 中断�� | 实� | 机制 |
> |---|---|---|
> | **① 门户�务��**(主因) | 2026-09-11 一天�� **48 次**(全是部署/探活) | ��� supervisor **内存 `mains` 表清空** → 旧实例�**孤儿**(进程还在但�被跟踪)→ �访问判 `not_running` → **�新拉起新实例(新端�/token/cookie �)** → 旧页�带旧 cookie → **401**。**这正是档案 51 的触��** |
> | **② 实例崩溃�自动��** | 近 4 天 `crash-restart` **69 次**(admin 34 / guest 16 / 测试� 19) | admin 根因 = `duplicate loader entry id: permission`(`exitCode 1`)——**�用与官方 `permission` 撞 id 的候选�件**所致;guest = 信�终止(无 exitCode),紧跟�务�� |
> | ③ 空闲回收 | 全历� `[idle-reap]` **仅 1 次**(09-09 测试值 20s) | **从�没生效**,�构�中断�� |
>
> **必查手法**:`journalctl -u dshs --since "-1d" | grep -c "Started DSH server login orchestrator"`(��次数)
> + `grep -c crash-restart` + `grep -c idle-reap` —— 三个数一比就知�该往哪查。
>
> **治本方�(待办)**:① 部署**攒批��**(别�改一处��一次);② **supervisor �动时收养已存在 scope**
> (本地模�目�**无**收养逻辑,`reconcile.ts` �覆盖 k8s)→ ����产生"新实例 + 401";③ 业务�件探活失败**自动摘 bundle**。
> ⚠� **�置风险**:`maxIdleInstances=4` × `MemoryMax=1024M`(硬�上界) = 4096M **远超** 宿主总� 1870MB(**上��预留**,但真跑满必然�页)→ **撑��,建议调 2**。
>
> ⚠� **dsh web 客户端的会�事件�走 SSE(`EventSource`),�是 WebSocket**(��
> `dsh-api-session-controller/lib/types/client/sessions/session.js`)。**凡"页�内自愈"类注入脚本,
> � hook `fetch`/`XMLHttpRequest` 是够�到主链路的** —— EventSource � 401 ��默 `onerror` �连,
> 界�什么都�显示(档案 50 因此对真实用户无效,档案 51 �在传输层根治)。
> 判断�端用什么传输**别猜**:`grep -rl "new WebSocket\|EventSource" <dsh 包>`。
>
> ⛔ **注入脚本(`src/supervisor/proxy.ts` 的 `SESSION_*_JS`)两��律 —— 2026-09-13 事故��的**:
> ① **它是 TS 模�字��,里�的 `\n` / `${` / �引�会在模�求值时先被处�一次。**
> &nbsp;&nbsp;写 `.join('\n')`(��斜�)⇒ 求值��**裸�行** ⇒ 注入的 JS 直接 **SyntaxError** ⇒
> &nbsp;&nbsp;**整段脚本�默�执行**(浮层 / 自愈 / 助手��全废,页��剩 dsh 自己的「连接异常�)。
> &nbsp;&nbsp;→ 用 `String.fromCharCode(10)`;**校验必须"先模�求值�� `node --check`"**:
> &nbsp;&nbsp;`node scripts/verify-inject.cjs lib/supervisor/proxy.js`(**已接入 `npm test`,勿绕过**)。
> &nbsp;&nbsp;⚠� `new Function(原文)` 与「grep 页� HTML 有没有标记�**都是�绿** —— �者跳过求值,�者验�出"跑�跑得起�"。
> ② **触��必须覆盖"页�开��动"**:用户盯�页�时无任何事件;现已有 **��时 25 s 心跳**
> &nbsp;&nbsp;+ `EventSource` / `WebSocket` 断�包装,且两者都**连续两次失败���**(��=原地 replace,会丢未�存输入)。
>
> 🔴 **�览器自动化:��许两件工具;Playwright 全��止**(用户 2026-09-13 明令:「**playwright �止使用,加到规则中**�):
> ① **`agent-browser`**(WorkBuddy 内置技能,**独立�览器,�碰用户环境**)—— **本项目默认走这�**(2026-09-13 选定);
> ② **`browser-harness`**(CDP **9223**,**附�用户正在用的 Chrome**)—— �在需�看用户现场时用,
> &nbsp;&nbsp;⛔ **动手�必须先 `list_tabs()`**;旧事故:在其�览器里登录测试� → **覆盖了用户的 `.ai1net.com` 的 `sid`**。
> ⛔ **�止 Playwright / `playwright-core`**(�「独立无头�用法)—— 本行**更正**此�把它写�「�读验收默认路径�的错
> &nbsp;&nbsp;(那是 2026-09-11 加的,与�页「� Playwright�**自相矛盾**;2026-09-13 已按用户明令统一为**�止**)。
> ��页与 API �数优先 `curl` / WebFetch,**别动�览器**。
| 项 | 值 |
|----|-----|
| �务器 | 47.77.182.89(Alibaba Cloud Linux al8);SSH:用别� `bt-server`(`~/.ssh/config`:`HostName 47.77.182.89`�`Port 32022`�`User root`�`IdentityFile ~/.ssh/id_ed25519`) |
| 门户 | dshs(systemd `dshs`,127.0.0.1:3080);�� `/opt/dshs`(git master) |
| 数� | `/var/lib/dshs/users/<id>/{home,ws}`;DB `/var/lib/dshs/dshs.db`(root 600) |
| 域� | **ai1net.com**(门户,CF 代� + 通��书,�站 443);用户�域 **`<用户�>.ai1net.com`** � portal proxy 转�实例;旧 `alotbuy.com` 301 → `ai1net.com`(档案 22 / 2026-09-19 �入) |
| 实例 | 门户 spawn `node /usr/local/bin/dsh --profile web --host 127.0.0.1 --port <动�>`;ISOLATION account(setpriv,uid∈[100000,199999];admin=114801) |
| 文档 | **本机 git 工作树 = 唯一�**:`E://ProgramData//AI技能//aliyun-dsh-server//dsh-server-docs`(仓库 `dsh_shenxian_doc`)→ scp 到�务器 `/opt/dsh/docs`(root 600 / README 644,**无 .git 的部署镜�**)。⛔ **本行 2026-09-13 更正**:原文写「�务器唯一� / 本地��留副本 / `dsh-server-docs/` 已废弃�——那是**更早的状�,现已作废** |
| 文档�步 | 改完跑**四件套**:`docs-audit.py` → `docs-index-stats.py --write`(INDEX 状�摘�**机器生�**)→ `docs-manifest.py` → `docs-sync-check.sh` → `docs-consistency.py`;**� scp 自己本次改的文件**�推��跑对账。⛔ `docs-status/` 机制**已�存在**(原文作废) |
| 硬规则 | 动**文档 / 代� / �务器**之�先抢**全局执行�**(`bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<会��>"`);�动**生产**���务器侧 `scripts/op-lock.sh`;**�一时刻�放一个执行会�**。规则实体在项目根 `CODEBUDDY.md`(§3 红线 R1–R9 / §6 并�纪律)—— **本技能��述�也�得与之冲�**。⚠� 释放 op-lock 必须带 `ME=<原�用者>`,�则按 R9 被拒 |
| 档案� | **�跑��,勿写死**(`ls 04-调整方案/ | sort -n | tail -1`;并行改动会打穿)(01-52 已�;**50/51/52** = 会�过期自愈注入(已被51�代) / 实例侧401�明�放 / 新用户实例无法�动;⚠� 无 48;**37/38 �有两�**(并行通�撞�)→ **归档�先 `mkdir <目录>/.lock-<n>` 原���**,光"先 ls �写"�够) |
## 六阶段�程
### 阶段 0 · 需求识别(先盘点�动手,勿跳步)
0. **⛔ 开工�置检查(2026-09-11 用户点��求,血的教训)**:**这是本技能最��的一步**。
病根 = �先查「已�有什么�就凭直觉装工具�猜机制;出错��用户追问,�把**早就写在文档里的说明**翻出�
(最刺眼的一次:我早在技能里写了「勿走 agent-browser�,自己�忘了,绕最远的路)。
任何「**装工具 / 写自动化 / 查机制**�之�,按��完这三�:
1. **先看�用技能列表**(会�开始就在)→ 有对应的**立刻加载技能**,�止�起炉�;
2. **先看本机/项目已有资产**:`~/.workbuddy/skills/`�项目 `.workbuddy/skills/`(尤其本技能)�`MEMORY.md`;
3. **�装任何东西�自问:本机已有的能�满足?** 能→�装;�能→**先说清楚�装**。
→ **固化结论(别��新�现)**:�览器自动化**��许两件** —— **`agent-browser`(默认,独立�览器)** 与
**`browser-harness`(看用户现场用,先 `list_tabs()`)**;⛔ **�止 Playwright / `playwright-core`**(2026-09-13 用户明令);
��页与 API �数用 `curl`/WebFetch,别动�览器。
1. **环境盘点**:本机/�务器/工具链/��/DB/实例状�先行(历�教训:未盘点就默认本机装 Docker 走错路线,被用户批评)。
2. **需求边界**:区分"方案请求"(�输出方案,P0 硬规则:**方案与执行分离,未�明确授��改文件**)与"任务明确直接执行"。
3. **用户确认**:关键分�用选项表收敛(AskUserQuestion:推�项置首标注 "(Recommended)",2-4 选项,�设"其他"��)。
4. **输出格�**:一��结论先行 + 表格化细节(状�/结果列)。
### 阶段 1 · 调研(��级实�优先,�止��文档推断)
0. **★ 用户报障第一步:先拿「真实失败请求�,别猜**(2026-09-11 档案 51 实�,�掉全部试错)。
用户�会说"报错�没�应��消�失败",而**平� journal 里有精确的 URL + method + status**:
```bash
ssh ... 'journalctl -u dshs --since "-30min" --no-pager \
| grep -E "401|403|404|500|502|503|crash-restart|dsh-child|not_running|YAMLException|ERR_MODULE" \
| grep -v "level.:30.*200"'
# 顺带看 host / remoteAddress(= 用户真实 IP)定�是��连的哪个�域
```
判�速查:
- `POST /api/session/prompt → 401` = **实例侧鉴�**(档案 51 的旧 cookie 问题);
- `502` = 实例根本没起� → 翻�时间段的 `YAMLException` / `ERR_MODULE_NOT_FOUND` /
`crash-loop-circuit-open`(档案 52 就是被这一步牵出�的);
- 401 先分辨**平� 401**(body `{"error":"unauthorized"}`)还是**实例 401**
(body `dsh web authentication required…`)——**改的地方完全��**。
1. **�读分�官方包**(�许):`/usr/local/lib/node_modules/@deepseek-ai/dsh/...` 内 bundle 读代�定�机制(slot 声明�inject 契约�fiber 注入)。**��写官方主程�**。
2. **�务器实测 > 推断**:区分 fence 层 / 应用层错误;�览器问题抓 console **完整 err 对象**(`description`/堆栈,勿�看字符串字段——`at new apply` 类堆栈是定�关键)。
3. **�端对照**:archive � ↔ 实例安装副本 md5 对账;官方行为对照第三方�件写法差异�字段对比(如 register 的 `locale`/`children`/`label` 函数形�)。
4. **多模型交�验�**:关键结论交�核对,�轻信�点推断。
### 阶段 2 · 规划(文档先行,方案确认�动工)
1. **方案对比表**:≥2 选项 + �自影�/风险 + 建议(用户�好:结构表对比+�由)。
2. **红线自查**(�项改造必过):
- R1 �触� dsh 自动获�最新版本;版本�级走独立"�级测试→评估→修�"�程(档案 07)
- R2 �改官方 dsh 主程�与缓存;扩展�走 profile 层 `dsh plugin remove/add`(tgz)官方机制
- R3 client bundle **严� `exports.default = apply`**——loader ESM/CJS interop 会�纯函数为�件主体 → 无 inject → `ctx.<svc>` 抛 `cannot get property ... without inject`;官方 bundle �导出 `apply`+`inject`
- **R4 �止用真实账�(`admin` / `guest`)� API 登录测试**——`auth.ts` 登录时 `deleteUserSessions(user.id)` 是 last-wins �活跃会�,**会当场踢掉用户正在用的�览器会�**(2026-09-10 实�:curl 登录 guest 把用户�览器 guest 会�顶掉,页�刷出 `unauthorized`)。�测登录�用 `node /opt/dshs/mksess.cjs` (**PG 直�**)建临时 session,或注册专用测试账�用完�删
- **专用测试账�模�(2026-09-11 实�)**:`POST /api/auth/register` → DB
`UPDATE users SET role='active', approved_by=<真实 admin id>`。⚠� 两个�:① `users` 表**没有 `status` 列**,
审批就是改 `role`,CHECK ��许 `admin/pending/active/disabled`(写 `'user'` 会 CHECK 失败);
② `approved_by` 有 **FK 指� `users.id`**,填字符串会 `FOREIGN KEY constraint failed`。
**凡�"�作该用户自己的实例"(�实例�回收��建�看它的 profile 文件)→ 必须用独立账�**,
�能借 admin/guest 的 session(借了就没法�实例�也污染真实用户)。用完 `DELETE sessions + users` 并 `rm -rf users/<id>`
- **R5 ������准收窄,扩大必须先确认**(2026-09-11 用户新增)——**先判方�**:这次改动是"扩大"还是"收窄"?命中「扩大�定义(新增 bwrap 挂载 / 放开�蔽 / 暴露平�目录或 env / 放宽 `ALLOWED_ENV` / 放� nft / �高��档�或放宽 approval / 新增用户�读写路径 / 让 root 执行链路的对象��用户�控)→ **必须在方案里写出「��影�评估�四问并等用户明确��**,�得顺手�;改了「触�文件�清�里的文件就一律按 R5 走。收窄�直接�,但�蔽类必须**真�动一次实例**验�(档案 42:�蔽 `/usr` 内文件让实例起��)
3. **影�评估**:实例��会�端�(authority �)→ 旧 WS 断连预期;登录 session 删除预期;数�目录��影�。**若命中 R5,�附「��影�评估�四问 + 改动�� diff「实例�访问路径清�(��版)�**。
4. **文档��**:`04-调整方案/` 建编�档案(模��下),先写需求与改动设计。
### 阶段 3 · 开�(�步 + 备份 + ��验�)
**�务器 TS/�置改�标准�程**(多文件改动):
1. `scp` �务器�� → 本地工作区 → 本地 Edit(**读一�→改一�→验�一�**,�止批�读全部文件)
2. `scp` 回�务器 `/tmp/` → `cp` 覆盖�先备份 `bak-<功能>-<YYYYMMDD>/`
3. `npm run build`(tsc 零报错)→ `systemctl restart dshs`
4. 本机直连验�(绕 DNS/代�):
```bash
# SECURE_COOKIES=true → 必须走 TLS;用 --resolve 把域�指到回环
curl -sk --resolve ai1net.com:443:127.0.0.1 https://ai1net.com/api/...
# 带登录�:-H "Cookie: sid=<token>"
```
(明文 `http://127.0.0.1:3080` 在 SECURE_COOKIES 下�通)
> ⚠� **全�覆盖 `/opt/dshs/lib/` 之�,必须先在 HEAD 上 `npm run build`**(2026-09-13 22:16 实测事故)
> 并行会��「整包 lib/ 覆盖�时用了一份**��他人已�交改动**的陈旧构建 ⇒ **把别人的已上线修�整体回退**(本例:`/api/dsh/status` 的 `quota` 字段凭空消失)。
> - **指纹**:`lib/**` 一整批文件 mtime 相� + �务��时间对得上 ⇒ 就是整包覆盖,�是点改。
> - **判�**:部署�立刻 `md5sum` 对账本机构建,或 `grep -c <你上次加的符�>` 目标产物。
> - **�律**:**部署完立刻�验「上一次刚验收过的东西�** —— 本例正是�这一步��现的。
> - �:**「已部署�≠「已�交�**;**�能拿�务器状�当版本事实�**(�务器曾跑�未 commit 的版本)。
**客户端�件(client bundle)部署�程**:
> ⚠� **动手改之�先�「基线校验�(2026-09-13 补,改第三方 tgz �件时尤其必须)**
> �件以 tgz 铺到实例,**本地那个「��目录��一定是线上正在跑的那份**(�能漂移 / 被别的会�改过 / 是旧快照)。
> 步骤:把**�务器上正在用的 tgz** 拉下�解包 → 与本地��目录�**忽略行尾**的全�比对(`diff --strip-trailing-cr`)→
> �文件确认「**差异�有我准备改的**�。�则�能用一套漂移的代�**覆盖线上**(本仓有过�类事故)。
> - �看文件�/版本��算校验(`package.json` 里的版本�能与实际�符;本仓 `lib/index.js` 的 `VERSION` 常�就长期��于 `package.json`)。
> - ⚠� **行尾会制造�差异**:`npm pack` �规范化行尾,手工拼装的 bundle 常是**混用行尾**(实测:线上 `client.js` CR=5222/LF=5547,本地纯 LF),去掉行尾�整文件的「差异�会瞬间收敛到真实改动。
> - �例(我犯的):**先改�验**。顺�错 = 风险窗�白开一段;改完�验�能�明「没出事�,�明�了「�会出事�。
1. archive �改版(`/opt/dsh/docs/04-调整方案/poc/<plugin>/lib/client.js` + package.json version/description)
2. `npm pack` → tgz 拷到用户 `ws/`(chown 用户 uid)→ `setpriv --reuid=<uid> --regid=<gid> --init-groups env HOME=... DSH_HOME=... dsh plugin --profile web remove/add <tgz>`
3. kill 实例进程 → 门户自动 respawn 新 pid+**新端�** → `journalctl -u dshs` 抓 `dsh web: http://127.0.0.1:<port>/?token=...`
4. 验� node_modules 副本 markers(version�关键字符串计数——区分代� vs 注释行)
### 阶段 4 · 验�(全链路 + �览器实测 + 清�)
1. **API 链路**:portal enter(sid)→ url token → 页� 200 → `llm/listProviders ok:true`
- **★ 验�用例必须�时覆盖「有请求体�与「无请求体�两类**(2026-09-11 档案 51 事�修正实�):
�测 `POST` 会整��掉 `GET`�`SSE`。本次补��版闸门写� `mayHaveBody && …` →
**POST 全绿�`GET`/SSE 完全�生效**,而 SSE ��是"页�已打开�实例被回收"时最早撞 401 的链路。
凡改动�"按 method / 有无 body 分支",**`GET` 那�必须�独跑一�**。
- **模拟�览器语义�测到底**:断言��是状��,还�**�带最�那一个旧 cookie�忽略任何新 cookie**
�跑一�(这�等价于真实�览器)。修完�新 cookie 是�**回写**(`Set-Cookie`)必须显�断言。
2. **�览器验�栈(2026-09-11 更正为�侵入�,勿走 agent-browser)**:
### ⛔ �律 0:动手�先 `list_tabs()` 确认「你在驱动哪个�览器�
**browser-harness 会附�到本机**正在�行的**用户日常 Chrome**(CDP **9223**)——2026-09-11 实�:
`BU_CDP_URL=9223` **�被尊�**(daemon 常驻共享,`BH_RUNTIME_DIR_SHARED=1`),� `BU_NAME` + �共享 runtime �建 daemon �
`list_tabs()` **�返回用户的 16 个标签页**(`admin.ai1net.com` / `guest.ai1net.com` / `portal.html#/plugins/manual`);
`curl --noproxy '*' http://127.0.0.1:9223/json/list` 直接�实 **9223 就是用户�览器的调试端�**。
**所以:脚本第一步永远是 `print([t["url"] for t in list_tabs()])`。**
- ��看到的**�是**你自己新开的空�览器 → **立刻�手**,���读检查。
- **�止**在未确认隔离��任何有副作用的动作(登录 / 输入 / �交 / 关闭标签)。
- 已造�的真实事故:在用户�览器里 `fetch('/api/auth/login')` → **覆盖其 `.ai1net.com` 的 `sid`**,用户在该�览器里��测试账�身份。
补救 = 删掉测试账�(cookie 失效 → 用户�新登录��)。**收尾必须 `close_tab` 关掉自己开的标签。**
### 正确调用(Windows,2026-09-11 实测�用)
```bash
R="C:/Users/Administrator/.workbuddy/skills/browser-harness" # 必须 Windows 路径�POSIX 路径 Windows Python �认
export PYTHONPATH="$R/src"; export BH_HOME="$R/.browser-harness-dev"
export BU_CDP_URL="http://127.0.0.1:9223"
env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy NO_PROXY="127.0.0.1,localhost" \
"$R/.venv/Scripts/python.exe" -m browser_harness.run < script.py # 脚本走 stdin,�� heredoc 引�地狱
```
helper:`js / click_at_xy / type_text / fill_input / press_key / switch_tab / close_tab / capture_screenshot / list_tabs / wait`。
### ⛔ dsh composer(Lexical)输入难点(2026-09-11 未攻克)
- composer 真身:`div[contenteditable="true"][data-lexical-editor="true"][data-composer-input="true"]`(class `uV2eYG_input`)。
- `type_text()` = `Input.insertText` → **绕过框架监�**,Lexical 模型�更新 = 没输入。
- `fill_input()`(�字符 `press_key` + `input/change`)实测**也未生效**。
- 真因:**目标标签�是��标签** → `el.focus()` � `document.activeElement` �是 `BODY`,CDP 鼠标/键盘到�了该标签;
`switch_tab` + `ensure_real_tab` + `Page.bringToFront` 都没解决。
- **对照**:Playwright 的 `page.click()` + `page.keyboard.type()` **�功过**(能真实�出消�并拿到回�)。
→ 结论:**�在独��览器里跑**(�与用户标签争��);**传输层 bug 一律用 curl 验�**,�览器�用于视觉/交互确认。
⚠� **工具选择�径(2026-09-13 用户明令 —— 这�优先于下�那段旧实测)**:
**��许两件工具** —— `agent-browser`(**默认**,独立�览器)�`browser-harness`(附 CDP 9223)。
⛔ **Playwright / `playwright-core` 全��止**(用户 2026-09-13 明令)。
本机现状:`agent-browser` 的 daemon **起��**(2026-09-13 实测:`open` 挂�零输出,而 `--version` / `node -e` 正常;Chrome 153 已装)⇒
退路走 `browser-harness`,但**必须用下�「独立 headless 实例�那套,��附�用户日常 Chrome**。
拿�到�览器时,**��页与 API �数一律用 `curl`/`WebFetch`**,UI 结构用「无�览器 harness�验收(�阶段 4 第 8 �三层验收)。
⛔ **`agent-browser` 2026-09-11 实测记录(�留作背景,勿�此�定上�的用户�径)**:
① 自带 Chromium �从 `storage.googleapis.com` 下 196MB → **必然超时失败**;
② 改用本机 Chrome + `--cdp` 时,环境里有 `HTTP_PROXY=http://127.0.0.1:2349`(WorkBuddy �务代�),
CLI 连 `127.0.0.1` 的 CDP **也走代�** → `Timeout connecting to CDP`
(�先 `env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy NO_PROXY="127.0.0.1,localhost"` �通);
③ �使通了,**标签/会�状��一致**:`tab list` 显示 `[t1] about:blank`,而 `get url` 报 `login.html`;
`open <实例�域>` �生效。→ **��览器就用下�这套 browser-harness**。
**核心教训:��用用户日常 Chrome。** 直接 `new_tab` 到用户正在用的�览器会��弹「�许远程调试?�——用户会烦。根因:**绕过 wrapper 脚本**(`python -m browser_harness.run`)就�会设 `BH_RUNTIME_DIR_SHARED=1`,daemon 无法常驻�用 → �次调用新建连接 → **�次弹一次授�**。
**定型�法 = 起独立 headless 实例(零弹窗��碰用户�览器�cookie 隔离)**:
```bash
# ① 一次性�动(��常驻;PATH 必须先修好,�则 rm/node 会失败导致 Chrome 根本没起)
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:/c/Windows:$PATH"
"/c/Program Files/Google/Chrome/Application/chrome.exe" \
--headless=new --remote-debugging-port=9223 --remote-allow-origins='*' \
--user-data-dir='C:\Users\Administrator\AppData\Local\Temp\chrome-bh-dsh' \
--no-first-run --no-default-browser-check --no-proxy-server --disable-gpu about:blank
# ② 探活:curl -s --noproxy '*' http://127.0.0.1:9223/json/version
```
```bash
# ③ �次验�(把 python 脚本写�文件��定�进 stdin,�� heredoc 引�地狱)
R="C:/Users/Administrator/.workbuddy/skills/browser-harness"
export PYTHONPATH="$R/src"; export BH_HOME="$R/.browser-harness-dev"
export BU_CDP_URL="http://127.0.0.1:9223"
"$R/.venv/Scripts/python.exe" -m browser_harness.run < "C:/Users/.../shot.py"
```
**登录�**:DB 直�临时 session(`user_agent=poc-ui`,用��删),� token �
`cdp("Network.setCookie", name="sid", value=TOKEN, domain=".ai1net.com", path="/", secure=True)` → `Page.reload`。
用户侧真实�览器完全��影�(�覆盖其 sid)。
**三个必踩�**:
- **`Emulation.setDeviceMetricsOverride` 按 target 生效** → 必须**先 `new_tab` �设覆盖**;设在旧标签上�开新标签 = 覆盖丢失(截图�有 758×482)。
- **hash 残留会骗人**:页��在上次的 `#/plugins/manual`,�载�目标元素�存在 → `js()` 返回 null / 元素测�得 `0/0`。**验�脚本必须显�导航到目标 tab**。
- **Chrome 必须先 `--headless=new` ��跑通�加��**;���动失败时先看 task 输出(常�是 PATH 未设导致�置 `rm` 失败,`&&` 短路,Chrome 从未执行)。
- 收尾:临时 session 删除;独立 Chrome �留��用(内存�),��就 kill **9223** 端�对应进程。
3. **��文件 vs �端改动**:改 `web/*.html` **无需��**(��直出,实测�务�动时间��);改 `src/**` � `npm run build` + `systemctl restart`。**别为�端改动���务**(会打断所有用户实例)。
4. **安全��测**:�� uid 探测�感目录(/root�/etc/shadow�DB)应 denied;中间目录 711(去 r 留 x)�破��进程访问。
5. **清�临时物**:`poc-*` session 用完�删(DB DELETE);测试 tab 关闭;临时 tgz/脚本归档或清。
6. **截图留�**:�功能验收存 PNG 到本地 tmp + present_files 展示。
7. **🔴 交付�必问一�:这份改动「出厂�了�**(2026-09-13 实� —— 用户第 2 次�馈「样�还是没��的**真因之一**):
改完 `lib/*.js` **≠** 用户会看到�化。出厂链固定为
**改文件 → bump `package.json` 版本 → `npm pack` → 上传 `/opt/dsh/artifacts/<包>-<版本>.tgz` → `node /opt/dshs/scripts/ensure-<包>.cjs --all --restart`**。
- **一秒自检**:`ls -la` 对比「�文件 mtime�与「最新 tgz mtime�—— **�文件更新 ⇒ 还没出厂**。
- ⚠� 打包脚本里带断言时**先把锚点核对存在�跑 `npm pack`**:断言若在写文件�抛错,版本�没改但 tgz 已被覆盖 ⇒
得到「内容是新版�版本��写旧版�的**�产物**(本次已踩,�手工删该 tgz 兜�)。
- 缓存�是借�:`src/supervisor/proxy.ts` 已对 `/plugins/`�`/assets/` 下� `Cache-Control: no-cache`,且模� URL 自带内容 `rev=<sha1>`
⇒ **bundle 一��览器下次请求就拿到新的**(「看�到�化�几乎都是没出厂,�是缓存)。
8. **视觉/UI 交付必须三层验收,且�拿到「真正下�到�览器的那份�**(2026-09-13 定型):
| 层 | �什么 | 能�现什么 |
|---|---|---|
| ① �测 | `scripts/verify-*.mjs` 断言 **class �与结构**(��断言文本),关键结构�项写死(例:**5 个弹窗都必须是 `.table-wrap + table.tbl`**) | 结构走样�分支没改到 |
| ② 实装 | `md5sum` 对� **本地产物 / � profile 的 `node_modules` 实链**,并断言**旧标记为 0** | 没铺��铺了旧版 |
| ③ 端到端 | `mksess.cjs` 临时会� → �实例首页 → 解出 `/plugins/??…&rev=<sha1>` → `curl` 回 bundle → grep 新/旧标记(**用完�删临时会�**) | bundle 没下��下�的是旧内容 |
> �� ① ⇒「脚本全绿但用户说没��;�� ①② ⇒ �掉「用户拿到的那份�。
9. **🔴 用户说「�考门户/�个现�页�的样��时:抄那个页�的��,��抄规范的抽象�目**(2026-09-13 第 3 次踩):
正确动作 = 打开 `web/portal.html` / `web/admin.html` 的 `<style>`(或它 link 的 `design.css`),
把 `.nav-card` / `table.tbl` / `.badge` / `.btn-sm` / `.card-h` / `.page-title` 的**实际�值���过�**,
并在注释里写明「本类� � 门户�类��的对应表(便于�核)。
⛔ ��:�引 `06-工作�UI规范 §4.5/§4.7` 的抽象�目�自行演绎 —— 用户会说「还是没��。
唯一�许的差异:**颜色走 dsh token `--dsw-*`**(嵌在 dsh ��内须�主题);字�/间�/圆角/动效与门户**�值一致**。
### 阶段 5 · 归档清�(文档�步 + 三层沉淀)
0. ★★ **交付门�:本机改完 ≠ 交付**(2026-09-15 加 —— �类已�生 **3 次**:09-13「��到本地打包�没部署�→ 用户「点开看还是和之�一样�;09-14「应用更改是哪里…还是和之�一样�;09-15 会�原�「**平�那�我�改了本机,从没部署到�务器 —— 那你看的当然还是旧页�**�)
**收尾��项自问三�:① 这次改的是哪一层?② 这一层的生效链路是什么?③ 最�一步走了��在「用户����验了�?**
| 改动所在层 | 生效链路(**缺一步都�算交付**) | 用户����验 |
|---|---|---|
| **平���页**(`web/*.html`�`web/i18n.js`�`portal.html`…) | `scp` 到�务器对应路径(如 `/opt/dshs/web/`)→ 注� **CDN/CF 缓存**(2026-09-14 实测��资�被 CF 缓存 **31 天**) | 用 `curl` �**线上**页�(带 `?lang=` 等�数)确认新内容;必�时 cache-busting |
| **平� TS**(`src/**`) | `npm run build` → **`systemctl restart dshs`** | 线上端点 / 页�实测(**�是**本地 build 通过) |
| **自研�件**(`poc/*`�client bundle / host �) | 打 tgz → **admin 导入候选池 → 实例「功能管���用** → **��实例**(client bundle 在**实例�动时**加载,� `06-工作�UI规范.md §7`) | �实例页 HTML 里真实 bundle URL **�串验�**(`rev` 已�) |
| **文档库 / 技能** | `docs-sync-check.sh` 对账 → `scp`(�相对目录)→ **�跑对账** | 对账「一致�计数回��且**残留项全是别人的 lane** |
| **�置**(env / nginx / nft / ��) | 改 → reload / restart 对应�务 | 生效值实测(`systemctl show` / `curl` / `nft list`) |
⛔ **四�"自我安慰",一�都�算交付**:**本机改完了** / **build 通过了** / **本地打包完�** / **已 commit 了**。
⛔ **也别回头问用户「���部署�**:�打断在线用户的上线属**我的 lane 内执行细节 ⇒ 直接�完**(`CODEBUDDY.md §3 R7-边界` + 素�库 **U20 / X9**);�有**��逆破�性�作**与**边界外六类**�先问。
📌 与**阶段 4** 的分工:阶段 4 验"**改对了没有**"(三层验收);本门�验"**东西到用户那儿了没有**"(链路完整性)。
1. **文档�盘(2026-09-13 现状:本地工作树 = �,�务器 = �读镜�)**:
- **�** = 本地 `D:\github\dsh_shenxian\dsh-server-docs`(**目录� git 工作树**),推 Gitea `dsh_shenxian_doc.git` 的 **main**。
- **四件套(顺�固定)**:`docs-audit.py` → `docs-index-stats.py --write` → `docs-manifest.py` → `docs-sync-check.sh`;�有 `docs-consistency.py`。
- **传�务器**:用**�个 `tar` 管�**(�个 `scp` 会因��一个 SSH 连接而超时):
`tar -cf - <相对路径…> | ssh bt-server 'cd /opt/dsh/docs && tar -xf -'`,�� `chmod`(目录 700 / 文件 600)。
- **对账**:`bash scripts/docs-sync-check.sh`(**�务器镜�无 `.git`**,�能� md5 对账;�点:`core.autocrlf=false` + `.gitattributes` 的 `* -text` 必须��)。
- ⛔ **`docs-status-sync.sh` 已�存在**(旧�程残留,**勿�引用/调用**);状�摘��在 `/opt/dsh/docs-status/文档库状�备注.md`。
- ⚠� **本库多会�共用** ⇒ �交�先 `git status`,**� `git add` 自己改的文件**;别人在途的改动(2026-09-13 实例:档案 76 md�`skills/dsh-opensource-release/SKILL.md`�`docs-manifest.json`)**一律��交�� scp**。
`docs-manifest.json` 是**派生文件**:它�述的是整库状�,若别人有未�交改动,**跟�一起�交会把别人的���「顺手上��**⇒ 此时��让它�� dirty。
2. **档案更新**:`04-调整方案/<NN>-*.md` 写"需求→改动文件→commit→验�记录";README.md 档案清�行�步;版本� bump。
- ⚠� **档案�必须『原�预留�,�能�"读一眼 ls �写"**(2026-09-11 **两次**撞�实�):**"�� + 写档案 + 改 `INDEX.md` + 改 `docs-status` �一串行事务"这�规则本身�够** —— 两�并行通��时 `ls` 时都读�到对方的文件,必然撞。**第二次撞�(37/38 �两�,4 个文件挤 2 个�)就是这么�生的**。
- **正确�法(原��)**:写档案�先原���,例如
```bash
cd /opt/dsh/docs/04-调整方案
n=$(ls | grep -oE '^[0-9]+' | sort -n | tail -1); n=$((n+1))
mkdir ".lock-$n" 2>/dev/null || { echo "�用,��"; } # mkdir 是原�的
```
或等价地用 `flock /opt/dsh/docs/.numbering.lock -c '...'` 包�"��+�文件"整段。**��与�盘之间的窗�必须�短,且��在中途放开去�别的任务。**
- **撞��处置**:�留**已 commit** 的那个��动(commit message 已引用它),把**自己未�交**的那个顺延改�(`mv` 文件 + 改 INDEX 行 + 改自己的 docs-status �更行 + 在自己档案内修正交�引用 + 加一�撞�说明)。**改��在�交��**,�则�制造一次引用错�。
- ⚠� **`docs/README.md` 的档案清�已��(止于 28)** → **���改 README**;全�清�在 `docs/INDEX.md`,以它为准。
3. **三层沉淀**(用户习惯,必�):
- 治本层:机制/�程固化进 skill/档案(本次�本 skill)
- 失误层:教训 append 到 `.workbuddy/memory/YYYY-MM-DD.md`(append-only,�改写历�)
- 记忆层:长期约定/红线写 `.workbuddy/memory/MEMORY.md`(≤3000 字符/会�)
4. **汇总表**:带状�/结果列交付,列明未完�项与副作用。**排版按 `dsh-feature-first §5.4`**(首�给判定 / 层级≤3 / �节≤7 行 / 表格≤5 列 / 一�信��说一次;细节进附录)。
5. **技能自身归档(本 skill 有改动时必�)**:工作副本在本机 `.workbuddy/skills/<name>/SKILL.md`(技能必须本地加载),�务器归档�在 **`/opt/dsh/docs/skills/<name>/SKILL.md`**(root 600)。改完本 skill �**��推**:
```bash
P=/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0
export PATH="$P/usr/bin:$P/mingw64/bin:/c/Windows/System32:/c/Windows:$PATH"
S="/c/Users/Administrator/AppData/Local/Temp/wb-scratch"; mkdir -p "$S"
cp "<本机 skill>/SKILL.md" "$S/SKILL.md" # 中文路径先转纯 ASCII
scp -i ~/.ssh/id_ed25519_dsh -P 22 "$S/SKILL.md" [email protected]:/tmp/SKILL.md
ssh -i ~/.ssh/id_ed25519_dsh -p 22 [email protected] \
'D=/opt/dsh/docs/skills/<name>; mkdir -p /opt/dsh/backups/docs-skills; \
cp $D/SKILL.md /opt/dsh/backups/docs-skills/SKILL.md.bak-$(date +%Y%m%d_%H%M%S); \
cp /tmp/SKILL.md $D/SKILL.md && chmod 600 $D/SKILL.md && chown root:root $D/SKILL.md && rm -f /tmp/SKILL.md'
```
**md5 必须�端一致**�算完�;�时更新引用它的元数�:`README.md` 模�表行�`INDEX.md` 场景速查行 + 全�清�行。**方�固定「本机 → �务器�,勿��覆盖**(�务器副本是归档,�是工作副本)。
⚠� **改「跨载体规则�时必�全载体扫�**(2026-09-15 实�):一�交互 / �程规则往往**�时躺在多个载体里**,
�改一处 ⇒ 其余载体立刻自相矛盾,**比�改更糟**。动手�先 `grep -rn` 关键短语,把命中处**全部**列出:
| 载体 | 典型�置 |
|---|---|
| 技能正文 | `SKILL.md` 的 §/�律/硬约�/�模�/自检 + `references/**` |
| 项目根常驻层 | `CODEBUDDY.md §1`(**连带�节的指针行**,如「排版按 …�) |
| 用户级记忆(跨项目) | `~/.workbuddy/MEMORY.md` |
| **钩�文案** | `scripts/stop-dialog-guard.py` 的 `REASON` / `CONTEXT` |
| **生�物(勿手改)** | `dsh-env-bootstrap/references/常驻规则-快照.md` ⇒ 改完��跑 `python resident-rules.py --snapshot` **�生�**,� `--check` 应 RC=0 |
| 登记行 | `README.md` 模�表 · `INDEX.md` |
判�:改完� grep 一�,命中应**�在已改处**。**钩�文案最关键** —— 它�轮都在"教训 AI 该怎么�",
与规则打架时�害最大(本轮实�:钩�写「��在结尾甩问题�,用户�「待确认内容放到最��,两��直接对撞)。
收� = 上表**�格确认** + 两副本 md5 一致 + 镜� md5 一致 + 七件套全绿。
## 档案模�(04-调整方案/<NN>-<标题>.md)
```markdown
# <NN>-<标题>(<日期> �地 / 调研)
## 背景与动机 # 需求���触�场景
## 用户决策 # 关键分� + 选择 + �由(�日期)
## 实现 # 改动文件清� + commit hash + 关键代�/�置片段
### A. ... # 分模�
## 验�记录 # 实测命令 + 输出 + 结论(�失败�试)
## 事故/踩�记录 # �现象→根因→规�(若适用)
## 回滚 / 注� # 回滚步骤�副作用��续待办
```
## 红线 R1-R8 原文速查
1. **�止�动 dsh 时自动获�最新版本**;版本�级独立�程(档案 07;/usr/local/etc/npmrc `update-notifier=false` 已全局生效)。
2. **�改官方 dsh 主程�与缓存**(`/usr/local/lib/node_modules/@deepseek-ai/dsh` ��赖);扩展�走 profile 层官方�件机制。
3. **client bundle 严� `exports.default = apply`**(或任何 default 函数导出)——详�上文 R3 解释(@dsh-local/portal-entry v0.4.0→v0.4.1 实�教训)。
4. **�止用真实账�(`admin`/`guest`)� API 登录测试**:登录 last-wins 会删该账�全部旧会�,直接把用户踢下线。改用 `mksess.cjs` **PG 直�**临时 session 或专用测试账�(档案 15 实�教训)。
5. **������准收窄,扩大必须先确认**(2026-09-11 用户新增,R5)——�下节「R5 ��扩大门��。
6. **R7|�止未�确认的批� / 全仓写入**(2026-09-12 用户新增红线)—— �下节「R7 批�写入门��。
7. **R8|中断在线用户的生产�更须先确认**(2026-09-12 用户新增红线)—— �下节「R8 生产�更知会�。
### R7 批�写入门�(2026-09-12 用户新增红线)
> **用户原�**:「为什么会犯这�错误,容易把�务器�崩,必须记录在红线中。�
**由�(真实事故)**:为了让 scp 出去的文件行尾干净,写脚本�历整个代�库把 **147 个文本文件** CRLF→LF。当期被�求的�是一�「把�个 UI 字符串改个��,**�作�径放大了两个数�级**。
**已�造�的实际�害**(�是�论风险):
- 147 个文件被标记为 M —— 若继续 scp / �交 / 推�,会覆盖�务器上正确的版本�产生巨型 diff 掩盖真实改动�并与并行会�冲�;
- ��用 `cp -r` �步�件目录时,把**当期并没有改过的** `cordis.patch.yml`�`lib/index.js` 也用 CRLF 覆盖到了**�务器**(已�现并��)。
**规则(硬性)**:
1. **��被明确�求的事**。执行中�现的�外问题(哪怕看起�"很��很好修")一律**先报告��动手**,�得顺手改。用户说"按建议处�"�授�**那�建议本身**,�是授�一切顺带优化。
2. **�止**对仓库或生产目录�:全库�历改写(`os.walk` / `find -exec` / `grep -rl | xargs`)�通�符�写�批� `chmod`/`chown`�**批��行符转�**�`cp -r` 整目录覆盖�`git add -A`。
3. **阈值**:一次�作若**�能影� >10 个文件**,或表述里出现「所有 / 整个 / 全库 / 全部�→ **�下�先问**,先产出**�影�清�**�决定。
4. **本机�是沙箱**:本机镜�与�务器是两份独立副本;本机的批�改动�便�带任何"部署"动作,也会在下一次 scp 时传导到生产。
5. **先用�点验�**:任何批�手段先对 **1 个对象**试,确认�果(`git status`�`file`�`md5`)符�预期�考虑推广。
6. **传播�比对待传清�**:scp / �步�必须 `git status` 确认**待传清���本次真实改动**,��被工具顺手改动的文件。
7. **行尾类问题一律先报告**:仓库 blobs 是 LF 而本机工作树因 system 级 `core.autocrlf=true` 呈现 CRLF(`D:/Program Files/Git/etc/gitconfig`)—— 这是**既有环境事实**,�构�"需�当场修�"的缺陷;�动必须先与用户确认范围。
### R8 生产�更知会(2026-09-12 用户新增红线)
**由�**:档案 58(内存优化)�59(�连�馈)两次改动都需��� `dshs`,**连续两次把在线用户踢下线**,直接引�用户"会�连接异常"报障。
**规则**:
1. 以下动作**都会中断在线用户**,执行�必须先说明「**影���断多久�为什么必须现在�**�并�得确认:
- `systemctl restart dshs`(drain 全部实例)
- `systemctl stop dsh-*.scope`(��个用户实例)
- 批�铺�件 / 改 `MemoryMax` / 改实例 env(都�实例���生效)
- �建 profile�改 profile patch
2. **能选低峰期就��在用户活跃时�**;无法��时明确告知"会断一次"。
3. **改完�验�**:���必须确认�务 active + 门户 200 + 实例能被拉起,��用户交代。
4. 附带:本机 `D:\github` 下的镜�**任何批�改动都视为"�能影�生产"**(� R7 第 4 �)。
### R5 ��扩大门�(2026-09-11 用户新增红线)
**先判方�:这次改动是「扩大�还是「收窄�?**
| 方� | 例� | 处置 |
|---|---|---|
| **收窄** | �少挂载�去掉白��项�收紧 nft�收窄 env | �直接�,但**�须验�**(�蔽类�能让实例起�� → 档案 42) |
| **扩大** ⚠� | 新增 bwrap 挂载 / `--bind`�放开被�蔽的路径�把平�目录或文件暴露给实例�给实例注入新 env�放宽 `ALLOWED_ENV`�放� nft(出网或宿主访问)��高��档�或放宽 `approval`�新增用户�读/�写路径�把 root 执行链路(解压 / chown / pnpm)的对象��用户�控 | **一律先出「��影�评估�并等用户明确��**,�止"顺手�了" |
**「��影�评估�四问(方案里必须��写,档案留痕)**:
1. **扩了什么** —— ��列具体路径 / 端� / env / ���(��写"优化了访问"这��糊�);
2. **��影�** —— 全部租户 / �租户 / 仅 admin;
3. **有没有�扩大也能实现的方案** —— 若有,必须先�;若确实没有,说明为什么;
4. **回滚方� + 验收方�** —— 回滚命令写清楚;验收**必须 diff 技能里的「实例�访问路径清�(��版)�**,
�项确认"新增项都是用户已��的"。
**🔒 安装类�作 = 扩大,必须先出「安装确认清��并�用户确认**(2026-09-11 用户明确�求:
「需�安装哪些�赖和工具,需�确认��能安装,��安装有风险的内容�)。
凡是�往**平�共享�**(`/usr/local/dsh-runtime`�`/usr/local/bin`�系统包)装东西,**一律�得自动执行**,
先列七项等用户点头:① �称+版本 ② **为什么需�**(哪个技能/�件在用)③ **��与校验方�**(官方 URL + sha256)
④ 体积 ⑤ 影��(**全部租户**)⑥ 风险点 ⑦ �载方�。
技能/�件**导入时的�赖体检��「报告 + 拦截�(缺失� 409),��代装**。
**触�文件(改这些就必须走 R5,无一例外)**:
`src/supervisor/orchestrator.ts`(bwrap args / `baseEnv`)�`src/supervisor/spawn.ts`(`ALLOWED_ENV`)�
`/etc/nftables-dsh-egress.nft`�`src/web/routes/skills.ts`�`src/web/routes/business-plugins.ts`�
`ensure-role-profile-patch.cjs`(角色 profile patch)。
> **历���**:档案 39(`/etc` 白��化)�40(bundled-skills 挂载)�41(技能上传/��)�42(Python �行时)
> 里�一次"扩大"都是在用户明确�求下��的;�例是档案 42 的�蔽�试 —— 属"收窄"但因触碰挂载结构
> 直接让实例起��,**收窄也必须跑真�动验�**。
### Profile 层 cordis patch 机制(角色化 UI �剪,档案 09)
- client �件行 id = **短 id**(`ui-settings-models`,�包�),� `dsh --profile web --dump-config`。
- **�用官方 client �件**(如设置��「模型�分区对普通用户):profile `cordis.patch.yml` 写 `- id: <短id>\n name: "@deepseek-ai/<包�>"\n disabled: true`(dsh-app-boot applyEntryPatches:� insert patch 按 id �入 overrides)。`--dump-config` 验�:目标行出现 `disabled: true` + `# == ... patched by <path>` 注释。
- **生效必须��实例**(client bundle �动时打包);`patchReload: live` 对 client �件增�**�生效**(实测 5 轮)。
- 当�生产 spawn �带 `--patch` overlay(enablePatch=false),disable �能写 profile 层 `cordis.patch.yml`。
- 幂等工具:`/opt/dshs/ensure-role-profile-patch.cjs [--restart] <username>`(全�=� admin 用户;�管�标记头则跳过;�默认空内容�覆盖)。admin profile �动 = admin �留该分区。
### Skill 装载机制(全员共享�读技能 = bundledSkillDir,档案 10)
- 技能由 agent preset 注册:`@deepseek-ai/dsh-agent-presets/presets/standard/agent.cordis.yml` L83-88(`skill-filesystem` + `tool-skill`),preset 无 config � → provider �置走 **env**。
- 技能分层 rank(`dsh-skill-filesystem/lib/index.js` roots()):project-dsh 100(`<项目>/.dsh/skills`)/ project-agents 200 / custom 300 / user-dsh 400(`$DSH_HOME/skills`,�用户独立)/ user-agents 500 / **bundled 600(`$DSH_BUNDLED_SKILL_DIR`,`trustedHost: true` �读共享)**。
- 扫�粒度:`discoverRoot` **�扫 1 层**(root 下�目录须� `SKILL.md`;或直接 .md 文件);`root/主技能/subskills/�技能/SKILL.md` **�会被�现** → subskill �独立�调需**平铺**到技能根。
- **编排器 env 白��(关键�点)**:`/opt/dshs/lib/supervisor/spawn.js` `ALLOWED_ENV` 仅 PATH/HOME/USER/TMP/LANG 等基础��;`baseEnv()` = `{...scrubEnv(process.env), HOME, DSH_HOME, DEEPSEEK_API_KEY}` → **新 env ��必须�时加白�� + baseEnv 显�注入**(src/*.ts �步),�则被 scrub 丢弃。这是改编排器(自研,�红线 2 对象)。
- MCN V1.0 技能跨平�部署注�:`subskills/browser-harness/envs/` 为 **Windows venv**(`Scripts/*.exe`),Linux �务器��用需�建;海外节点直连抖音风控风险高,数�类优先 RedFox API。
### Skill 管��(编排器 API + ��页,档案 11)
- **dsh 技能�硬规则**:`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`(`@deepseek-ai/dsh-skill/lib/index.js` SKILL_NAME)—— **中文�技能 dsh �默丢弃**。MCN 等�中文�技能需先改 `SKILL.md` frontmatter `name` 为 kebab(如 `mcn-workstation`)。
- **API 形�**:`/api/skills/shared`(admin 三件套 GET/POST/DELETE)+ `/api/skills/mine`(任何登录用户);上传 base64 body `{ file, filename, force? }`;�端走系统 `tar/unzip` 解压�路径穿越校验��法�校验。包结构:�顶层目录 + � SKILL.md。
- **�盘属主**:shared → root:root 0755(OS ��兜底�读);mine → `home` 属主用户 uid/gid(确�用户�改自己的技能)。踩�:`chownTree` 须 chown 顶层目录自身(首次实现� chown �项,目录�留 tar 包内 owner)。
- **watch �时生效**:`skill-filesystem` 对共享根 watchManager 监� addDir/unlinkDir/SKILL.md → 自动 invalidate registry(**无需��实例**,��已核实)。
- **编排器 env 注入 DSH_BUNDLED_SKILL_DIR**:spawn.ts ALLOWED_ENV 加�� + orchestrator.ts baseEnv 显�注入(`config.bundledSkillDir`,默认 `<dataRoot>/bundled-skills`);这是上一节「全员共享�读技能层�的实例侧�地。
## 通用�维锚点(��用)
- 门户���实例需�新 launch:`curl -b "sid=..." -X POST http://127.0.0.1:3080/api/dsh/enter` → 返回实例 port + token url
- 实例检测:`pgrep -af "^node /usr/local/bin/dsh --profile web"`;监�:`ss -tlnp | grep :<port>`
- 测试 session 生�:`node /opt/dshs/mksess.cjs`(**PG 直�**,连接串�自 env / `dshs.env` / `dshs.service.d`;10 分钟;`user_agent=poc-curl2`);清�:DELETE WHERE user_agent='poc-curl2'
- **存�会�档�体检(档案 36 P0-1,�读,建议定期跑)**:��档�**会�级播�**(建会�时写 `permission/preset` + `sandbox/mode` + `approval/policy`,**resume ��播**)→ 改默认档��惠�新会�。查全平�还有多少会�是旧档�:
```bash
for f in /var/lib/dshs/users/*/home/sessions/*/*/session.jsonl.zstd; do
[ -f "$f" ] || continue
printf "%-12s %s\n" \
"$(zstd -dc "$f" 2>/dev/null | head -c 4000 | grep -o '"preset":"[a-z-]*"' | head -1)" \
"$(echo $f | sed 's#.*/users/##')"
done
```
2026-09-11 实测:**总会� 10,`workspace-write` 10,`danger-full-access` 0** —— 档案 33 之�**一�都没�移**。
- **技能投放体检**:`for u in /var/lib/dshs/users/*/; do echo "$(basename $u): $(ls -A $u/home/skills 2>/dev/null|wc -l)"; done; ls -A /var/lib/dshs/bundled-skills | wc -l` —— 2026-09-11 实测**全为 0**(机制就绪但零投放,档案 36 P0-2)。
- **文档备份目录�先建**:`mkdir -p /opt/dsh/backups/docs` �则 `cp ... .bak-<ts>` 报 `No such file or directory`(本次踩�)。
- æœ�务器 git:`git -C /opt/dshs`(user.name/email = [email protected])
- **⚠� �务器 git �交�先 `git status --short`**:工作树常滞留未�交文件(曾有一次 `git add -A` 把 `/logout` 路由�`ensure-role-profile-patch.cjs`�`mksess.cjs`�`*.bak` 一并�进 `fa718d2`);应**定� `git add` �暂存本次改动文件**。
- **登录直达冷�动竞�(档案 13)**:`enter` 返回打开 URL �必须等 launch token(`waitForLaunchTokenForUser`);实例 spawn � `status` � running 但 HTTP 路由未就绪,�览器撞�动窗�会 404(`proxy.ts` 的 404 �对应 unknown_user/not_running,此 404 �自实例自身�传)。验�用「enter 并� + `--resolve` 回� curl�:URL 必须� `?token=`,�域应 303→200。
## 本机 Git Bash 环境�(2026-09-11 实�,几乎�次都踩)
- **PATH 常�**:`Bash` 工具里 `dirname`/`head`/`ls` 报 `command not found`,且 `~/.ssh/id_ed25519_dsh` 也找�到(PATH 里没有 ssh)。**�次先�置**:
```bash
P=/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0
export PATH="$P/usr/bin:$P/mingw64/bin:/c/Windows/System32:/c/Windows:$PATH"
```
- **node 是 Windows 程�,�认 `/c/...` msys 路径**(会拼� `d:\c\Users\...`)。脚本里写路径�用 `C:/Users/...`;而 `ssh`/`scp`/`cp` 这些 msys 程��用 `/c/...`。
- **中文路径下�文件���** → 中转文件先 `cp` 到纯 ASCII 路径(`C:/Users/Administrator/AppData/Local/Temp/wb-scratch/`)� `scp`。
- **heredoc 通� UTF-8 安全**:`ssh HOST 'cat > /tmp/x' << 'EOF'`(引�定界符)�原样传输中文,已多次验�;但**别在 heredoc 定界符��接 `' 2>/dev/null`** 之类的引�拼接,会 EOF 报错 —— ��脚本改�「本地写 → scp → 远端 node 执行�。
- **PowerShell 工具在本会�输出被�**(命令�功但无 stdout)→ 本地�作优先用 Bash(修好 PATH �)。
- **`.bak-*` �能已被 git 跟踪**(历��交过):��批�挪动/删除它们,�则产生�外的 `D` �更;��定� `git add`。
## 外部数��缓存必须带结构版本守�(2026-09-11 实�)
�数��或在缓存�目里增删字段时,**必须�时 bump 一个 `CACHE_VERSION`**,并在读缓存时校验版本/结构(�符�视为未命中�拉)。
踩�实录(档案 29):白��路由从「git clone + 解� yml�改为「拉 `plugins.json`��,`whitelist-cache/index.json` �是**旧格�**且 `fetchedAt` 在 TTL(6h)内 → 被直接�用 → �目缺 `owner`/`importKind` 字段 → 按字段过滤时 `e.owner.toLowerCase()` 抛异常 → **接� 500**。加 `CACHE_VERSION` 守��自动�拉修�。
�套好习惯:① 缓存读失败/��用时**�级用旧缓存并返回 `stale: true`**,�端�示;② 排�按「热度�字段��,让无效�目自然沉底,默认首页就是最有价值的那些。
## 官方白���件��(档案 29 定型�径)
- **数��**:`https://awesome-dsh-plugin.com/plugins.json`(官方规范地�,3.1MB / 3408 � / 23 分类;npm 镜�包 `dsh-plugin-catalog`)。字段:`name/owner/url/page/category/description{zh,en}/npm/version/stars/downloads/install/added`。**��**�去 git clone 仓库解� 3431 个 yml —— 其 `tarball` 是�选字段,覆盖率仅 6%(211/3431)。
- **导入�径 = 仅预构建**(平�侧��执行第三方构建脚本):有 `npm` → `registry.npmjs.org/<name>` → `dist-tags.latest` → `dist.tarball`(scoped 包地� `@scope%2Fname`,� `encodeURIComponent(name).replace('%40','@')`);有 `tarball` → release 资产;两者皆无(`install` 为 `github:owner/repo`)→ 拒�并回显原因。**�导入 1792/3408 = 52.6%**,官方下载� TOP10 全覆盖。
- **npm 官方 tarball �直接喂现有 `stageTgzArchive`**(`package/` 包装层已被 `findPackageJson` 兼容)→ 无需改 DB schema。
- **筛选排�技巧**:给列表加 `onlyImportable=1`,并在�端把「�看�导入�默认打开;按 `downloads` ��使未� npm 的�目自然沉底。
- **安全兜底�外包**:收录 ≠ 安全审计(上游 README 明确)。所有导入包�走平� `stageTgzArchive` 扫�,P0 命中�阻断,并把原因**在页�内回显**给 admin 判断(例:热门�件 `FuRongJun-1999/dsh-memory` 因 `docs/…example.yml` 被判 P0,疑似误报)。
## dsh 沙箱与��预设机制(2026-09-11 ��核实,改默认值必看)
**两层隔离,别混淆**:
| 层 | ��供 | 说明 |
|---|---|---|
| **内层** | dsh 自带沙箱 `read-only` / `workspace-write` / `danger-full-access` | � Landlock 或 bubblewrap �端。**本机��用**:内核 5.10(Landlock 需 5.13+,LSM 无 landlock),bwrap 嵌套探测失败 → **fail-closed 拒�执行任何 shell**(`@deepseek-ai/dsh-sandbox/lib/index.js:185`)|
| **外层** | 平� `systemd-run --scope` + `bwrap` + `setpriv` | **真正的边界**:mount 路径隔离 + uid 隔离 + cgroup(**基础 448M → 上界 1024M**:`MemoryHigh=MIN` 软� + `MemoryMax=MAX` 硬�;**与�件开关无关**(档案 96)/150%/128)。内层失效�影�它 |
**��预设�件**(row `@deepseek-ai/dsh-permission-presets`,短 id 一般� `permission-presets`):
- 内置预设表(**sandbox 与 approval �对绑定**,这是关键�):
```js
'workspace-write': { sandbox: 'workspace-write', approval: 'ask' }
'danger-full-access': { sandbox: 'danger-full-access', approval: 'never' } // � 选它=�时摘掉审批
```
- `defaultPreset = config.defaultPreset ?? inferredDefault`;默认� `workspace-write`(→ 本机�「shell 全废�)。
- 设置�久化命�空间 = **`permission`**(settings key `permission.defaultPreset`,值必须是 `presets` 里的键�)。
**改默认值的官方�法(�碰官方主程�,R2 �规)**:
> � **首选:`DSH_PERMISSION_MODE` 环境��**(2026-09-11 定案,档案 33)。
> 官方 `@deepseek-ai/dsh-base/cordis.patch.yml:217/233` 直接读它:
> ```yaml
> mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
> policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'"
> ```
> 在编排器 spawn 时注入 `DSH_PERMISSION_MODE=danger-full-access` �� **全局默认「完全���+ �审批**,且:
> ① �必铺 profile(env � spawn,**新用户自动生效**);② 完全�用官方逻辑(预设表 / 设置 UI 都�动)。
> �地两处:`spawn.ts` 的 `ALLOWED_ENV` 加该键 + `orchestrator.ts` 的 `baseEnv()` 注入(值� `process.env.X ?? 'danger-full-access'`,�维�覆盖)。
> ⚠� **副作用**:��档�是**会�级播�**(会�创建时写 `permission/preset` / `sandbox/mode` / `approval/policy` 三���事件)→ **旧会��跟�新默认**,必须**新建会�**�生效。给用户的�术是「新开一个会��,�是「刷新页��。
其余�法(需�更细粒度时�用):
1. **profile 层 `cordis.patch.yml` 改该行 `config`** —— 已核实 `applyEntryPatches` 里
`const { id, insert, name, ...overrides } = patch` → **patch 的其它键会作为 overrides �入目标行**,所以 `config:` �覆盖。
注� **`config` 是整体替��是深�并** → 必须把内置的两个预设一并写全。
2. 或改设置:`$DSH_HOME/settings.yaml` 加
```yaml
permission:
defaultPreset: <presets 里的键�>
```
3. **profile 是�用户一份** → �� `ensure-role-profile-patch.cjs` 那样给�个用户(�新用户)铺;改完**必须��该用户实例**�生效。
4. 用户��在实例「设置 → ���自助切�(`ui-permission`),这是设计内的。
**推�形�**:��直接用 `danger-full-access`(会连审批一起摘掉)。在 patch 里**新增一个自定义预设**:
`sandbox: danger-full-access` + `approval: ask`,语义 = 「隔离由平�容器�供,高风险�作�需确认�。
## 实例��� · 软件共享 · 网络与安全边界(档案 38a,2026-09-11 实测)
### �律:实例能看到什么,完全由 bwrap 挂载�决定
`orchestrator.ts` `spawnAsUser()` 的 `bwrapArgs` **�挂这些**(照抄,勿凭记忆):
```
--ro-bind /usr /usr � � /usr/local(宿主装一次 = 全员共享的现�通�)
--ro-bind /lib64 /lib64
--symlink usr/bin /bin ; usr/sbin /sbin ; usr/lib /lib � ��根(档案 23)
--tmpfs /etc + 14 项文件白�� � �下「/etc 白���;**��用 --ro-bind /etc /etc**
--dev /dev --proc /proc
--bind <userRoot>/tmp /tmp � �用户独立,1777
--bind <userRoot> <userRoot>
--ro-bind-try <userRoot>/home/profiles/<web|headless>/{cordis.patch.yml,package.json,pnpm-lock.yaml} � �读,防绕过投放管控
--unshare-pid � 注�:**没有 --unshare-net**
--chdir <userRoot>/ws -- setpriv --reuid <uid> --regid <uid> --clear-groups <cmd>
```
> ⚠� **最常�的�**:**平�侧任何新增的共享目录,都必须�步补一� `--ro-bind`,�则实例内根本�存在**。
> 实�(档案 38a P0):`bundledSkillDir` = `<dataRoot>/bundled-skills`(=`/var/lib/dshs/bundled-skills`)**没被挂载** → 以平�原样�数进实例 `ls` = `No such file or directory`;而 `dsh-skill-filesystem/lib/index.js:84,181` 是 **`resolve()` + 实例内直接读盘**(�是编排器推数�)→ **档案 10/11 的共享技能层实际失效:技能投进去也�现�了**。修法 = `'--ro-bind-try', bundledSkillDir, bundledSkillDir`(放在 `--bind root root` **之�**)。
#### `/etc` 白��(档案 39,2026-09-11 �地)
**`--ro-bind /etc /etc` ��用** —— 实例会读走 `/etc` 下 **576 个 others-readable 文件**,�**平�情报**:
`/etc/systemd/system/dsh-*.{service,path}`�`/etc/nftables-dsh-egress.nft`(**出网护�规则全文**)�
`/etc/cron.d/dsh-*`�`/etc/letsencrypt/renewal/*.conf`。
正确写法 = **空 `--tmpfs /etc` + �文件 `--ro-bind-try`**,白�� 15 项(�行时实测必需最�集):
`ld.so.cache` `ld.so.conf` `passwd` `group` `nsswitch.conf` `hosts` `resolv.conf` `host.conf`
`services` `localtime` `os-release` `machine-id` `pki/tls/certs` `pki/ca-trust` **`alternatives`**
- ⚠� **symlink 必须 `realpathSync()` �绑「真实目标 → symlink 原路径�**,�则实例内是断链
(`nsswitch.conf` → `/etc/authselect/`;`localtime` → `/usr/share/zoneinfo/`)。
- 🔴 **`alternatives` 是「软链枢纽目录�,必须整体挂 —— 这是最容易��且事故最�蔽的一�**
(2026-09-11 实测踩到):`/usr/bin/python3` 是**两跳软链**
`/usr/bin/python3 → /etc/alternatives/python3 → /usr/bin/python3.6`,
中间一跳在 `/etc` → 沙箱内 `python3` **�默 `command not found`**(�是报错,是"命令�存在")。
一次就打断 **21 个命令**:`python3` `python` `pip3` `pip-3` `pydoc3` `python3-config` `pyvenv-3`
`easy_install-3` `unversioned-python` `ld`(→`ld.bfd`,node-gyp 编译�用) `pax`
`print-*` `ifup` `ifdown` `lpc`。
→ **判定准则:�绑"直接指� /etc 的软链"是�够的,必须枚举"整�链�会穿过 /etc"的�目**:
```bash
find /usr/bin /usr/sbin /usr/libexec /usr/local/bin -maxdepth 1 | while read -r f; do
[ -L "$f" ] || continue; c="$f"; n=0
while [ -L "$c" ] && [ $n -lt 10 ]; do t=$(readlink "$c")
case "$t" in /*) c="$t";; *) c="$(dirname "$c")/$t";; esac
case "$c" in /etc/*) echo "$f -> $c";; esac; n=$((n+1)); done
done | sort -u
```
安全性:`/etc/alternatives` 33 项**全部指� `/usr` 或 `/lib64`**(已�读挂载),��凭�/平�情报。
- ⚠� **这类回归的唯一验收手段是"工具清���对比"**(`for c in …; do command -v $c; done`)或**会���**
—— 它�报错��崩溃,�是能力�默消失,所以 `/etc` 白��类改动**必须跑一次全�工具对比**。
- 实测效果:��项 **222→13**��读文件 **576→27**�平�情报 **4/1/2/8 → 0/0/0/0**;
�时 `node` / `os.userInfo` / DNS / node-TLS / `curl` / `dsh --dump-config`(549 行/176 �件)**全通**。
- 凭�类本�就读�到(`dshs.env` 600�`shadow`/`gshadow` 0000�`sudoers` 440)——
**别把"没泄露"当�本次收益**。
- 判�:**`/etc` 里凡是"平�的秘密"就白��化**;`/usr` �必动(审计�实零凭�,且是�行时宿主;
`/usr/bin` 的 1151 个 CLI 是 agent 唯一手脚,去掉=平�核心能力归零)。
#### ⛔ H5 红线:OUTPUT 链按「目的地���本机�务 = 自伤(档案 39 实测踩到)
实例是「先收�回�的�务端:nginx(root) → 实例端�的 SYN �法,但**实例回的 SYN-ACK 与�续数�包
`daddr` �样是 `127.0.0.1`**。若写 `meta skuid <uid段> ip daddr {127.0.0.0/8,…} reject` 一刀切,
**回包会被一起拒掉** → nginx 无法回��**实例整体��用**。
- **症状指纹**:root 连实例端� **timeout(丢包)而�是 refused**。refused = 规则�打在客户端方�(正常);
**timeout 往往�味���都被打到**。
- **正确写法**:�匹�**主动�起**的连接 —— TCP 用纯 SYN�UDP 用 `ct state new`:
```
meta skuid 100000-199999 ip daddr { 127.0.0.0/8, 172.17.0.1, <eth0>, <公网EIP> } \
meta l4proto tcp tcp flags & (fin|syn|rst|ack) == syn counter reject with tcp reset
meta skuid 100000-199999 ip daddr { …�上… } meta l4proto udp ct state new counter reject
```
- 与平�既有 `src/supervisor/firewall.ts` 的 **portGuard**(iptables `-m owner ! --uid-owner 0 -j REJECT`)
**语义一致�范围更全**,且��赖 `config.portGuard` 开关;portGuard 的存在**��实例进程自身�需�任何
loopback 连接**。
- **验�闭环(缺一��)**:① root 回�必须拿到 `303`/`200`(**�能�看 `ss` 有 LISTEN**);
② 实例内自连宿主端�必须 `ECONNREFUSED`;③ `nft list table ip dsh_egress` 看 counter 是�命中。
- **改�先查有没有实例在跑**:`systemctl list-units "dsh-*scope" --no-pager --all`。
- 一键�现:`bash scripts/install-egress-guard.sh`(� nft + service,已纳入版本控制)。
#### ⛔ 无法�蔽 `/usr` 内的文件(档案 42 实测踩到 → 实例起��)
想「�蔽 `/usr` 里�个文件�(例如�掉旧解释器 `python3.6`)时**�能用�加挂载**:
`--ro-bind /dev/null /usr/libexec/platform-python3.6` 会让 bwrap 报
`Can't create file at …: Permission denied` → **实例直接起��**。
根因:bwrap 需�**在 DEST 创建挂载点**,而 `/usr` 是 **ro-bind(�读)** → 创建失败。
`/etc` 之所以能自由白��,是因为它先 `--tmpfs`(�写)��项 `--ro-bind-try`。
- **判�**:**�有「先 tmpfs �白���的目录��自由增删**;ro-bind 的目录�能整目录决策。
- **�行�体(未采用,收益低风险中)**:`--tmpfs /usr/libexec` + rebind 必需�项
(`git-core` `getconf` `gawk`/`awk` `coreutils` …)→ 与 /etc �构的白��,但�有�类的"�默回归"预案。
- ⚠� **改 bwrap �数�必须真�动一次实例验�** —— 本类错误会让**所有实例无法�动**(�是�默�级);
改�先确认无实例在跑:`systemctl list-units "dsh-*scope" --no-pager --all`。
#### 实例共享 Python �行时(档案 42)
- 系统 `python3` = **3.6.8**,本体 `/usr/libexec/platform-python3.6` 是 **dnf/yum 的兄弟解释器**
(shebang 用�对路径 `/usr/libexec/platform-python`)→ **�能移除**;而 `/usr/bin/python3`
�是两跳软链(� `/etc/alternatives`),**动它安全**。
- 平�已装**�移� Python 3.12**:`bash scripts/install-python-runtime.sh`(幂等 / �离线)
→ 解压 python-build-standalone 到 **`/usr/local/dsh-runtime/python-3.12.14/`**,
`/usr/local/bin/{python3,python,pip3}` 指过去。
`/usr` 已 ro-bind + `/usr/local/bin` 在实例 PATH 首� → **实例内 `python3` � 3.12,零代��无需��**。
- **�移打包�元 = `/usr/local/dsh-runtime/` 一个目录**(自包�,��赖系统 rpm):
`tar czf dsh-python-runtime.tar.gz -C /usr/local dsh-runtime` → 目标机解压回原� + �跑脚本(��建软链)。
- 📌 **对 AI/用户的引导(这是"3.6 �开放"的正解,�是�蔽)**:明确写「Python 用 `python3`(= 3.12);
`python3.6` / `python2` 是系统�留�**��支�**�。实例内 `/usr/local/bin` 在 PATH 首�,
`python3` 已指� 3.12 —— 引导的�本为零�风险为零;而�蔽属"改挂载结构",会让实例起��(�上)。
### 🔒 基础�行时版本冻结(档案 44)
**�求:�止用户以任何方��级 Python / pip / node 等基础包**,��版本差异导致�件功能��用。
**已�天然�立的护�**(都�需��外代�):`/usr` 是 ro-bind →
实例内**改�了** `/usr/local/dsh-runtime/**`;`pip install`(默认)被 Read-only 挡;
`npm/pnpm -g` 失败(`/usr/local/lib/node_modules` �读);`$HOME/.local/bin` **�在**实例 PATH(固定
`/usr/local/bin:/usr/bin:/bin`);dsh 主进程由 orchestrator spawn,PATH �自 root 环境,��用户影�。
**唯一的� = Python 的 user-site**(2026-09-11 实测):
`pip install --user` 一旦创建 `$HOME/.local/lib/python3.12/site-packages`,它就会进入 `sys.path`
且**排在平� `site-packages` 之�** → 用户装的��包**盖�平�包**(实测 `import packaging` 拿到用户版 26.3)。
**修法(两��制性 env,官方机制�互�冲�,已注入 `baseEnv` + 放行 `ALLOWED_ENV`)**:
```
PYTHONNOUSERSITE=1 # user-site �进 sys.path
PYTHONUSERBASE=/usr/local/dsh-runtime/.no-user-install # pip --user 写�读� → 明确报错
```
- 实测:`sys.path` 中无 user-site;`pip --user` 报
`Can not perform a '--user' install. User site-packages are disabled for this Python.`ï¼›
历�污染失效(平�包��被盖);**`--target` + `PYTHONPATH` ��用**(这是给用户的推�路径)。
- ⚠� 这两�虽是"注入 env"(R5 触�项),但**方�是收窄**(�制用户侧安装)→ �直接�,档案留痕��。
**版本漂移巡检**:`scripts/runtime-baseline.cjs`(`--accept` 写基线;默认比对,漂移则退出� 1),
cron **�天 05:10** 跑;基线存 `/opt/dsh/state/runtime-baseline.json`(当� = python3 3.12.14 /
pip 26.2.1 / node 22.23.2 / npm 10.9.8)。**�级�行时�必须 `--accept` 更新基线**,�则会一直告警。
### 📦 实例共享工具(jq / ripgrep / ffmpeg,档案 46)
**核心答案:用户�工具/程�包时,`�需�`�人装一份** —— 宿主装一次全员共享。��:
实例内 `/usr` 是 **ro-bind** 且 `/usr/local/bin` 在实例 PATH **首�** → 宿主安装**全部实例(�新用户)
立���**,零代��无需��;而用户侧本�就装�上(`/usr` �读 + 无 sudo)。
- **一键安装**:`bash scripts/install-shared-tools.sh`(幂等 / 固定版本 / **官方 sha256 校验** / `--force`)
当�已装:`jq 1.8.2`�`ripgrep 15.2.0`(musl ��)�`ffmpeg`+`ffprobe`(BtbN ��构建,自带全部编解�库)。
- **新增共享工具的标准动作**:优先找**���文件**�行版 → 放 `/usr/local/dsh-runtime/bin/` +
软链到 `/usr/local/bin/` → 更新 `/usr/local/dsh-runtime/SHARED-TOOLS.md` →
跑 `node scripts/runtime-baseline.cjs --accept` 刷新版本基线。
- **�移�元**:整个 **`/usr/local/dsh-runtime/`** 一个目录(Python + 这些工具都在里�)→
`tar czf dsh-runtime.tar.gz -C /usr/local dsh-runtime`,目标机解压回原� + �跑两个安装脚本。
- ⚠� **版本���别用通用规则**:ffmpeg 的版本形如 `N-126492-gefb0a7e5e7`(**�� `x.y`**),
基线脚本对它�独用 `ffmpeg version (\S+)`。
- ⚠� **Playwright �未�**:它�是�文件,除�览器二进制外缺 **11 个系统 `.so`**(`libnss3`/`libgbm`/
`libatk`/`libcups`/`libdrm`/`libxkbcommon`…)。系统库� `dnf install`(宿主装一次�样全员��);
�览器二进制建议共享� + `PLAYWRIGHT_BROWSERS_PATH`,但**注入该 env 属 R5「给实例注入新 env�= 扩大**,
须先出「��影�评估�并�得确认。
- 🖥� **admin �视化**:门户 **`#/runtime`「�行环境�**页(档案 47)+ `GET /api/admin/runtime`
(admin �读)—— 列�称/版本/**��**/**安装脚本**/���载 + **版本漂移告警**(对比基线)+
目录/体积/最近�更 + 折�的「�级 / �载 / �移怎么��。
**术语澄清**:「宿主�= **�务器本身**(�是�个用户);安装由**平�以 root** 执行,用户侧装�进 `/usr`。
- pip �点:默认装 runtime 的 site-packages(**�读被挡**);**推� `--target <ws>/.pylibs` + `PYTHONPATH`**(实测�用)。
- ⚠� 宿主 `/usr/local/bin/python3` 现在是 3.12 → 已核实宿主**无任何东西�赖裸 `python3`**
(dnf/yum �对路径;BT-Panel 用自带 pyenv �对路径)。
- ⚠� **平�自身 bug(待治本)**:`business-plugins.ts` / `ensure-biz-plugins.cjs` 以 **root** 跑 `pnpm`
且 `HOME=<userRoot>/ws` → 在用户工作区留下 **root 属主的 `.local/`** → 用户 `pip install --user`
报 `Permission denied`(自己的目录里装�了包)。**�能简�改 HOME**(�与实例共用 pnpm store,
�则 `ERR_PNPM_UNEXPECTED_STORE`)→ 应在 pnpm �程�把 `ws` 下的 root 属主项 chown 回用户,
或放进 `ws-cleanup.cjs` �自愈。
- � **本环境出网走 TLS 中间人代� → Python 脚本必须显�指定 CA**(2026-09-11 guest 会�实�)。
�务器上有**两份 CA bundle**:
- `/etc/pki/tls/certs/ca-bundle.crt` —— **�代� CA**(**curl 默认读这份**,所以 curl 一直正常)
- `/etc/ssl/certs/ca-bundle.crt` —— 系统原始,**��代� CA**(**Python 默认读这份**)
→ 在实例里用 Python 抓 HTTPS 会报 `URLError(SSLCertVerificationError: …)` 或
`unable to get local issuer certificate`。**正解(零平�改动,就是 A 方案)**:
```bash
SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt python3 your_script.py
```
```python
# 或写进脚本(推�,��次手敲)
import os; os.environ.setdefault("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt")
# requests 亦�用 REQUESTS_CA_BUNDLE 指��一文件
```
**写技能/脚本时把这一�固化进去** —— 这是**环境特性,�是用户错误**;�固化就会��踩
"�个工具莫�连�上网"。**��**改�平�注入 env:那属 **R5「给实例注入新 env�= 扩大**,
而一行代�就能解决。
(相关:`/etc/ssl` 已加入实例 `/etc` 白��,�则连系统 CA 都读�到 —— 档案 49 事�修正 2。)
**跨用户共享本地安装 → 一律走宿主**:
| 方� | 改动 | 评价 |
|---|---|---|
| **宿主 `/usr/local` 或 `/usr/bin`**(推�)| **零代�**,`/usr` 已 ro-bind | 装一次全员生效�新用户自动。全局�版本 |
| `<dataRoot>/shared-*` + `--ro-bind` + env 白�� | 改 `orchestrator.ts` + `spawn.ts` ALLOWED_ENV | 与 bundled-skills �构,**必须先修 bind** |
| 让用户自己 `pip/npm install` | — | � 用户侧**装�上**(`/usr` �读 + 无 sudo),装上了也是 N 份��且污染工作区 |
### 资�与网络边界(改默认值/�容�规划�必看)
- **宿主 1870MB / 2 vCPU / 40G �分区,`quotaon /` 未�用 = 无�盘��** → �租户�写满打瘫全平�。
- �实例内存 **`MemoryHigh=448M` → `MemoryMax=1024M`**(**与�件集�无关**;�档案 96)+ `CPUQuota=150%` + `TasksMax=128` → **并�实例上�约 2–3 个**(宿主 1870MB)。
- **bwrap � `--unshare-pid`,� `--unshare-net`** → 实例与宿主**共享网络命�空间**。**已在档案 39 缓解**:nft **H5** ��实例**主动**访问 `127.0.0.0/8` + eth0 + docker0 + 公网 EIP(写法与��上「H5 红线�)。⚠� 残留:实例�� `bind 0.0.0.0`(入站方�未�制)。
- 出网护� `table ip dsh_egress`(`/etc/nftables-dsh-egress.nft`,一键�现 `scripts/install-egress-guard.sh`):**H1** `reject` 云元数� `100.100.100.200`;**H5** ��实例主动访问宿主自身;其余新建外�� `log prefix "dsh-egress" level info` **�拦截**。DNS = 阿里云内网 `100.100.2.136/138`(**���� `100.100.0.0/16`**)。
- `HOME=<userRoot>/ws`(�是 `home/`)→ pip/npm 缓存全�在**用户工作区**里(`ws/.npm`�`ws/.cache/pip`),会被门户 `#/files` 展示�也�能被 ws-cleanup 触碰。
### �读诊断�方(�改用户目录��起实例)
```bash
B=/usr/bin/bwrap; R=/var/lib/dshs/users/<UID>
# 把 diag 脚本 ro-bind 进命�空间,��往用户 tmp 写文件
$B --ro-bind /usr /usr --ro-bind /lib64 /lib64 \
--symlink usr/bin /bin --symlink usr/sbin /sbin --symlink usr/lib /lib \
--tmpfs /etc --dev /dev --proc /proc \
--bind $R/tmp /tmp --bind $R $R --ro-bind /tmp/diag.sh /tmp/diag.sh \
--unshare-pid --chdir $R/ws \
-- setpriv --reuid <uid> --regid <uid> --clear-groups /bin/sh /tmp/diag.sh
# �� bwrap �数��读��猜:直接看失败/�行中的 scope
systemctl list-units "dsh-*" --no-pager --all | grep scope
```
> ⚠� **`pgrep -f "dsh --profile web"` 会匹�到你自己的命令行**(命令串里�该模�)→ 拿到� PID。改用 `ps -eo pid,user,cmd | grep "^ *[0-9]\+ .*node /usr/local/bin/dsh"` 或直接读 `systemctl list-units "dsh-*"`。
### 📋 实例�访问路径清�(��版 · 2026-09-11 档案 39 收窄�)
**�有一�是"该用户的数�",其余都是"公共软件"或"平�自己"——写文档/答用户时按此表�径。**
| 能读到 | �� | 说明 |
|---|---|---|
| `<dataRoot>/users/<该用户id>/` | **读写** | **唯一属于该用户的数�**:`home/`(= `DSH_HOME`:profiles/sessions/settings/storages/`skills/`(**已�用**)/ `skills-library/`(**已�用的自建技能**,档案 41)/ `.credentials.yaml`)�`ws/`(= `HOME`,默认工作区,pip/npm 缓存也在这)�`tmp/`(该用户的 `/tmp`,1777)�`patches/*.yml`�`handoff.json` |
| `/usr` | �读 | 公共软件层(node/dsh/npm/pnpm + `/usr/bin` 1151 个 CLI + `/usr/lib64`)。**审计�实零凭��零用户数�** |
| `/lib64` | �读 | 系统共享库(node 动��赖 7 个 `.so`) |
| `/etc`(**白�� 15 项**) | �读 | `ld.so.cache` `ld.so.conf` `passwd` `group` `nsswitch.conf` `hosts` `resolv.conf` `host.conf` `services` `localtime` `os-release` `machine-id` `pki/tls/certs` `pki/ca-trust` **`alternatives`**;**其余 `/etc` = 空 tmpfs(�写�临时�退出�消)** |
| `/dev`�`/proc` | — | �行必需;`--unshare-pid` → `/proc` ��本命�空间进程 |
| `/bin` `/sbin` `/lib` | 符�链接 | → `usr/bin` `usr/sbin` `usr/lib`(补�标准布局,��沙箱探针 execvp 失败) |
| `<dataRoot>/bundled-skills/` | **�读** | 平�共享技能层(档案 40 起已挂载)。**内容对�个登录用户�读** → �止放内部文档/�有�示�/凭� |
| 读�到 | �� |
|---|---|
| `<dataRoot>/users/<其他用户id>/` | bwrap 未挂载 + uid 隔离(实测�列自己那 1 项) |
| `<dataRoot>/dshs.db` | 未挂载(实测 No such file) |
| `/etc` 白��外**全部**(systemd �元 / nft 护� / cron / letsencrypt / ssh / firewalld / selinux / 576 个文件) | 档案 39 `/etc` 白�� |
| `/root`�`/home/*`�`/opt`�`/var`(自己路径除外)�宿主 `/tmp` | bwrap 未挂载 |
| 宿主 `127.0.0.1:*`�eth0�docker0�公网 EIP | nft **H5**(实测 ECONNREFUSED) |
| 云元数� `100.100.100.200` | nft **H1** |
**�然�达(已知残留)**:公网(pip/npm/API/LLM)�阿里云内网 DNS `100.100.2.136/138`�
`bind 0.0.0.0`(**入站方�未�制**,跨租户互通� H5 的��侧拦截)��实例命�空间内 `setpriv` �的自有进程。
**技能的投放通�(2026-09-11 实测)**:
| 通� | 路径 | ��改 | 备注 |
|---|---|---|---|
| preset 自带 | `<dsh>/node_modules/@deepseek-ai/dsh-agent-presets/presets/<preset>/skills/` | **��** | dsh �内置 **2 个**(`cordis-plugin-development`�`editing-cordis-compositions`,都在 `cordis` preset 里)。**改它 = 改官方包(R2 红线)** |
| bundled 共享层 | `$DSH_BUNDLED_SKILL_DIR`(rank **600**,全员�读) | 平� | ⚠� **当�断的**:�下 |
| 用户层 | `$DSH_HOME/skills`(rank **400**,�用户独立,watch �时生效) | 用户 | 平�"用户级��"应走这层 |
> ✅ **bundled 层已修�(档案 40,commit `37e016f`)**:bwrap args 在 **`--bind root root` 之�**追加
> `'--ro-bind-try', bundledSkillDir, bundledSkillDir`(仅当 `config.bundledSkillDir !== ''`)。
> 实测:实例内 `ls $DSH_BUNDLED_SKILL_DIR` ���`readFileSync(SKILL.md)` = OK(= �现�件�立)�
> `mountinfo` 为精确�� `ro,nosuid,nodev`�`touch` = Read-only;**���未扩大**(`ls users/` � 1 项�
> DB ��读�`/etc` � 13)。⚠� 此�"�注入 env 就以为�好了"是错的 —— **skill-filesystem 在实例内读盘,
> env �决定"去哪儿找",目录�挂载就是找�到**(通用判�:�件在实例内 `resolve()` + 读盘的东西,
> 必须真的出现在命�空间里)。
> ⚠� **宿主 `<dataRoot>/bundled-skills` 目��为空** —— 机制通了,但**尚未投放任何技能**。
> 🔬 **修法安全性已实测(2026-09-11,guest uid 100002 实跑)**:加该行� `mountinfo` = `…/bundled-skills → �� ro,nosuid,nodev`(**精确��路径 + �读**);`ls …/users/` **�列用户自己那一个**(看�到其他用户);读 DB = **No such file**;`touch` 挂载点 = **Read-only file system**。**对照组(�加)= `/var/lib/dshs/` 里�有 `users`** → 支��零�化,且**父目录本�就是 bwrap 为用户 root ��的**(今天线上已��该路径�,`users/` 是��自己的空壳),**�是这一行新暴露的**。
> ⛔ **红线:�能挂最内层路径**。写� `--ro-bind /var/lib/dshs /var/lib/dshs`(父目录)= **一次性把全部用户 home + DB + 凭�放进�个实例**。这是本改动唯一的真实风险,且属手滑型错误 —— 改完必须**�字�核 bwrap args**,并跑一次上�的���实测。
> ⚠� **副作用(设计�图,但�说清)**:挂上� `bundled-skills/` 里的内容**对�个登录用户�读** → 该目录**�止放内部文档 / �有�示� / 凭�**;投放清�必须按"全员��"审一�。
> ⚠� **dsh 技能没有"�用"机制**(`skill-filesystem` 无 disable/deny)→ ranked 600 的共享层**无法 per-user 关闭**,�适�"人人必须有的基线技能";�让用户���,必须用 rank 400 的用户层(�制进/删掉)。
**平�侧 API ↔ 技能层 的映射(2026-09-11 核实 `src/web/routes/skills.ts`;档案 41 �已扩充)**:
| API | 鉴� | �点 | 层 | 说明 |
|---|---|---|---|---|
| `GET/POST/DELETE /api/skills/shared`(+`/apply`) | **`requireAdmin`** | `<dataRoot>/bundled-skills` | bundled **600** 共享�读 | ✅ 档案 40 起已挂载��现;对用户 **`locked:true`(���用/删除)** |
| `GET/POST /api/skills/mine`(+`/apply`) | `requireAuth`(任�登录用户) | `<userRoot>/home/skills` | 用户 **400** 独立 | ✅ 用户自建技能;`GET` �项带 `source`/`enabled`/`locked` |
| `POST /api/skills/mine/:name/enable` \| `/disable` | `requireAuth` | �用 ↔ `<userRoot>/home/skills-library/` | 用户 **400** | **dsh 无 disable 机制** → 「�用�= `rename` 出扫�根(**�留文件**,���用)。watch �时生效,**无需��** |
| `DELETE /api/skills/mine/:name` | `requireAuth` | 两处�删 | — | 彻底移除用户自建技能 |
| ��守� | — | — | — | 与共享技能�� → 上传/�用/�用/删除**一律 409**(�则用户白传一份永远被 rank 600 压���关�掉的技能) |
> ⚠� **投放�必须平铺 —— `discoverRoot` �扫 1 层**:带 `subskills/` 的技能(实� `短视频工作�/subskills/{mcn-data-insight,mcn-dou-analysis,…}`)**�技能�会被�现**。投放时把 `subskills/*` ��为顶层,或�层�投一个技能。`references/`�`scripts/` 是技能内部相对路径引用,**��影��别动**。
> ⚠� **`scripts/` 的�用性由"宿主基线"决定**(技能脚本� bash 执行 → ��读 `/usr` 约�):宿主缺 `python3.9+`/`ffmpeg`/`rg`/`jq`/�览器时,脚本写出�也跑�动。**投放�先核对宿主基线**,别在 SKILL.md 里写"请先装 X"(用户侧装�上)。
> ⚠� **安全:分清"�在执行"**(档案 41 定稿)——
> · **�行**技能 `scripts/`:走 agent 的 bash 管线,困在用户沙箱里(uid+bwrap+cgroup+`/etc` 白��+H5)→ **自伤,�接�**,**�该以"用户能跑任�代�"为由�止**;
> · **上传/解压**技能包:**平�以 root 执行** → **这�是平�级风险点**(�下「技能上传安全�)。
> 注�档案 33 �默认 `danger-full-access` + `approval: never` → 技能脚本**��弹确认**。
### 技能上传安全(档案 41 · 该类�洞的通用判�)
**P0 实测�现的根因:`zip` 的�员�以是符�链接,而 `unzip` 默认原样��它。**
symlink 的**目标写在 zip 元数�里��在任何文件内容中** → `scanDir`(�读文件内容,且� `!st.isFile()` 直接 `continue`)**天然看�到**;�员�校验(拒�对路径 / `..`)也**放行**。
�� `applyStaged` 以 **root** 执行 `chownTree`/`chmodTree`,二者**跟�**符�链接 → 改写**链接目标**(宿主任�文件)的属主与��。实测 `-rw------- root:root` → **`-rw-r--r-- <租户uid>`**;指� `/etc/shadow` = 密�哈希 chmod 644 全机�读 + chown 给该租户。
攻击� = **传了 `owner` 的那�路由**(`mine`;`shared` �传 owner,��影�)。
**必须�时覆盖两个维度**:① **文件内容**(现有 `BLOCK_PATTERNS` P0 阻断 / `WARN_RULES` P1 告警 —— `scanDir` 内部**会 throw 400**,�是�告警);② **归档元数�与文件类型**(symlink / 硬链 / 设备 / FIFO + 解压规模)。
**三项加固(已�地)**:
1. `findSpecialEntry()` —— 解压�**立刻**拒�任何�普通文件/目录�目(在任何 `chown/chmod` 之�);
2. `sumUncompressedSize()`(解� `unzip -l`)+ `MAX_SKILL_FILES=2000` + `MAX_SKILL_UNCOMPRESSED_BYTES=200MB`
—— `UPLOAD_BODY_LIMIT=180MB` ��**压缩体**,而宿主 `quotaon /` 未�用 = **无�盘��** → zip bomb �跨租户 DoS;
3. 纵深防御:`chownTree` 用 **`lchownSync`** 且跳过 symlink;`chmodTree` 跳过 symlink(**Linux 无 `fs.lchmod`**,ENOSYS)。
> 更彻底(未�):以目标 uid **��解压**(`setpriv --reuid <uid> ... unzip`),让整�链路都�在 root 下。
**技能的�赖:没有机制(最大缺�)**。`dsh-skill-filesystem` **�处�任何 install/deps/package.json** —— 技能包�能� `SKILL.md` 写"请先装 X",由 agent �行时手敲。对比�件:`package.json.dependencies` + `pnpm add` 自动解�(store 硬链�用)。约�:实例内 `/usr` **�读** → 系统级装�上;�能 `--user`/本地;无共享��用户�装一份。
→ **平�若�多用户投放带�赖的技能,必须在�用时"代装"**:Node �赖装到该技能目录(或用户 ws 下统一 `node_modules` + `NODE_PATH`);Python �赖用**用户级 venv**(`$DSH_HOME/.venvs/<skill>`)并让脚本用该解释器。**��让 agent 临时 pip/npm install**(�读 `/usr` + 供应链 + ���现)。
## 技能 vs �件:怎么区分(2026-09-11 更正,别用"有没有代�/界�"判)
**先纠正一个常�误解**:技能**�以带 `scripts/`**(实�:`短视频工作�/scripts/MCN_CYLG_API.py`�`mcp-config.json`)。所以「技能=纯内容无代��「�带界�的�件就是技能�**都��立**。
**正确判� = 「代�何时�被�执行�**:
| | 技能(skills) | dsh �件(plugins) |
|---|---|---|
| 包形� | 目录 + `SKILL.md`(+ `scripts/`�`references/`) | npm 包 + **`dsh.bundle.patch`**(+ �选 `client.js`) |
| **代�执行者** | **agent 通过 bash 工具按需调用**(SKILL.md 指示 → 模型决定) | **cordis 在实例�动时加载执行**(无人决定) |
| 是��审批/沙箱 | ✅ 走统一工具管线(approval + 沙箱 + uid/cgroup) | � **��**,它自己就是实例进程的一部分 |
| �与框架生命周期 | �(纯文件) | 是(注册�务/工具/UI,进 `profile.bundles`) |
| 装载时机 | �行时按需�现,**watch �时生效** | **�动时**,改�必须�� |
| 失败�果 | 脚本报错 / agent 表现�佳 | **实例起��(崩溃循环)** |
| �明度 | 命令进会�记录,�追溯 | �默�行 |
**�编程判�(我们代�里已在用)**:看包里有没有 **`dsh.bundle`**(`isBundle()` 判 `dsh.bundle.patch`)→ 有=�件,无=技能。**有无 client �(UI)�是�件的�选属性**,与"是�是技能"无关。
**风险分层(�由�准)**:技能低风险�三�——**惰性**(�调用�执行)+ **��**(命令在 transcript)+ **失败�致命**;�件高风险的核心�由是 **开机�执行�无人介入�失败致命**。
> ⚠� 注�:档案 33 把默认档改� `danger-full-access` + `approval: never` �,**技能脚本也��弹确认**(��沙箱/uid/cgroup 约�),两�路线的"人工拦截�"差�因此缩�——分层的主���从"审批"转为"是�被框架主动加载 + 失败是�致命"。
## 术语与环境约定(2026-09-11 档案 38b)
- **术语:统一�「功能�件�**(2026-09-11 起由「业务�件�改�)。**�改显示文案/注释,代�标识符����**:表� `business_plugins`�包� `@dsh-local/business-plugins`�section id `business-plugins`�API 路径 `/api/plugins/{business,mine}`�文件� `business-plugins.ts` / `ensure-biz-plugins.cjs`�目录 `poc/business-plugins/`。
- **`ensure-biz-plugins.cjs` 已是「版本感知 + 自动�最新产物�**:�级�程 = 把新包丢进 `/opt/dsh/artifacts/` → 跑一次脚本(��会自动 `pnpm add`)。**�级时��先删旧包**,�则�赖表里的旧 `file:` 指�失效文件会让 `pnpm` 整体 ENOENT 失败(正确顺�:先放新包 + 改�赖指� → `pnpm add` → �删旧包)。
- **`.gitignore` 已忽略 `*.bak-*`**:`git add <dir>` 会把�目录的未跟踪备份一并暂存(已踩一次)。�交�用 `git status --short` 核对,或定� `git add <具体文件>`。
- **�务器 Python 是 3.6.8,��动系统 `python3`**:`/usr/libexec/platform-python` 是 `dnf`/`yum` 的 shebang,替�/�级会直接��包管�器。**�维脚本一律用 Node**(平�技术栈就是 Node);必须用 Python 时写 3.6 兼容代�(无 `subprocess.run(capture_output=)`�无 f-string `=` 调试等 3.7+ 特性)。确需现代 Python 就**并行装 `python3.11`**(仓库有),别切 alternatives。
## 功能�件�用:探活 · 快照回滚 · ��件隔离(档案 34/35,2026-09-11)
**原缺陷**:`/api/plugins/mine/apply` 在 `pnpm add → reconcile → restartMain` 之�**直接标 success,全程无探活** → �件�崩实例(崩溃循环)时任务�报「已应用�。
**正确�程**:快照 → �用项直接 remove(�引入新代�,永远安全)→ �用项**先整批试一次** → 探活 → 失败�回滚 + **��件隔离**(能起�的�留�起��的�独摘掉并记 `plugin_incident` 审计)。
**探活的两个必踩�(都�实测�现)**:
1. **�能�判 `restartMain` 的返回值** —— 编排器里没有该用户实例时(portal ��清了内存��或实例被空闲回收)它返回 `undefined`,会把**无辜�件**误判为「�兼容�。→ 无实例时先按 `/api/dsh/enter` �路径 `launch` 一个�探。
2. **固定短等待会把好�件判死** —— �版「固定 2.5s 稳定窗��实测好�件也报「未产出 launch token�。→ 改为**轮询**(500ms 间隔 / 总预算 60s):拿到 `launchToken` 且 `status==='running'` �,�过 2.5s 确认没闪崩�算通过。
3. 探活失败�**回传 dsh 的真实错误**(如 `duplicate loader entry id` / `ERR_MODULE_NOT_FOUND`),��泛化 —— 这是 admin 判断"这个�件为什么�"的唯一线索。
**�:平� bundle 包�能被 ws 清�删掉**(档案 35 P0)。`ws-cleanup` 的 T1 会无�件删 ws 顶层 `.tgz`,而三个平� bundle 是以 `file:<ws>/*.tgz` 安装的 → 删掉� profile 的 `dependencies` 指��存在文件 → **任何 pnpm �作 ENOENT 失败**(�用功能�件必然失败)。**且��蔽:`node_modules` 已装好,实例照常�行,�在下次 pnpm �作时爆�。** 修法:清��读�个用户**所有 profile 的 `file:` �赖**,被引用的文件一律��(别硬编�文件�)。平� bundle 的��建��在 `/opt/dsh/docs/04-调整方案/poc/`,产物存 `/opt/dsh/artifacts/`。
**造测试 fixture 的�**:bundle 的 `package.json.name` **必须与 `cordis.patch.yml` 里 `insert` 的 `name:` 一致**,�则 `ERR_MODULE_NOT_FOUND`;且打包�用 `package/` �缀的 npm-pack 布局(`tar czf x.tgz package`),平铺布局 pnpm �能�接�。
**平� bundle 的铺装(档案 36)**:`scripts/ensure-biz-plugins.cjs` —— 幂等,判定**看 `dsh.profile.bundles` �看 `dependencies`**(包内 `cordis.patch.yml` �对 bundles �员生效;有 dep 没 bundles = 等于没装)。�程:�制产物 tgz 到用户 ws(chown)→ `pnpm add file:` → 对� dsh plugin add 的 reconcile。**新用户自动铺 = 审批时 detached `spawn`(fire-and-forget)+ cron 巡检兜底。**
**�:`pnpm-workspace.yaml` 会让 pnpm 拒� `add`**。dsh 会给�些 profile 生� `pnpm-workspace.yaml`(`packages: [.]`)→ pnpm 视为 workspace root → `ERR_PNPM_ADDING_TO_ROOT`。→ **所有 `pnpm add` / `pnpm remove` 都�带 `--ignore-workspace-root-check`**(有无该文件都工作)。
**�:判断「装没装�**永远看 `dsh.profile.bundles`,��看 `dependencies` —— 两者�以�一致,而�一致时组件是**没生效**的。
**�:Node 22 的 `execFile` 类型**:`execFile(file, args, { stdio:'ignore' }, cb)` 在本项目 TS 版本报「'stdio' does not exist in type ExecFileOptions�。�� fire-and-forget 用 `spawn(..., { stdio:'ignore', detached:true }).unref()`。
## 会�记录��(分��工作区/用户的真实对�,2026-09-11 定型)
用户常�「看看 XX 工作空间下的对�记录,能�现哪些问题�。**这是�读任务,�与其他�读任务并行**,且**适�丢给�代�**(大文件长分�,别�主上下文)。
**路径规律**(`<UID>` = 用户 id,admin = `cce6d1cd-b376-4304-80f0-0e1c58c9ffde`):
```
U=/var/lib/dshs/users/<UID>
$U/home/sessions/--<cwd 路径把 / �� ->--/session-<uuid>/session.jsonl.zstd
$U/home/storages/workspace.json # 工作区注册表:id → { path, title, sessionIds }
```
例:`--var-lib-dshs-users-<UID>-ws-mcntimo--` 对应工作区 `$U/ws/mcntimo`。
**�:文件是「追加�多帧 zstd�,�是普通 zstd。**
- `zlib.zstdDecompressSync` / `createZstdDecompress` �解第一帧 → �得 240 字节的 session 头,然�报 `Unknown frame descriptor`。
- **必须用 zstd CLI**(�务器默认没装):`yum install -y zstd`(al8 官方� `zstd-1.5.1-2.0.2.al8`,安全,已获用户授�)。然� `zstd -dc <src> > /tmp/s.jsonl` —— 1.6 MB 明文正常解出。
- 用户点过 `/export`(`command/done` → `Session log download requested.`)拿到的是**这�多帧 zstd**,**用户根本打�开** —— 属平�缺陷(档案 36 P1-6)。
**分�脚本写法(2026-09-11 踩��定型)**
```bash
# � 别用:ssh 'node -e "..."' —— 内层引�转义在�引� ssh 命令里必�
# ✅ 一律:本地写 → scp → 远端执行
P=/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0
export PATH="$P/usr/bin:$P/mingw64/bin:/c/Windows/System32:/c/Windows:$PATH"
S="C:/Users/Administrator/AppData/Local/Temp/wb-scratch"
scp -i ~/.ssh/id_ed25519_dsh -P 22 "$S/an.cjs" [email protected]:/tmp/an.cjs
ssh -i ~/.ssh/id_ed25519_dsh -p 22 [email protected] 'node /tmp/an.cjs /tmp/g.jsonl users'
```
- ⚠� **`.cjs` �缀是必须的**:�务器 `/tmp/package.json` � `"type":"module"` → `/tmp/*.js` 被当 ESM,`require is not defined in ES module scope`(本次踩�)。
- 脚本按 `MODE` 分支输出(`users` / `errors` / `tools` / `assistant` / `seq`),**一次写好�用**,���� scp。
- `type` 分布里 `request/header` ���完整 system prompt + 工具表(�明文相当大比例)→ **务必按 `type` 过滤**,别全� dump。
**JSONL 结构**:�行一� JSON。第 1 行 `{"type":"session", cwd, agentPreset, ...}`;其余 `type` ∈ `request/header`(�完整 system prompt + 工具表)�`assistant/chunk`�`assistant/message`(� `reasoning` + `tool-call`)�`tool/result`�`turn/end` 等。工具结果里能直接看到真报错原文。
**分��点**(产出「问题清��,P0/P1/P2 + 行� + ≤60 字原文片段):
① 工具报错/�试/超时/��被拒(**沙箱�制是高频根因**)② 用户挫败信�(��纠正����求�放弃)③ **平�侧缺陷**(��无�应�报错��读�路径写死�工作区为空�会�中断)④ �点与无谓的工具调用 ⑤ **安全**:凭�明文(�报「第 N 行疑似凭�,类型 X�,**��抄值**)�越��试。
**�外�用信�**:报错在会�中的**�置分布**(`grep -n`)能区分「已修�还是「一直在�生�。
**必须先查「是�是已知问题��报(2026-09-11 定型)**:动手�先 `ls /opt/dsh/docs/04-调整方案/` + 读�类旧档案(如 21 �顿 / 23 环境�制 / 32 上次��),**新�现�明确标注「新增�还是「修正/补充旧档案�**,�则会把已归档的结论当新问题��报。
**产出�的闭环(别�输出清�)**:① � `04-调整方案/<NN>-<主题>.md`(�「修正旧档案的哪�结论��节);② `INDEX.md` §二 追加 `04-<NN>` 行并更新状�摘�(**README 的档案清�已定格为历�对照,别�改 README**);③ `INDEX.md §六.1` 的「下一��+1�`03-路线图与待办.md` 补登记;④ 收尾四件套:`python scripts/docs-audit.py`(**退出� 0**)→ `python scripts/docs-manifest.py`(刷新机读清�)→ `bash scripts/docs-sync-check.sh`(全绿)→ `MINE="<我改的文件>" PUSH=1 bash scripts/handoff-guard.sh`(幽�文件硬判定)。
## 多任务并行调度�议(2026-09-11 用户�出,长期沿用)
**核心结论**:**�以并行,但并行度由「冲�域�决定,�由任务数�决定。** �读/调研类�自由并行;改� → 构建 → �� → �交 → 写文档是**�通�**,必须串行。
### 冲�域清�(�域必串行,跨域��并行)
| 冲�域 | 涉�资� | 并行性 |
|---|---|---|
| 门户�端代� | `/opt/dshs/src/**` → `npm run build` 全��编译 `lib/` | � 独�:一批��许 1 个改�任务 |
| �务�� | `systemctl restart dshs` | � 全局;��会打断所有用户实例 |
| �端页� | `web/*.html`(�文件�写覆盖�写;marker-splice 会互相清�) | � �文件串行 |
| 代�仓库 | `git add` / `git commit`(index.lock) | � 串行 |
| DB schema | `dshs.db` 建表/�移 | � 串行(普通读写事务短,�并行) |
| 文档 | `/opt/dsh/docs/**`�`docs-status/**`(�务器唯一�,�点文件) | � 串行(�写覆盖) |
| 本地记忆 | `.workbuddy/memory/*.md`�`MEMORY.md` | � 串行(并� append 会丢更新) |
| 实例�行� | �用户 profile / 实例进程 | ⚠� **跨用户�并行**;�用户串行 |
| 临时测试会� | `sessions` 表 | ✅ �并行,但 UA 必须带 worker 标识(`poc-<worker>`),清��删自己的�缀 |
| �读调研 | 读���看日志�`--dump-config`�外网抓��兼容性测试 | ✅ 完全�并行 |
### 批次模型与�赖处�
1. **一批 = 1 个「改�通��+ N 个「�读通��**;改�任务之间永远排队串行。
2. 待办先建� **DAG**,��标注「�赖�+「冲�域�;**无�赖且冲�域��**��许�批。
3. **收�在主会�**:并行通�产出汇总回主会�,统一写档案 → commit → `docs-status-sync.sh --pull`(这三步本身也是串行�通�)。
4. **用户侧多会�按「领域�切分**(A=门户�端 / B=文档梳� / C=兼容性测试),**��按「�一领域的��切片�切** —— �者必然�域冲�。
5. **开跑�先出「并行批次表�**(任务 / 冲�域 / �赖 / ��并行)给用户确认。
6. 我侧�用并行�代�(`Agent` + `run_in_background`)跑**�读通�**;**写�作必须回到主会�串行执行**。
### �例(�域并�的具体破�形�)
- 两会��时 `npm run build` → `lib/` 产物交�污染,`systemctl restart` 互相打断实例
- 两会��时改 `portal.html` → �写覆盖�写(marker 区�被对方整段替�)
- 并� `git add/commit` → `index.lock` 冲�
- �时写 `/opt/dsh/docs/**` 或 `MEMORY.md` → �写覆盖 / 丢更新
- 用�一 UA �缀批�删测试会� → 误删�一个 worker 的会�
### �地机制(2026-09-12 建立):三把� —— 别�记忆,�判�
冲�域判断是「设计�,还需�「执行时的强制判��。当日 3 次实�事故�补上三把�:
| � | �置 | 管什么 | 拿法 |
|---|---|---|---|
| **全局执行�**(粗) | `交接�/.exec-lock` | �一时刻��许**一个执行会�**动「文档 / 代� / �务器� | `bash scripts/handoff-guard.sh --claim-exec "<会��>"` |
| **�级�用�**(细) | `交接�/.doing-<��>` | 这个�归��(供�账与接管) | `bash scripts/handoff-guard.sh --claim <��> "<会��>"` |
| **�务器侧�作�** | `/opt/dsh/state/.op-lock/<�作�>`(跨机��) | �正在动**生产**:�� / drain / 改实例 env·quota / 批�铺�件 / 改 nginx·nft·�书 | `bash scripts/op-lock.sh claim <�作�> "<影��:�会被断�断多久>"` |
- **顺�**:开工 **先抢全局� → ���务器�**;完工 **��**释放(先放�务器�,最�放全局�)。
- 🔒 **�的生命周期 = 任务的生命周期**(2026-09-14 用户明令):**抢到�的任务,�有"执行完� → ��释放"�算完�**;⛔ **�止"抢��一���解�就结�回�/会�"**(�是独�资� + 本库无心跳机制 ⇒ 别人既等�到也判�出你死没死)。�套:① 抢�**�**先列出**收�步骤**(�地 → 校验 → 推�/对账 → 收尾);② 中途��(等用户�� / 等窗�)⇒ **先 `--release-exec` ��**;③ **结�语必须对�状�负责** —— 写明"已释放",或**显�点�**"��在 `<OWNER>` + 原因 + 下一步"(仅�释放通���用);⛔"忘了 / ��完就走"��许。
- **一�命令预检**:`ME="<会��>" MINE="<我�改的文件>" bash scripts/handoff-guard.sh [��]`;推��加 `PUSH=1`(�用「幽�文件�硬判定:对账结果里出现未声明的「仅本地�文件 → 直接失败)。
- **`mkdir` �原�**:��失败 = 有会�正在动 → **�手**,别�试�别"抢一下看看"。
- **�务器侧��带身份**:`ME="<会��>" bash scripts/op-lock.sh claim <�作�> "<影��>"` —— �传 `ME` 会记� `unknown-session`(2026-09-12 实测踩到两个�果:handoff-guard 的�1d】把**你自己的�**当�别人的;`release` 的归属校验也会拒你)。`release` **�能由�用者本人**执行,冒充别人 → 拒�并�示 R9。
- ⛔ **�得人工删���得接管**(**R9**,2026-09-12 用户明令「严格�止这类�作�):**AI 一律�得** `rm -rf 交接�/.exec-lock`��得删 `交接�/.doing-*`,也�得以「�有者疑似已死 / �� / 太久没动 / �在�读分�没产出�为由**�方�接管**。�**�能由�有者自己释放**(`--release-exec` / `--release`);**抢�到� ⇒ �手 + 报告用户**,**�的处置��属于用户本人**(�撤也�能用户自己动手)。`handoff-guard.sh` 输出里的「人工删� / 接管�字样**��构�授�**(该文案已�步作废)。�由:平�**无心跳机制**,AI **没有任何判�**能确认对方已死 —— 删� = 在无法验�的��下�方�撤销互斥,一旦对方�在跑,就退回「两个会��时改�一批文件�。
- **为什么�务器�必须�独加�**:文档冲�� `git status` / mtime 还能看出�,**�务器��更看�出�**(`systemctl` �会告诉你 10 分钟����过)。
- 判定与工具细节� `交接�/README.md §一 §三 §六`;机制记录� `04-调整方案/69-并�治��地-commit常�化与�务器侧�.md`。