Files
dsh_ai1net_server/dsh-server-docs/08-skills/dsh-change-workflow/SKILL.md
T
admin e6207aa691
build / build-and-scan (push) Canceled after 0s
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

320 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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: 1.0.0
updated_at: 2026-09-15
last_change: 【2026-09-22 按要求统一版本号】frontmatter `version` → `1.0.0`(原 v2.9.2-playwright-banned);正文与历史中的版本号为当时记录,未改动。此前 【2026-09-12 新增「三把锁」落地机制 + 更正过期指向】把「多任务并行调度协议」从**设计**补成**可强制执行的判据** —— 全局执行锁 `05-交接单/.exec-lock`(同一时刻只允许一个执行会话)+ 单级占用锁 `.doing-<单号>` + **服务器侧操作锁 `/opt/dsh/state/.op-lock/`**(管生产态:重启 / drain / 改实例 env·quota / 批量铺插件 / 改 nginx·nft·证书,因「服务器态变更看不出来」故必须显式加锁);工装 `07-scripts/handoff-guard.sh`(新增【1d】分支)与 `07-scripts/op-lock.sh`。同时**更正产出闭环里的过期指向**:原文写 `docs-status/文档库状态备注.md` + `bash 07-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:唯一缝隙=Python user-site 会盖平台包;修法 PYTHONNOUSERSITE + PYTHONUSERBASE 双保险;版本漂移巡检 07-scripts/runtime-baseline.cjs + cron 05:10);补「3.6 不开放」的正解(引导优先,遮蔽不可行)+ 属主自愈(ws-cleanup --reclaim-only,平台 root 跑 pnpm 污染用户 ws);**R5 已立**。此前新增红线 R5「权限可见面只准收窄,扩大必须先确认」**(含「R5 权限扩大门禁」整节:扩大/收窄方向判定表、权限影响评估四问、触发文件清单、以「实例可访问路径清单」做 diff 验收);阶段 2 红线自查与影响评估同步加入 R5 门禁;此前新增「**无法遮蔽 /usr 内文件**」(ro-bind 目录无法创建挂载点 → 叠加遮蔽会让实例起不来;判据「只有先 tmpfs 再白名单的目录才可自由增删」+ 改 bwrap 参数后必须真启动实例验证)+「**实例共享 Python 运行时**」(可移植 3.12 装到 /usr/local/dsh-runtime,迁移打包单元,pip 落点与护栏)+「平台 root 污染用户工作区」待治本项;【档案 39 事后修正】`/etc` 白名单补 **`/etc/alternatives`** —— 它是「软链枢纽目录」,漏掉后 `/usr/bin/python3` 两跳软链断裂,**21 个命令静默 command not found**(python3/pip3/ld…);新增判定准则「必须枚举整条链条会穿过 /etc 的条目」+ 探测器命令 + 「白名单类改动必须跑工具清单前后对比」;此前新增「**技能上传安全**」节(zip symlink 是**元数据**攻击面 → scanDir 内容扫描天然看不到;必须同时覆盖「内容」与「元数据/文件类型」两维度;三项加固);API 表扩到 enable/disable/delete 与 `locked` 语义;安全判据改为「**分清谁在执行**:运行=用户沙箱=自伤 / 上传解压=root=平台风险」;**撤回**我上一轮「scanDir 只告警不拦截」的错判(它内部会对 P0 throw 400);此前更正「bundled 层已修复(档案 40,commit 37e016f)」+ 实例可访问路径清单补 `bundled-skills` 行 + 判据「实例内 resolve+读盘的资源必须真的挂进命名空间(只注入 env 无效)」;此前新增「**实例可访问路径清单(权威版)**」表(能读/读不到/残留三类,写文档与答用户按此口径)+「`/etc` 白名单」(14 项最小集 + symlink 须 realpath 绑目标 + 222→13/576→27 实测)+「**H5 红线:OUTPUT 链按目的地址封本机服务=自伤**」(症状指纹 timeout vs refused、必须只匹配主动发起 TCP 纯 SYN / UDP ct state new、验证闭环三条);bwrap 参数照抄清单与只读诊断配方同步改为 `--tmpfs /etc`;出网护栏补 H1/H5 与一键复现脚本;技能投放通道补 API↔层映射表·平铺要求·宿主基线约束
agent_created: true
---
# dsh-change-workflow — dsh 平台改造工作流
dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改造的标准处理方法。把平台运维历史中反复验证过的处理方式固化为可执行流程:**先识别边界 → 源码级调研 → 方案对比确认 → 小步开发 → 全链路验证 → 归档清理**。
> 📂 **平台速查表(域名 / 生效路径 / 端口 / 存量事实)已下沉** → `references/00-平台速查.md`
> ⛔ **两条留在这里的判据**:① 正域名 = `ai1net.com`(`alotbuy.com` 是已降级的 301 旧域 —— 抓它只会拿到 301 HTML,**别误判为「改动没生效」**);② 平台页由门户直出(`127.0.0.1:3080`),生效副本只有 `/opt/dshs/web/` 一份。
## 六阶段流程
### 阶段 0 · 需求识别(先盘点再动手,勿跳步)
0. **⛔ 开工前置检查(2026-09-11 用户点名要求,血的教训)**:**这是本技能最重要的一步**。
病根 = 不先查「已经有什么」就凭直觉装工具/猜机制;出错后要用户追问,才把**早就写在文档里的说明**翻出来
(最刺眼的一次:我早在技能里写了「勿走 agent-browser」,自己却忘了,绕最远的路)。
任何「**装工具 / 写自动化 / 查机制**」之前,按序做完这三条:
1. **先看可用技能列表**(会话开始就在)→ 有对应的**立刻加载技能**,禁止另起炉灶;
2. **先看本机/项目已有资产**:`~/.workbuddy/08-skills/`、项目 `.workbuddy/08-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`)的会话来跑测试** —— 🔴 **理由已于 2026-09-20 更换(序47 落地)**:
~~"`auth.ts` 登录时 `deleteUserSessions(user.id)` 是 last-wins 单活跃会话,**会当场踢掉用户正在用的浏览器会话**"~~
—— **last-wins 已被序47 取消**(现为**多会话并存 + 每用户上限 + 显式登出全部**,见
`04-调整方案/145-…md` 与 `04-调整方案/08` 头注)⇒ ⛔ **不得再拿"会踢人"当理由**(那是本技能 R4 的旧理由,
照旧用会得出相反判断)。**现在仍然禁用的三条真实理由**:
① 借来的会话**办不了该用户自己的事**(停实例/回收/重建/看 profile 文件——见下面那条"独立账号");
② 会在真实账号的会话列表与 `audit_log` 里留下**不属于该用户的一次动作**(观测面被污染);
③ 真实账号的会话被拿去做破坏性验证(`logout-all` / `revoke`)时,**真会把用户踢下线**。
要测登录态一律用**专用测试账号**(注册 → 审批 → 用完即删)或 `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`。
⚡ **更省事的等效做法(2026-09-20 序47 实跑验证)**:直接 `INSERT INTO users (...) VALUES (...)`,
`pass_hash` 用产品自己的 `hashPassword`(`lib/web/auth.js`)现算(`scrypt$salt$hash`),`role='active'`,
`uid` 取 `max(uid)+1` ⇒ **绕开邮箱验证码与 Turnstile**(注册页两道门都开着,见档案 134)。
**凡要"操作该用户自己的实例"(停实例/回收/重建/看它的 profile 文件)→ 必须用独立账号**,
不能借 admin/guest 的 session(借了就没法停实例、也污染真实用户)。用完 `DELETE sessions + users` 并 `rm -rf users/<id>`
⚠️ **拿新版平台验证登录态时还有一个坑**:`sid` cookie 是 `Secure` + `Domain=.ai1net.com` ⇒
本机 `curl -c <jar>` **不会**为 `127.0.0.1` 存这条 cookie(jar 文件干脆不生成)⇒ 后续请求全是**假 401**。
正确做法 = **从 `set-cookie` 响应头里直接取 `sid` 值**,再用 `-b "sid=$VAL"` 带上。
- **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` 关掉自己开的标签。**
> 📂 **浏览器验证栈的调用细节已下沉** → `references/08-浏览器验证栈详解.md`(正确调用 · dsh composer(Lexical)输入难点 · agent-browser 旧实测记录 · 独立 headless 定型做法 · 三个必踩坑)
> ⛔ **留在这里的判据**:只用 `agent-browser` / `browser-harness`(**Playwright 全面禁止**);**动手前第一步先 `list_tabs()`**;**绝不附着用户日常 Chrome**。
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/07-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 定型):
| 层 | 做什么 | 能发现什么 |
|---|---|---|
| ① 单测 | `07-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` 的**实际取值逐条搬过来**,
并在注释里写明「本类名 ← 门户某类名」的对应表(便于复核)。
⛔ 反面:只引 `01-规范/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 在**实例启动时**加载,见 `01-规范/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 07-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、`08-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/调整方案
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/08-skills/<name>/SKILL.md`(技能必须本地加载),服务器归档位在 **`/opt/dsh/docs/08-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 -P 22 "$S/SKILL.md" [email protected]:/tmp/SKILL.md
ssh -i ~/.ssh/id_ed25519 -p 22 [email protected] \
'D=/opt/dsh/docs/08-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` |
| **钩子文案** | `07-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 一致 + 七件套全绿。
> 📂 **档案模板(8 段结构)已下沉** → `references/01-档案模板.md`
## 红线 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. **R4 禁止借真实账号(`admin`/`guest`)的会话跑测试**:⛔ **旧理由("登录 last-wins 会删该账号全部旧会话、把用户踢下线")自 2026-09-20 起已失效** —— 序47 已改为**多会话并存**(`04-调整方案/145-…md`)。**仍然禁止**的三条理由:① 借来的会话办不了该用户自己的实例操作 ② 污染真实账号的会话列表与 `audit_log` ③ 拿真实账号去验证 `logout-all`/`revoke` 会真把人踢下线。改用 `mksess.cjs` **PG 直插**临时 session 或**专用测试账号**(档案 15 实证教训 + 序47 复核)。
5. **权限可见面只准收窄,扩大必须先确认**(2026-09-11 用户新增,R5)——见下节「R5 权限扩大门禁」。
6. **R7|禁止未经确认的批量 / 全仓写入**(2026-09-12 用户新增红线)—— 见下节「R7 批量写入门禁」。
7. **R8|中断在线用户的生产变更须先确认**(2026-09-12 用户新增红线)—— 见下节「R8 生产变更知会」。
> 📂 **R7 / R8 / R5 三条红线的由来与完整判据已下沉** → `references/02-红线详解-R5-R7-R8.md`(**要判「算不算批量写入 / 要不要先知会 / 这次是扩大还是收窄」时必读**)
> 📂 **Profile·Skill 装载与管理面三节已下沉** → `references/03-沙箱与技能机制.md`(Profile 层 cordis patch 机制 · Skill 装载机制(bundledSkillDir)· Skill 管理面(编排器 API + 静态页))
> 📂 **通用运维锚点已下沉** → `references/04-运维锚点与取证.md`(重启用锚点 · 存量会话档位体检 · 功能插件启用:探活·快照回滚·逐插件隔离 · 会话记录取证)
## 本机 Git Bash 环境坑(2026-09-11 实证,几乎每次都踩)
- **PATH 常坏**:`Bash` 工具里 `dirname`/`head`/`ls` 报 `command not found`,且 `ssh` 不在 PATH 里(私钥真名是 `~/.ssh/id_ed25519`,⛔ 没有 `id_ed25519_dsh` 这把)。**每次先前置**:
```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`。
> 📂 **数据源与插件口径已下沉** → `references/05-插件与数据源口径.md`(外部数据源缓存必须带结构版本守卫 · 官方白名单插件来源 · 技能 vs 插件怎么区分 · 术语与环境约定)
> 📂 **沙箱与权限预设机制已下沉** → `references/03-沙箱与技能机制.md`(**改沙箱/权限默认值前必读**)
> 📂 **实例可见面与边界整章已下沉** → `references/06-实例可见面与共享边界.md`(bwrap 挂载面铁律 · 🔒 基础运行时版本冻结 · 📦 实例共享工具(jq/ripgrep/ffmpeg)· 资源与网络边界 · 只读诊断配方 · 📋 实例可访问路径清单 · 技能上传安全)
> 📂 **技能/插件区分判据与术语约定已下沉** → `references/05-插件与数据源口径.md`
> 📂 **功能插件启用与对话取证已下沉** → `references/04-运维锚点与取证.md`
## 多任务并行调度协议(2026-09-11 用户提出,长期沿用)
**核心结论**:**可以并行,但并行度由「冲突域」决定,不由任务数量决定。** 只读/调研类可自由并行;改码 → 构建 → 重启 → 提交 → 写文档是**单通道**,必须串行。
> 📂 **并行调度详解已下沉** → `references/07-并行调度详解.md`(冲突域清单 · 批次模型与依赖处理 · 反例 · 落地机制:三把锁 `--claim-exec` / `.doing-<单号>` / 服务器侧 `/opt/dsh/state/.op-lock`)
## 详情索引(references/)
> 本技能 = **主干(本文件)+ 详情档**。主干只留判据 / 流程主干 / 命令骨架;长表、案例、实测记录、历史细节在下面各档。
>
> **跨档引用怎么查**:主干与详情档正文里出现的「§N」「见 §8 坑 N」「见下表 / 见上表」等编号,**按本表的「覆盖的原章节」列定位到对应档**(下沉后原编号不再有独立章节标题)。
| 详情档 | 覆盖的原章节 | 原行段 | 行数 |
|---|---|---|---|
| `references/00-平台速查.md` | 平台速查(硬编码事实,勿猜) | L14–L92 | 79 |
| `references/01-档案模板.md` | 档案模板(04-调整方案/<NN>-<标题>.md) | L391–L403 | 13 |
| `references/02-红线详解-R5-R7-R8.md` | R7 批量写入门禁 · R8 生产变更知会 · R5 权限扩大门禁 | L414–L480 | 67 |
| `references/03-沙箱与技能机制.md` | Profile 层 cordis patch 机制 · Skill 装载机制 · Skill 管理面 · dsh 沙箱与权限预设机制 | L481–L504 + L555–L600 | 70 |
| `references/04-运维锚点与取证.md` | 通用运维锚点(重启用) · 功能插件启用:探活 · 快照回滚 · 逐插件隔离 · 会话记录取证 | L505–L525 + L935–L998 | 85 |
| `references/05-插件与数据源口径.md` | 外部数据源缓存必须带结构版本守卫 · 官方白名单插件来源 · 技能 vs 插件:怎么区分 · 术语与环境约定 | L539–L554 + L907–L934 | 44 |
| `references/06-实例可见面与共享边界.md` | 实例可见面 · 软件共享 · 网络与安全边界 · 铁律:实例能看到什么,完全由 bwrap 挂载面决定 · 🔒 基础运行时版本冻结 · 📦 实例共享工具 · 资源与网络边界 · 只读诊断配方 · 📋 实例可访问路径清单 · 技能上传安全 | L601–L906 | 306 |
| `references/07-并行调度详解.md` | 冲突域清单 · 批次模型与依赖处理 · 反例(同域并发的具体破坏形态) · 落地机制 | L1003–L1052 | 50 |
| `references/08-浏览器验证栈详解.md` | 正确调用(Windows,2026-09-11 实测可用) · ⛔ dsh composer(Lexical)输入难点 · 三个必踩坑 | L231–L293 | 63 |