build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(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/;交接单不入库(政策)。
320 lines
42 KiB
Markdown
320 lines
42 KiB
Markdown
---
|
||
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 |
|