dsh-orchestrator-canvas · 工作画布流
本仓库 100% 由 AI 生成 —— 需求、代码、测试、文档都是人与 AI 对话里由 AI 写出来的,人只负责验收和拍板。 实验性 · 自用性质 · 非商业:它紧贴 DeepSeek Harness(DSH)的宿主接缝(
apply/inject/ client bundle / 配置解析),只在写它的那个 DSH 版本上验证过,宿主升级后可能直接失效。 安装前必读:DSH 的 profile 加载是全有或全无——一个插件import失败会连坐整个 profile,甚至把配置写残。装之前先备份 profile 里的package.json与cordis.patch.yml。 没有发布到 npm:把本目录放进 profile 的node_modules(或"<包名>": "link:<本目录绝对路径>"),再在package.json的dsh.profile.bundles里登记,重启 DSH。 卸载/回滚:从dsh.profile.bundles摘掉 →pnpm remove <包名>或删掉 junction → 重启;出问题就用备份的那两个文件还原。
把「下达任务 → 拆解 → 派子代理 → 验证据 → 确认交付」做成一张图 + 五道闸门,
并在会话右侧/主叠加一个只读编排画布。主智能体只统筹,活由子代理干,
但每一步的完成都必须有落到 run.json 里的证据 —— 图即真相。
- 宿主侧:5 个模型可见工具
orch_plan / orch_dispatch / orch_verify / orch_accept / orch_state - 客户端侧:输入框工具栏一个「◈ 编排画布」小圆点 → 点开是全帧画布(
shell.overlay) - 数据:
<dsh-home>\orchestrator\runs\<runId>\{run.json, events.jsonl}
为什么是「组合式」而不是再造一个调度器
| 能力 | 现有件 | 本插件的取向 |
|---|---|---|
| 并行派子代理 | 宿主 ctx.subagents / subagent 工具 / Agent Teams |
直接用,不自写调度引擎 |
| 会话流程图投影 | dsh-flowglass(三列泳道、子代理支线) |
可并装,与本插件的编排图正交,互不依赖 |
| 图数据模型 | @xiaoso/dsh-workflow-lite(TS + ReactFlow + elkjs,5MB 产物) |
不 vendor:借鉴 nodes/edges/status 字段命名,实现自己写 |
| 闸门 + 证据链 + 接受摘要 | 无现成直替品 | 本插件新增,这是真正的增量 |
所以组合方式是:宿主已有的调度能力照用 + flowglass 负责「这对对话发生了什么」的自动流视图 + 本插件负责「任务被拆成哪几步、每步的证据够不够、能不能验收」。画布是投影,不是新的编排入口。
安装 / 卸载
# 安装(官方路径:让插件管理器执行 install_bundle,target 指向本目录
# —— 插件的版本号会以「源目录的 package.json」为准)
# 等价手工:在 profile package.json 的 dsh.profile.bundles 追加 "dsh-orchestrator-canvas",
# dependencies 加 "dsh-orchestrator-canvas": "link:<repo>"
# ⚠️ 必须是 link: 不能是 file:。pnpm 对 file: 是「复制」,会把客户端 bundle 冻在安装那一刻,
# 之后改 src/ 重建 client.js 都不会生效(2026-10-29 的「画布打不开」就是这么来的)。
# 装完在 profile 目录跑一次 pnpm install 让 node_modules 变成指向源码目录的 junction。
# 卸载:从 bundles/dependencies 移除后重启。
# ⚠️ remove_bundle 对 link:/file: 依赖会**删掉本地源码目录**(实测 2026-10-29),
# 卸载前先把 <repo> 归档一份。
Remove-Item -Recurse -Force <dsh-home>\orchestrator # 可选:删历史 runs
装完先跑审计闸门,再重启(DSH bundle 是「全有或全无」,一个插件 import 失败会连坐整个 profile):
node <workspace>\tools\audit-dsh-plugins.mjs # 期望 PROBLEMS: none
五道闸门
1. Brief(改写,不扩权)
orch_plan 的 brief 是主智能体对用户原话的改写,落盘为 run.json.brief,画布顶部只显示它。
规则:brief 只能收敛范围,不能新增需求;后续 orch_verify 的判定也要对着 brief 说话。
2. Decompose(DAG)
节点类型三种:step(由子代理执行)、check(必须挂在一个 step 之后,产出证据)、merge(可多父,只做汇合验收)。
orch_plan 落盘前做五项校验:id 唯一 / 边两端存在 / 无环 / 类型合法 / 每个 step 至少被一个合法 check 覆盖。
返回 runId + order(拓扑序)+ layers + dispatchable。
3. Dispatch(真派活)
orch_dispatch 调 ctx.subagents.startActivation 派一个子代理,把 prompt / childId / 原始输出 / stopReason
写回 run.json.nodes[].attempts[]。并行度上限默认 3(maxParallel),超了直接拒绝并说明;
依赖未完成的节点、check/merge 节点都会被拒。失败重跑出新 attempt,不覆盖历史。
派发是异步的:工具返回后子代理仍在后台跑,主智能体下一轮用 orch_state 看结果。
4. Verify(证据非空)
{ "kind": "command|test|artifact|review", "value": "npm test → 26/26 pass", "author": "model", "at": "2026-10-29T13:20:00+08:00" }
orch_verify 的硬规则:evidence 为空或任一字段缺失 → 直接拒绝。
verdict: "fail" 会把该 check 记失败、把下游全部标 blocked,并返回「重试 prompt 补丁」。
一个 check 失败后,只有它挂的那个 step 重跑成功,check 才会被重新激活(不推翻历史 attempt)。
5. Accept(人工显式确认)
orch_accept 的前置条件:所有 step done + 所有 check verdict=pass + 用户显式确认。
不满足时拒绝并列出缺什么(缺哪个节点、缺什么证据、哪个 check 还挂着)。
满足时产出「交付物 → 节点 → 证据」摘要写进 run.json.acceptance。
确认走 ctx.userQuestions,但只在 root agent 的工具调用里弹(子代理没有人类应答者)。
状态机
pending → running → done
↘ failed → 下游全部 blocked
blocked(上游失败 / 被 reject 的合并点)
- 状态只能经工具写入;画布、抽屉、
/_dsh/orchestrator-canvas/api/*全部只读。 events.jsonl逐行记录plan / dispatch / verify / accept / prune事件,带自增seq与at。run.json原子写:写 .tmp → fsync → rename,同一 run 的写走 Promise 队列,不会写出半个文件。
画布
- 入口:输入框工具栏左侧的「◈ 编排画布」按钮(
conversation.input.left),常驻显示,不占常态版面。 - 本体:点开后是
shell.overlay上的全帧面板 —— 不挤压会话布局,Esc / 点遮罩 / ✕ 关闭。 - 运行归属与过滤:
orch_plan会把发起它的会话 id 与工作目录写进run.json的sessionId/cwd;画布顶部有三档范围:- 本会话(默认):只看当前会话发起的运行;
- 本工作区:看当前会话所在目录下、所有会话发起的运行;
- 全部:跨会话、跨目录全看。 范围只存在内存里(默认本会话),客户端不写 localStorage,保持只读红线。当前范围过滤后为空但有历史运行时,画布会显示「本范围暂无 · 已显示全部」并退回全部,避免空面板。
- 0.1.5 及更早创建的 run 没有这两个字段,会落到「本会话」以外的兜底视图(
sessionId为null)。
- 布局:列 = 依赖深度(左→右),同列按类型(执行→检查→汇合)纵排;有父节点的卡片垂直对齐父节点中心,再按最小间距下推,避免连线穿卡片。
- 卡片:状态点 + 类型徽标 + 状态文字 + 目标 + 子代理标识 + 证据数 + 耗时 / 第 N 次 / 有错误。卡片 240×112。
- 抽屉:点卡片 → 右侧浮层抽屉展示 目标、判定、brief、被阻塞原因、失败原因、派发 prompt、子代理原始返回、证据列表、全部 attempt 记录。
- 缩放/平移:滚轮(按住 Ctrl 或触控板)缩放,拖空白处平移,右下角 +/-/100% 复位。
- 窄屏:
≤820px自动降级为单列列表视图(信息不丢,只是不画连线)。 - 主题:只用
--dsw-alias-*token,亮/暗自动跟随;字号全部 ≥12px。 - v0 不做:拖节点改图(图只能由
orch_plan写入)。
配置
cordis.patch.yml 里给插件行传 config(全部可选):
- id: orchestrator-canvas
name: dsh-orchestrator-canvas
config:
runsDir: '<dsh-home>\orchestrator\runs' # 默认取 <DSH_HOME>/orchestrator/runs
maxParallel: 3 # 默认 3
subagentProvider: spawn # 默认 spawn;不存在时退回注册表第一个
subagentCwd: '' # 子代理工作目录,空则继承当前进程
autoPrune: true # 默认 true:每次 orch_plan 前做一次保留策略
retentionDays: 30
retentionBytes: 209715200 # 200MB
outputLimit: 12000 # 单次工具返回里子代理原始输出的截断长度
验证(作者自测,全部离线)
cd <repo>
node --check src/index.js
node --check src/graph.js; node --check src/store.js
node --test # 41 tests:graph 15 / store 11 / client-bundle+layout 9 / apply 接缝 6
node tools/build-client.mjs --verify # 手写 bundler 构建 + vm 真 materialize factory
node tools/preview.mjs # 6 张 PNG:正常/含失败/含并行 × 亮/暗
node <workspace>\tools\audit-dsh-plugins.mjs # PROBLEMS: none 才算过闸
powershell -NoProfile -ExecutionPolicy Bypass -File <workspace>\tools\verify-orchestrator-canvas-boot.ps1 -Rounds 3
# 3 次冷启动:隔离 DSH_HOME 里启 desktop profile 副本,每次断言 health+state 200
为什么冷启动要「隔离 DSH_HOME + profile 副本」:dsh --profile desktop 被 Electron 独占(CLI 直接
error: profile "desktop" is managed exclusively by the Electron application),而重启真正的桌面实例会把
正在用的会话一起杀掉。折中做法是复制 package.json / cordis.patch.yml,node_modules 用 junction 指回
真实 profile,在临时 DSH_HOME 里用 dsh web 冷启动——这走的是同一条 bundle 加载 + patch 合成 + 插件
mount + 客户端 bundle 表构建路径(实测 3/3 都是 http-200/200,stderr 里 orchestrator-canvas 相关错误 0 行)。
Electron 外壳本身的冷启动仍是唯一没在本机自动化的验收项。
tools/build-client.mjs 的 MODULES 是客户端模块的唯一事实来源:新增 src/client/*.js
必须登记,否则构建直接报错(这是 2026-10-29 echocat 事故的防线:手写 bundler 漏登记模块
会让 client entry import 失败并连坐整个 profile)。
test/apply.test.mjs 是接缝测试:它不看源码字符串,而是真实 apply(ctx, config) 之后
拿注册出来的 5 个工具按真实顺序调用(plan → dispatch → verify → accept),断言落盘结果:
merge自动汇合必须写进events.jsonl(kind: "merge"),否则审计日志里汇合步骤不可见;- check 判 fail 会把下游(含 merge)标
blocked,orch_accept拒绝并点名缺口,且拒绝本身落事件(kind: "accept-rejected"); - 闸门全过时
orch_accept走userQuestions弹确认,确认后run.json.status = "accepted"并写acceptance.acceptedAt。
events.jsonl 可能出现的 kind:plan / dispatch / dispatch-done / dispatch-fail / dispatch-error /
verify / merge / accept / accept-rejected。
三个设计决策(规格 §9 要求回答)
① 是否复用 @xiaoso/dsh-workflow-lite 的 JSON 结构?—— 不复用,只对齐命名。
workflow-lite 的图是「可点击执行的流程」为核心(节点带 executor/坐标/按钮语义,产物是 5MB 的 TS+ReactFlow 包),
而本插件的图是「带证据闸门的状态机」:字段重点在 attempts / evidence / verdict / blockedReason,
这些在 workflow-lite 里没有对应物。复用它的 schema 等于把 run.json 钉死在对第三方包字段的假设上,
而这个包的升级不受我们控制 —— 图即真相的前提是我们完全掌握写入口。折中做法:nodes/edges/id/type/status
命名与它保持一致,将来真要迁移,写一个字段映射脚本即可,不需要重写数据层。
② check 判定是纯确定性还是允许 LLM reviewer 打分?—— v0 纯确定性,review 只作为证据类型存在。
判定输入只有两样:verdict(pass/fail,由主智能体下)与 evidence[](必须有值,且每项必须带
kind/value/author/at)。不引入打分权重、不引入多 reviewer 投票 —— 理由:可复现(同一份证据任何时候复核结论一致)、
成本可控(不额外烧 token)、责任清晰(判定者 author 写进证据链)。
需要主观判断的场景走 kind: "review":把 reviewer 的意见原文当 value 存下来,人仍然能读到「谁、什么时候、依据什么说的」。
③ 历史 run 保留多久?—— 30 天 + 200MB 双阈值,滚动删最旧的。
每次 orch_plan 前跑一次 pruneRuns(autoPrune: false 可关)。先按天数删(updatedAt 优先,读不到退 mtime),
超字节上限再从最旧往新删。run.json 单文件很小,200MB 实际能放相当久的历史;
真正的体积来自 events.jsonl 与子代理输出,属于「证据」,值得留。
边界:如果一个 run 自己就超过 200MB,它会被删掉 —— 这是刻意的(宁可丢历史,也不能让磁盘无界增长)。
已知限制
- 子代理派发是后台异步的:插件重启后,进行中的 attempt 无法恢复,只会保留
running状态; 点「刷新」看到的一直是running就说明子代理侧已经丢了,需要人工重新 dispatch(会生成新 attempt)。 orch_accept的「用户显式确认」依赖ctx.userQuestions;若当前会话是子代理(无人类应答者), 工具会拒绝并提示「无法在当前上下文向用户确认」。已实测:编排跑在子代理里时 accept 无法落盘, 必须在你本人的会话里调(这也是test/apply.test.mjs用 stub userQuestions 覆盖该路径的原因)。- 子代理
orch_dispatch目前是等子代理跑完再返回(不是「派出去就返回、后续轮询」): 好处是主智能体拿到返回就能直接orch_verify;代价是一次 dispatch 可能等几十秒。 所以「派发后节点长时间 running」这种场景在当前实现里不会出现,画布上的 running 只对应进行中的那一次调用。 - 画布不做图编辑(v0 约束),改图只能重新
orch_plan。 - 预览 PNG 用本机 Edge/Chrome headless + CDP 截图。若
node tools/preview.mjs在本机起不来浏览器 (2026-10-29 实测:已有 Edge 实例残留时会一直挂住),可以先用--headless=new --remote-debugging-port=9224自己起一个,然后node tools/preview.mjs --cdp 9224复用它;也可以用--html-only只生成 HTML。
变更记录
0.1.6(2026-10-29)
- 新:运行归属。
orch_plan把发起者的sessionId与工作目录写进run.json,orch_plan的返回与/api/state都带出来;画布顶部加「本会话 / 本工作区 / 全部」三档过滤,默认本会话,过滤后为空自动退回全部并提示。 - 加:
/api/state?session=<id>用会话服务反查header.cwd回传context(取不到只回cwd:null,不抛错)。 - 测试:41 条(新增「run 归属字段 + /api/state 会话上下文」接缝测试)。
- 约束:过滤范围只存内存、不写 localStorage——客户端保持「不写任何持久状态」的只读红线。
0.1.5(2026-10-29)
- 修:画布打不开。根因是 profile 里用的是
file:依赖,pnpm 复制了一份旧客户端且不随源码更新; 改成link:+pnpm install后 node_modules 指向源码目录,改代码只做「页面刷新」即可生效。 - 修:点节点卡片打开抽屉时整块浮层消失。原因是
/api/state的节点摘要把evidence压成条数, 而抽屉按数组渲染,number.map抛错被宿主 slot 边界吞掉并卸载子树。现在抽屉同时兼容两种形状。 - 修:卡片文案被 flex 压扁(goal 高度 11px)。卡片 240×112、goal/meta 不参与收缩、meta 单行省略。
- 修:抽屉改为浮在画布右侧(绝对定位),不再挤压画布;顶部标题与按钮不再换行。
No comments yet. Be the first to write one.