# 运维锚点 · 会话记录取证 · 功能插件启用 > **归属**:技能 `dsh-change-workflow` 的详情档(**按需读**,不是每次都要读)。 > **本档覆盖**:通用运维锚点(重启用) · 功能插件启用:探活 · 快照回滚 · 逐插件隔离 · 会话记录取证(原行 L505–L525 + L935–L998)。 > **主文件 / 判据与流程主干** = `../SKILL.md`(§1 六阶段流程 · §2 红线 R1–R8 原文速查 · §3 本机 Git Bash 环境坑 · §4 并行调度结论)。 > **来源**:2026-09-22「技能重组线」把 `../SKILL.md` 的 L505–L525 + L935–L998 段**逐行原样**下沉到本文件,未改一字。 > **跨档引用**:正文里的「§N / 见 §8 坑 N / 见下表」等编号,用 `../SKILL.md` 末节「详情索引」的原章节列定位。 > **维护**:本文件与 `../SKILL.md` 的指针行成对;改内容时同时核对主文件的指针描述是否仍准确。 --- ## 通用运维锚点(重启用) - 门户重启后实例需重新 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 :` - 测试 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-` 报 `No such file or directory`(本次踩坑)。 - 服务器 git:`git -C /opt/dshs`(user.name/email = deploy@dsh.alotbuy.com) - **⚠️ 服务器 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。 ## 功能插件启用:探活 · 快照回滚 · 逐插件隔离(档案 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:/*.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)**:`07-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 工作空间下的对话记录,能发现哪些问题」。**这是只读任务,可与其他只读任务并行**,且**适合丢给子代理**(大文件长分析,别吃主上下文)。 **路径规律**(`` = 用户 id,admin = `cce6d1cd-b376-4304-80f0-0e1c58c9ffde`): ``` U=/var/lib/dshs/users/ $U/home/sessions/----/session-/session.jsonl.zstd $U/home/storages/workspace.json # 工作区注册表:id → { path, title, sessionIds } ``` 例:`--var-lib-dshs-users--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 > /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 -P 22 "$S/an.cjs" root@47.77.182.89:/tmp/an.cjs ssh -i ~/.ssh/id_ed25519 -p 22 root@47.77.182.89 '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-调整方案/-<主题>.md`(含「修正旧档案的哪条结论」小节);② `INDEX.md` §二 追加 `04-` 行并更新状态摘要(**README 的档案清单已定格为历史对照,别只改 README**);③ `INDEX.md §六.1` 的「下一号」+1、`01-规范/03-路线图与待办.md` 补登记;④ 收尾四件套:`python 07-scripts/docs-audit.py`(**退出码 0**)→ `python 07-scripts/docs-manifest.py`(刷新机读清单)→ `bash 07-scripts/docs-sync-check.sh`(全绿)→ `MINE="<我改的文件>" PUSH=1 bash 07-scripts/handoff-guard.sh`(幽灵文件硬判定)。