# 平台改造六阶段 + 红线 R1-R11 原文速查 > **归属**:技能 `dsh-workflow` · 详情档(主干 `../SKILL.md`) > **本档覆盖**:原技能 `dsh-change-workflow` **全文**(正文 288 行 + frontmatter 变更历史) > **provenance**:文档库版(**超集**:含 R9–R11 与已修订的 R8;本机版 R1–R8 是其真子集) > **搬运方式**:**逐行未改**;内部附件链接已改写为 `dsh-change-workflow/<附件名>`(附件在本目录下) > ⚠️ 原技能 `dsh-change-workflow` **已合并退役** ⇒ 见到该名按本档读。 --- > 🔵 **作用域(2026-09-26 从 WorkBuddy 用户级技能迁入)**:本技能描述的是 **dsh 平台项目**的作业方法,已提升为 DSH 全局技能,供任何会话发现与加载。 > ⚠️ 正文里的**宿主路径与脚本位置是迁移时的旧值**(如 `~/.workbuddy`、`CODEBUDDY.md`、`07-scripts/`、`文档库`)。~~在 DSH 上以**当前工作区的规则文件**与 `$DSH_HOME` 下的实际落点为准~~ 🔴 **2026-10-02 纠正:宿主是 WorkBuddy、不是 DSH;`$DSH_HOME` 本机已悬空** ⇒ 以**当前工作区的规则文件** + `E:\ProgramData\.workbuddy\skills\` 下的实际落点为准;⛔ 不要把正文里的旧路径当现行事实。 > ⚠️ 冲突时:**工作区规则 > 本技能**。 ### 🔴 宿主落点对照 —— ⚠️ **本节方向已作废(2026-10-02 纠正)** > 🔴 **左右方向与现状相反,⛔ 不要再照它执行。** 下表是 **2026-09-26 从 WorkBuddy 迁往 DSH 当时** > 的形态(让人把 `~/.workbuddy/08-skills//` 换成 **`$DSH_HOME/skills//`**)。 > **现状**:宿主是 **WorkBuddy**(不是 DSH);技能真身就在 `E:\ProgramData\.workbuddy\skills\\`; > 而 `$DSH_HOME`=`E:\ProgramDSH\.dsh` 在本机**目录已不存在**(悬空老值,只剩环境变量还留着)。 > ⇒ 正确方向是 **WorkBuddy ← DSH**:正文里见到 `$DSH_HOME/skills//`,换成 > **`E:\ProgramData\.workbuddy\skills\\`**。 **下表保留仅为留痕(右列=已作废的旧落点)**: | 正文里的旧路径 | ~~DSH 侧实际落点~~(**已作废**) | |---|---| | `~/.workbuddy/08-skills//`、`.workbuddy/08-skills//` | ~~**`$DSH_HOME/skills//`**(技能根,被 watch,改动下一请求生效)~~ 🔴 **作废:方向反了**,现行落点=**`E:\ProgramData\.workbuddy\skills\\`** | | `.workbuddy/memory/MEMORY.md`、`~/.workbuddy/MEMORY.md` | **`$DSH_HOME/AGENTS.md`**(全局,每轮注入)/工作区 `AGENTS.md`(项目级) | | `.workbuddy/memory/YYYY-MM-DD.md`(按日记忆) | **无对等物** ⇒ 教训写进本技能 `references/` 或项目台账「实测教训」节 | | `CODEBUDDY.md`(项目常驻规则) | **同一份文件仍生效**(已在 preset 覆盖里加入 DSH 的指令候选),无需改名 | | `07-scripts/.py`(文档库脚本) | 同左,但**路径必须写全**:`D:\github\dsh_shenxian\dsh-server-docs\07-scripts\.py` | | `~/.workbuddy/settings.json` 的 hooks | **`$DSH_HOME/hooks/hooks.json`** + `dsh-guard.py` | | WorkBuddy `automation_*` / 积分口径 | **无对等物**(见 `dsh-auto-handoff-chain` 的不可移植清单) | > 🔴 **表尾收口(2026-10-02 补)**:**上表每一行的右列都属同一"迁往 DSH"的旧形态,一律作废** —— > 尤其第 2 行(`$DSH_HOME/AGENTS.md`)与第 6 行(`$DSH_HOME/hooks/hooks.json`):本机**没有 `$DSH_HOME`**。 > 已实测的现行落点只有三条,照这三条找: > ① **技能根** = `E:\ProgramData\.workbuddy\skills\\`; > ② **hook 注册面** = `E:\ProgramData\.workbuddy\settings.json` 的 `hooks` 段(2026-10-02 实测:本轮就是往那儿加的第 6 条); > ③ **记忆** = 用户级 `~/.workbuddy/MEMORY.md`(跨项目)+ 工作区 `<工作区>/.workbuddy/memory/`(按日 + 长期)。 > ⛔ 上表左列里凡写 `$DSH_HOME/…` 的,**不要再当落点用**;`E:\ProgramDSH\` 整树已于 2026-09-28 删除 > (见 `dsh-local-env/references/01-环境引导与迁移.md` §"同名影子树")。 > ⚠️ 正文其余处出现的旧路径属**历史记录**(当时的事故与实测),**保持原样不改** —— 改写历史记录等于造假。 --- # dsh-change-workflow — dsh 平台改造工作流 dsh 多租户平台(服务器 47.77.182.89,dshs + dsh 0.1.2-rc.1)功能改造的标准处理方法。把平台运维历史中反复验证过的处理方式固化为可执行流程:**先识别边界 → 源码级调研 → 方案对比确认 → 小步开发 → 全链路验证 → 归档清理**。 > 📂 **平台速查表(域名 / 生效路径 / 端口 / 存量事实)已下沉** → `dsh-change-workflow/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. **先看本机/项目已有资产**:`$DSH_HOME\skills\`(尤其本技能,技能根被 watch、改动下一请求生效)、`MEMORY.md`; 3. **要装任何东西前自问:本机已有的能否满足?** 能→不装;不能→**先说清楚再装**。 → **固化结论(别再重新发现)**:浏览器自动化**只允许一件** —— **`browser-harness`**(调用姿势 ⇒ 本节末指针); ⛔ **`agent-browser` 已禁用**(2026-09-25 用户明令:「**改为 browser-harness,只允许使用这个**」); ⛔ **禁止 Playwright / `playwright-core`**(2026-09-13 用户明令); 静态页与 API 取数用 `curl`/WebFetch,别动浏览器。 1. **环境盘点**:本机/服务器/工具链/权限/DB/实例状态先行(历史教训:未盘点就默认本机装 Docker 走错路线,被用户批评)。 2. **需求边界**:区分"方案请求"(只输出方案,P0 硬规则:**方案与执行分离,未经明确授权不改文件**)与"任务明确直接执行"。 3. **用户确认**:关键分叉用选项表收敛(`ask_user_question`:推荐项置首标注「(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.` 抛 `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/` ⚠️ **拿新版平台验证登录态时还有一个坑**:`sid` cookie 是 `Secure` + `Domain=.ai1net.com` ⇒ 本机 `curl -c ` **不会**为 `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-<功能>-/` 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=" ``` (明文 `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//lib/client.js` + package.json version/description) 2. `npm pack` → tgz 拷到用户 `ws/`(chown 用户 uid)→ `setpriv --reuid= --regid= --init-groups env HOME=... DSH_HOME=... dsh plugin --profile web remove/add ` 3. kill 实例进程 → 门户自动 respawn 新 pid+**新端口** → `journalctl -u dshs` 抓 `dsh web: http://127.0.0.1:/?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. **浏览器验证栈(只走 `browser-harness`;⛔ `agent-browser` 与 Playwright 均已禁用)**: ### ⛔ 铁律 0:动手前先 `list_tabs()` 确认「你在驱动哪个浏览器」 ⛔ **不要假定端口号** —— 端口绑定会变,**断言端口=把当日观察写成永久结论**: - 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`)。 - 2026-09-26 实测:**9222 被用户浏览器占(PID 25272)· 9223 空闲** —— 与 09-11 的归属**相反**。 ⇒ **判据不是端口号,是 `list_tabs()` 返回的标签页是不是你自己开的**。端口每次现测: `curl --noproxy '*' http://127.0.0.1:/json/list`。 **所以:脚本第一步永远是 `print([t["url"] for t in list_tabs()])`。** - 只要看到的**不是**你自己新开的空浏览器 → **立刻停手**,只做只读检查。 - **禁止**在未确认隔离前做任何有副作用的动作(登录 / 输入 / 提交 / 关闭标签)。 - 已造成的真实事故:在用户浏览器里 `fetch('/api/auth/login')` → **覆盖其 `.ai1net.com` 的 `sid`**,用户在该浏览器里变成测试账号身份。 补救 = 删掉测试账号(cookie 失效 → 用户重新登录即可)。**收尾必须 `close_tab` 关掉自己开的标签。** > 📂 **浏览器验证栈的调用细节已下沉** → `dsh-change-workflow/08-浏览器验证栈详解.md`(正确调用 · dsh composer(Lexical)输入难点 · agent-browser 旧实测记录 · 独立 headless 定型做法 · 三个必踩坑) > ⛔ **留在这里的判据**:只用 `browser-harness` **一件**(⛔ `agent-browser` + 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=` ⇒ **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=` → `curl` 回 bundle → grep 新/旧标记(**用完即删临时会话**) | bundle 没下发、下发的是旧内容 | > 只做 ① ⇒「脚本全绿但用户说没变」;只做 ①② ⇒ 漏掉「用户拿到的那份」。 9. **🔴 用户说「参考门户/某个现成页面的样式」时:抄那个页面的源码,不要抄规范的抽象条目**(2026-09-13 第 3 次踩): 正确动作 = 打开 `web/portal.html` / `web/admin.html` 的 `