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

42 KiB
Raw Blame 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: 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 · 需求识别(先盘点再动手,勿跳步)

  1. ⛔ 开工前置检查(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,别动浏览器。

  2. 环境盘点:本机/服务器/工具链/权限/DB/实例状态先行(历史教训:未盘点就默认本机装 Docker 走错路线,被用户批评)。

  3. 需求边界:区分"方案请求"(只输出方案,P0 硬规则:方案与执行分离,未经明确授权不改文件)与"任务明确直接执行"。

  4. 用户确认:关键分叉用选项表收敛(AskUserQuestion:推荐项置首标注 "(Recommended)",2-4 选项,不设"其他"占位)。

  5. 输出格式:一句话结论先行 + 表格化细节(状态/结果列)。

阶段 1 · 调研(源码级实证优先,禁止只靠文档推断)

  1. ★ 用户报障第一步:先拿「真实失败请求」,别猜(2026-09-11 档案 51 实证,省掉全部试错)。 用户只会说"报错/没反应/发消息失败",而平台 journal 里有精确的 URL + method + status:

    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…)——改的地方完全不同。
  2. 只读分析官方包(允许):/usr/local/lib/node_modules/@deepseek-ai/dsh/... 内 bundle 读代码定位机制(slot 声明、inject 契约、fiber 注入)。绝不写官方主程序。

  3. 服务器实测 > 推断:区分 fence 层 / 应用层错误;浏览器问题抓 console 完整 err 对象(description/堆栈,勿只看字符串字段——at new apply 类堆栈是定位关键)。

  4. 双端对照:archive 源 ↔ 实例安装副本 md5 对账;官方行为对照第三方插件写法差异逐字段对比(如 register 的 locale/children/label 函数形式)。

  5. 多模型交叉验证:关键结论交叉核对,不轻信单点推断。

阶段 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/代理):

    # 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。

  1. 静态文件 vs 后端改动:改 web/*.html 无需重启(静态直出,实测服务启动时间不变);改 src/** 才 npm run build + systemctl restart。别为前端改动重启服务(会打断所有用户实例)。

  2. 安全面复测:降权 uid 探测敏感目录(/root、/etc/shadow、DB)应 denied;中间目录 711(去 r 留 x)不破坏子进程访问。

  3. 清理临时物:poc-* session 用完即删(DB DELETE);测试 tab 关闭;临时 tgz/脚本归档或清。

  4. 截图留证:每功能验收存 PNG 到本地 tmp + present_files 展示。

  5. 🔴 交付前必问一句:这份改动「出厂」了吗(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 一变浏览器下次请求就拿到新的(「看不到变化」几乎都是没出厂,不是缓存)。
  6. 视觉/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 没下发、下发的是旧内容

    只做 ① ⇒「脚本全绿但用户说没变」;只做 ①② ⇒ 漏掉「用户拿到的那份」。

  7. 🔴 用户说「参考门户/某个现成页面的样式」时:抄那个页面的源码,不要抄规范的抽象条目(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 · 归档清理(文档同步 + 三层沉淀)

  1. ★★ 交付门禁:本机改完 ≠ 交付(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 验"改对了没有"(三层验收);本门禁验"东西到用户那儿了没有"(链路完整性)。
  2. 文档落盘(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。
  3. 档案更新:04-调整方案/<NN>-*.md 写"需求→改动文件→commit→验证记录";README.md 档案清单行同步;版本号 bump。

    • ⚠️ 档案号必须『原子预留』,不能靠"读一眼 ls 再写"(2026-09-11 两次撞号实证):"取号 + 写档案 + 改 INDEX.md + 改 docs-status 同一串行事务"这条规则本身不够 —— 两条并行通道同时 ls 时都读不到对方的文件,必然撞。第二次撞号(37/38 各两条,4 个文件挤 2 个号)就是这么发生的。
      • 正确做法(原子锁):写档案前先原子占号,例如
        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,以它为准。
  4. 三层沉淀(用户习惯,必做):

    • 治本层:机制/流程固化进 skill/档案(本次即本 skill)
    • 失误层:教训 append 到 .workbuddy/memory/YYYY-MM-DD.md(append-only,不改写历史)
    • 记忆层:长期约定/红线写 .workbuddy/memory/MEMORY.md(≤3000 字符/会话)
  5. 汇总表:带状态/结果列交付,列明未完成项与副作用。排版按 dsh-feature-first §5.4(首屏给判定 / 层级≤3 / 每节≤7 行 / 表格≤5 列 / 一条信息只说一次;细节进附录)。

  6. 技能自身归档(本 skill 有改动时必做):工作副本在本机 .workbuddy/08-skills/<name>/SKILL.md(技能必须本地加载),服务器归档位在 /opt/dsh/docs/08-skills/<name>/SKILL.md(root 600)。改完本 skill 后单向推:

    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 这把)。每次先前置:
    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-调整方案/-<标题>.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