用户令逐字:「E:/ProgramData/.workbuddy/skills 提交仓库是指的这里」—— 即本目录就是仓库(2026-10-07 已在本目录建仓,见当日日志 §22),本轮把余下未纳管的 9 个技能一并提交。 本次入库(9 个技能,46 个文件): 1、`AI HOT` 2、`draw-ui` 3、`dsh-diagnose` 4、`dsh-knowledge` 5、`dsh-local-env` 6、`dsh-opensource-release` 7、`dsh-workflow` 8、`oil-motion` 9、`skills-security-check` 提交前核对: · **凭据类扫描**(`*.env` / `*token*` / `*.key` / `*secret*` / `*.pem`)⇒ **零命中** ✓; · 体积合计约 20 MB(`draw-ui` 12M + `oil-motion` 6.5M 是大头,形态为配图/素材 —— 仓库 `.gitignore` 里明写「`assets/*.png` 是内容不是产物」⇒ 属刻意入库); · 运行产物仍按既定规则排除(`__pycache__` / `logs/` / `tmp/` / `.venv/` / `*.egg-info` / `uv.lock` / `.workbuddy/`)。
13 KiB
跨机状态回流(③)
归属:技能
dsh-diagnose· 详情档(主干../SKILL.md§3) 本档覆盖:原技能dsh-distributed-state-readback全文 原行段:原SKILL.md全文 209 行,其中 frontmatter 占前 5 行已剥离 ⇒ 本档承载第 6–209 行 搬运方式:逐行未改(⛔ 未删任何判据 / 命令 / 事故事实) ⚠️ 本技能已由dsh-diagnose合并,原名dsh-distributed-state-readback退役 ⇒ 见到该名按本档读。
DSH 跨机状态回流:判缺陷 or 判缺失能力,以及通路怎么选
§0 这个技能治什么
DSH 是 Manager + worker 形态:用户的 profile 树、实例、沙箱都长在 worker 上, 而平台 API、控制面 DB、UI 都在 Manager 上。只要一个状态"真身在 worker、读取在 Manager",就必然出现"读到的是假的"。
典型症状(⚠️ 都是静默的,不报错、不崩,只是值不对):
/api/plugins/mine的enabled跨机时恒为false- admin 面看到的启停状态与用户实例实际不一致
- 用户"启用了但刷新就没了"
- 某个字段永远是旧值 / 永远是初始值
- 命令(apply / launch / stop)下发成功,但状态回不来
⛔ 最容易犯的错:把它当代码缺陷去修(去查 enable 是不是失败了、是不是权限问题、是不是包没装上)。 先按 §1 判性质,判错方向会白烧一整轮。
§1 第一步:判「缺失能力」还是「代码缺陷」(三条排除)
动作:挑一个现场实例,独立取证"那件事在原机上到底成没成"。 三条都排除掉 ⇒ 是缺失能力,不是缺陷(⇒ 该立项做回流,而不是 debug)。
| # | 要排除的假设 | 怎么证(在实例所在那台机上取,⛔ 不是 Manager) |
|---|---|---|
| 1 | 「操作本身失败了」 | 看目标机的文件系统:软链/目录/写入的声明实际存在吗?(ls -la + 解一下软链能不能落到 package.json) |
| 2 | 「执行环境没配好」 | 看目标机的进程命令行:tr '\0' '\n' < /proc/<pid>/cmdline —— 挂载/参数是否真的注入了? |
| 3 | 「调用链断了」 | 看下发是否真走到远端:调用端有没有"已转发到 X 机"的日志/返回值? |
判据一句话:
原机上成了 + Manager 侧读不到 = 缺失能力(拓扑必然),⛔ 不是缺陷。
✅ 本判据在 DSH 上的实证:客实例 uid 100002 在 106(worker)上 enable storyforge —— ① 106 上软链与 package.json 的 file: 声明都在;② 沙箱命令含 --ro-bind-try 共享层挂载;③ applyPlugins 确实转发到了 106。
⇒ 三条全排除 ⇒ 性质 = 单向下发、无状态回流(有去路、没回路)。
§2 第二步:定位"缺的到底是哪条路径"(⚠️ 最省时间的一条)
别一口气说"回流缺失",先把读/写两条路径分开看:
| 路径 | 形态 | 缺不缺 |
|---|---|---|
| 写路径(apply / launch / stop) | Manager → worker(下发) | 多数已有 |
| 响应路径(任务返回值) | 装配返回对端读出来的真值 | 多数已有 ✅ |
读路径(GET 那种"随时查状态") |
Manager 读自己本机盘 | 🔴 常缺的就是这条 |
判据:若"操作完成后立刻返回的值是对的、但刷新页面后变错" ⇒ 缺的只有读路径。 ⇒ 设计目标应写成「让读路径也拿到真值」,⛔ 不是「把返回值再采一遍 / 加个缓存」。
⚠️ 实测教训:DSH 里 apply 的响应已经是真值(回的是对端 bundles),前端切换后状态正常;只有页面重读 GET 时才变 false。若不先做这一步分辨,会把"加缓存"当成解法 ⇒ 造出第三个真相。
§3 第三步:通路三候选,用物理可行性筛(⛔ 不是用"哪个更好"筛)
三条候选的传统比法是"延迟/成本/复杂度",在 DSH 上先要用"物理上走不走得通"筛:
候选 A · Manager 主动拉 ✅ 默认首选
做法:复用已有的 Manager→worker 调用,把它的返回值加宽。
🔴 先找"已经在跑的调用" —— DSH 上已有一条:src/supervisor/leased-spawner.ts 的 reportHost()
每次心跳就 GET <agentUrl>/healthz 一次,却只读回 {ok, instances}。
⇒ 最小改动 = 把响应体加宽(加 perUser),⛔ 不必新建链路。
优点:零新增网络口、零新增凭据、不新增入站面、失败语义(心跳断了 = 不可达)白捡、与既有归属判据天然一致。
缺点:非事件驱动(要等心跳周期);响应体随用户数增长(必须设上限);/healthz 语义被拓宽 ⇒ 防"重响应拖垮存活探测"。
候选 B · 共用台账 ⚠️ 通常不能单独成立
做法:apply 成功时写控制面台账,读时查台账。
🔴 致死点(先查这个):对账(reconcile)是谁驱动的?
DSH 上 reconcile 由 worker 本机定时器驱动(src/worker/agent.ts 的 20 s 本地定时器),
Manager 不在场 ⇒ "apply 时写台账"根本收不到那次变更 ⇒ 台账覆盖不了 reconcile 路径。
⇒ B 不是独立候选,只能当 A 或 C 的缓存。判据:台账一旦落表就会有人当权威读 ⇒ 必须明确它只是缓存。
优点:事件驱动、零延迟;admin 一次查询看全域;与既有台账同库。 缺点:引入第二个真相(台账 vs 对端 profile 必漂);覆盖不到 reconcile;多 Manager 联邦下"落哪个库"会错(用户迁机后台账残留);不可达时无判据。
候选 C · worker 主动上报 ⚠️ 常撞红线
做法:worker 在状态变更时主动 POST 给 Manager。
🔴 先查一条:worker 有没有入站口?
DSH 的硬口径是 「worker 只拨出、无入站口」 ⇒ "worker 推给 Manager"物理上走不通,
要新开一条 worker→Manager 出站通道或入站端点 ⇒ 撞 CODEBUDDY.md §1「扩大可见面」红线门禁。
优点:纯事件驱动最省资源;状态天然新鲜;多 worker 缩放到更优。 缺点:新增出站通道+新凭据+新信任关系 = 扩大可见面;Manager 须开入站口(与既有形态冲突);失败/重试/退避全要另造;补历史仍要配一条 A 式的拉取。
筛法总结(照这个顺序问)
- worker 有入站口吗? 没有 ⇒ C 直接撞红线,需用户授权才能谈。
- reconcile 谁驱动? 在 worker ⇒ B 不能单独成立。
- 已有一条 Manager→worker 的周期调用吗? 有 ⇒ A 的增量 = 加宽返回值,成本最低。
⇒ 结论通常是 A 为骨架 + B 降级为可选缓存,C 需先授权。
§4 语义细节(不写这三条 ⇒ 回流做出来照样是错的)
4-1 asOf 必带,stale 现算不落库
| 字段 | 语义 | 要点 |
|---|---|---|
enabled |
对端事实 | 取"事实"不取"意图" |
asOf |
该读数的采集时刻(epoch ms,与全库口径一致) | ✅ 必填,否则无法判陈旧 |
stale |
now - asOf > 阈值 |
🔴 派生、现算 —— ⛔ 落库的 stale 自己也会陈旧 |
source |
'pull' | 'task' |
诊断用 |
阈值自决 = 3 × 心跳周期:单跳失败不该立刻报陈旧(网络抖动),连续 3 跳才说明腿真断了。 ⚠️ 阈值须与心跳周期同源计算,⛔ 不写两个独立常量(两套判据必然漂移)。
4-2 三层语义别混用(🔴 这是"永远 false"的根因)
| 层面 | 真值在哪 | 谁权威 |
|---|---|---|
| 意图(用户点了要开) | 控制面审计流水 | 平台 |
| 事实(profile 里真开了) | 对端 profile 的 bundles | 实例所在那台机 |
| 不可用(能不能开) | 数据面状态台账 | 平台 |
⇒ GET 的状态字段取"事实",不取"意图"。
⚠️ 失败重试期间应仍显示旧事实,⛔ 不显示"意图已提交" —— 否则用户看到 true 但实例上没装 = 更糟的假象。
4-3 降级:⛔ 不许把"未知"显示成"未启用"
| 场景 | 行为 |
|---|---|
| 单跳失败 | 不动读数(阈值内) |
| 连续 ≥3 跳失败 | stale=true + UI 标"可能滞后" |
| 该机 status = down | stale=true + 保留最后一次成功读数 |
| 从未成功拉过 | 回落本机快照 + 标 source 缺失 ⇒ UI 显**"未知"** |
🔴 最后一行是重点:当前缺陷的成因就是"把未知显示成未启用" ⇒ ⛔ 别在新代码里复制它。
§5 与既有台账的关系(别硬塞)
先比粒度:既有台账多是**「插件级」(每插件一行); 用户启停状态是「用户 × 插件级」** ⇒ 粒度不同,塞不进去。
⇒ A 案不建表(内存缓存即可);B 案才建,且:
- 🔴 必须落控制面(与既有控制面表同库)—— 按「归属/租约类只有 Manager 能写」的分层判据。
- ⛔ 不得落 worker 本地库(会造出"用户迁机后台账留在旧机")。
§6 收尾:交付件骨架
规划棒出件时按此 10 节写(照 DSH 现有交付物风格):
- 问题定义(现象 + 排除项 + 影响面按严重度)
- 事实基础(现读代码,带行号;⛔ 不凭记忆)
- 由事实推出的硬约束(C1/C2/C3)
- 通路形态三候选(逐条优点 + 缺点 + 一张对比表)
- 写入时机(T1 操作完成后 / T2 对账时 —— ⛔ 两个都要)
- 陈旧度语义(
asOf/stale/ 阈值) - 失败重试与不可达降级(场景表)
- 与既有台账的关系(粒度对比 ⇒ 建不建表)
- "不做会怎样"(四时间尺度,说明为什么不能长期搁置)
- 落地轮廓(每候选的实现提示 + 验收判据)+ 附复核命令
§7 反模式(⛔ 见到就打住)
- 当缺陷 debug —— 原机上已成了 ⇒ 是缺失能力,⛔ 别查权限/装包。
- 想"加个缓存" —— 缓存就是第二个真相;先修读路径。
- 让 worker 主动推 —— 先问"worker 有入站口吗"。
- 只做 apply 时写状态 —— 漏掉 reconcile(实例启动对账会把状态改回去)。
- 落库
stale—— 派生值必须现算。 - 把"未知"显示成"未启用" —— 这是原缺陷的成因。
- 两个独立常量算阈值 —— 阈值必须与心跳周期同源。
- 台账当权威 —— 台账是缓存;权威在对端 profile。
- 替用户拍形态 —— 三候选各有优有劣 ⇒ 上抛(候选竖排成段、每条写优点+缺点)。
§8 关键文件地图(DSH · ⚠️ 行号会漂,用 grep 现找)
| 要找什么 | 去哪 |
|---|---|
读路径(GET 读本机盘) |
src/web/routes/business-plugins.ts 的 /api/plugins/mine |
| 写路径(下发+响应真值) | 同文件 mine/apply 的 await supervisor.applyPlugins(user.id, []) |
| 已有周期调用(A 案落点) | src/supervisor/leased-spawner.ts 的 reportHost()(GET /healthz) |
| 免凭据口 | src/worker/agent.ts 的 onRequest hook(/healthz 豁免) |
| worker 本地定时器(reconcile 驱动点) | src/worker/agent.ts 的 healTunnel / reconcileTunnel |
| 归属真相 | 控制面 dsh_instances.host_id(src/db/repo.ts) |
| 主机心跳落点 | dsh_hosts.last_heartbeat(src/db/repo.ts 的 setDshHostStatus) |
| 共用纯读盘函数 | src/supervisor/plugin-assembly.ts 的 readProfileManifest |
找"已有周期调用"的命令:
cd D:/github/dsh_shenxian
grep -rn "healthz\|setInterval" src/supervisor/ src/worker/ --include=*.ts
变更历史(原 frontmatter · 逐字保留)
name: dsh-distributed-state-readback
description: DSH 多租户平台(Manager + worker 集群形态)里判断与设计「跨机状态回流」的方法 —— 治「平台侧读到的状态是假的 / 永远是 false / 永远是旧值」这类**静默错误**。当出现「某字段在跨机时恒为 false」「admin 看到的启停状态与用户实际不一致」「命令下去了但状态回不来」「刷新后状态回跳」「要不要让 worker 主动上报」时使用。核心:先用三条判据把「缺失能力 vs 代码缺陷」分开,再按「拉/推/共用台账」三候选的**物理可行性**筛掉走不通的形态。
agent_created: true