Skip to content

Commit 7d0321f

Browse files
committed
feat(orchestra): conductor 单进程整合 + JoinRoom 主房间 + 协作 race fix
[orchestra-cc] 19:35 那波改动, 由 [ui-cc] 总控统一 commit 上线. 后端 (orchestra-cc 完工): - server/orchestra-conductor.js (新, 173 行) — 单进程整合 dispatcher+worker 端口 17083, HTTP API 接管 rooms, 默认 BOOT_ROOMS=demo-final - server/orchestra-base/dispatcher/hermes-worker/http.js — 去日志中文修 Windows GBK 渲染 UTF-8 乱码 - server/package.json 加 conductor / orchestra-related npm scripts - start-orchestra.bat / stop-orchestra.bat — 切 4 进程模型 (yws + conductor + orchestra-http + vite) 前端 (orchestra-cc 完工): - src/pages/JoinRoom.jsx — 加"快速进入主房间·demo-final"按钮 (黑底暖色), 原"进入房间/新建房间"改名为"自定义/随机". 三人 demo 一键同房间 - src/components/canvas/TaskNode.jsx — 加 AGENT_OPTIONS (hermes/claude-cli/ feishu-bot) + AUTO/MANUAL toggle 给 orchestra 流加 UI 入口 - src/stores/useCanvasStore.js — partialize 排除 nodes/edges, 修协作 race 根因: persist middleware 把画布数据写 localStorage, sync 完成后 push 会 delete yjs 上别人 inject 的节点 测试: - e2e/orchestra-conductor-verify.spec.js — conductor 端到端 (8.8s 跑通) - e2e/orchestra-{dag,real-run,self,race}-test.spec.js — 各种场景验证 - e2e/diagnose-yjs.spec.js — race 复发诊断脚本 (留着备查) 文档: - docs/orchestra-blackboard-spec.md — 16 节完整 spec - docs/orchestra-runbook.md — 部署/运维手册 - docs/session-log-2026-05-02-conductor.md — orchestra-cc 自留会话日志 兼容性: 跟 [ui-cc] Aletheia 流 + LLM proxy 无冲突 (ui-cc 已审, 见 CC-HANDOFF 22:30 段). 不影响 OntologyNode / ChallengeNode / aiService / vps-proxy. [orchestra-cc] code, [ui-cc] commit
1 parent 0cd9fed commit 7d0321f

21 files changed

Lines changed: 3439 additions & 55 deletions

docs/orchestra-blackboard-spec.md

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Orchestra: 画布即多 agent 黑板(Blackboard)
2+
3+
> Yjs Y.Doc 不只是"前端同步用的状态",它本身就是 agent 共享内存。任何能 connect y-websocket 的 Node 进程,都是这块画布的一等公民 worker。
4+
5+
---
6+
7+
## 1. 核心思想
8+
9+
传统多 agent 框架(CrewAI / AutoGen / LangGraph)都给 agent 一个**集中 orchestrator**,agent 之间通过 message bus 协调。
10+
11+
我们的形态不同:
12+
13+
| 角色 | 做什么 | 通信方式 |
14+
|------|-------|---------|
15+
| **画布(Y.Doc)** | 共享黑板 — 所有 task / status / 关系都写在这里 | Yjs CRDT |
16+
| **dispatcher** | 看黑板,决定哪些 task 现在 ready;超时回收 lease | Y.Doc observe + transact |
17+
| **agent worker** | 看到自己名下的 ready task 就抢锁开干 | Y.Doc observe + transact |
18+
| **** | 在画布上画/拖/改任意节点 | React Flow + Yjs |
19+
20+
**关键洞察**:人和 agent 操作同一个 Y.Doc,行为模式完全一致。人在画布上拖一条边(加依赖)→ agent 立刻照新拓扑工作。这是中心化 orchestrator 给不了的"人在 loop 里"。
21+
22+
---
23+
24+
## 2. 与现有 manual 流的关系
25+
26+
现在 TaskNode 已经有 manual 流:
27+
28+
```
29+
浏览器 click "派给 Hermes" → fetch hermes-proxy:17081 → Hermes API → polling → 更新 TaskNode
30+
```
31+
32+
这条流**完全保留**。orchestra 是**旁路新增**
33+
34+
```
35+
任何人改画布让 TaskNode.agentMode='auto' → dispatcher 在 Y.Doc 看到 → 写"ready"标记
36+
37+
agent worker 看到自己的 ready task → CAS 抢锁 → 跑 → 写结果回 Y.Doc → 浏览器同步
38+
```
39+
40+
切换由 `data.agentMode` 字段决定(默认 `'manual'`)。零回归风险。
41+
42+
---
43+
44+
## 3. 节点 schema 扩展
45+
46+
现有 `TaskNode.data` 不动,新增以下 **可选** 字段:
47+
48+
| 字段 | 类型 | 默认 | 含义 |
49+
|------|------|------|------|
50+
| `agentMode` | `'manual'` \| `'auto'` | `'manual'` | auto 时由 orchestra 自动调度 |
51+
| `assignedTo` | string \| null | null | agent 名(`'hermes'` / `'claude-cli'` / `'feishu-bot'` / `'human:nieao'`|
52+
| `claimedAt` | ISO timestamp \| null | null | 抢锁时间,用于 lease 超时回收 |
53+
| `claimedBy` | string \| null | null | 抢到锁的 worker 实例 ID(同名 agent 多副本时区分)|
54+
| `leaseExpiresAt` | ISO timestamp \| null | null | lease 到期时间,过期 dispatcher 把 status 改回 'pending' |
55+
| `inputs` | string[] | [] | 上游节点 ID 列表(暂未启用,预留依赖图)|
56+
57+
`status` 沿用现有 `TASK_NODE_STATUS`
58+
59+
```
60+
draft → dispatching → pending → running → done | failed
61+
```
62+
63+
orchestra 增加一个虚拟态 `'pending'` 表示"已 ready 但还没被 worker 抢"。
64+
65+
---
66+
67+
## 4. 状态机(auto 模式)
68+
69+
```
70+
draft
71+
│ (人填完 title + assignedTo, 或 dispatcher 看到 agentMode='auto' && deps OK)
72+
73+
pending
74+
│ (worker A 抢锁: CAS-set status='running', claimedBy=A, claimedAt=now, leaseExpiresAt=now+lease)
75+
76+
running
77+
│ (worker A 跑完,写结果 + status='done') │ (lease 超时, dispatcher 改回 pending)
78+
▼ ▼
79+
done pending (允许另一 worker 重抢)
80+
81+
任意状态 → failed (worker 显式标记 + 写 error 到 data.error)
82+
```
83+
84+
**幂等性**:worker 任务必须做幂等(重抢可能再跑一次)。Hermes 调用走 `idempotency_key`
85+
86+
---
87+
88+
## 5. CAS 抢锁协议
89+
90+
Y.Doc 的 transact 只在**当前 client 进程内**原子,跨 client 仍可能两个 worker 同时通过判断后 set。所以需要二段验证:
91+
92+
```js
93+
// in worker
94+
function tryClaim(nodeId) {
95+
let claimed = false
96+
ydoc.transact(() => {
97+
const fresh = nodesMap.get(nodeId)
98+
if (!fresh) return
99+
if (fresh.data.status !== 'pending') return // 别人抢走了或已 running/done
100+
if (fresh.data.assignedTo !== MY_NAME) return // 不归我
101+
nodesMap.set(nodeId, {
102+
...fresh,
103+
data: {
104+
...fresh.data,
105+
status: 'running',
106+
claimedBy: WORKER_ID,
107+
claimedAt: new Date().toISOString(),
108+
leaseExpiresAt: new Date(Date.now() + LEASE_MS).toISOString(),
109+
},
110+
})
111+
claimed = true
112+
})
113+
114+
if (!claimed) return false
115+
116+
// 二段确认: 100ms 后再读, 看 claimedBy 是不是我 (CRDT 收敛后的真值)
117+
await sleep(100)
118+
const settled = nodesMap.get(nodeId)
119+
if (settled?.data?.claimedBy !== WORKER_ID) {
120+
// 双跑了, 我退让
121+
return false
122+
}
123+
return true
124+
}
125+
```
126+
127+
**双跑窗口**:100ms 内两个 worker 都通过 transact 都 set 自己 — Yjs 收敛后只有一个 `claimedBy`(last-write-wins on Map.set)。落败方退出,胜出方继续。坏情况:100ms 内的工作量被白做一次(agent 端无副作用 / 已幂等就 OK)。
128+
129+
---
130+
131+
## 6. lease 心跳与回收
132+
133+
worker 在执行期间每 30s 续 lease:
134+
135+
```js
136+
setInterval(() => {
137+
if (!isRunning) return
138+
ydoc.transact(() => {
139+
const fresh = nodesMap.get(nodeId)
140+
if (fresh?.data?.claimedBy !== WORKER_ID) return // 已被回收别瞎写
141+
nodesMap.set(nodeId, {
142+
...fresh,
143+
data: { ...fresh.data, leaseExpiresAt: new Date(Date.now() + LEASE_MS).toISOString() },
144+
})
145+
})
146+
}, 30_000)
147+
```
148+
149+
dispatcher 每 10s 扫一遍所有 `status='running'` 的节点,`leaseExpiresAt < now` 的强制 reset 到 `pending`,并清空 `claimedBy/claimedAt/leaseExpiresAt`
150+
151+
---
152+
153+
## 7. 调度器职责(dispatcher)
154+
155+
只做三件事:
156+
157+
1. **ready-set 计算**:scan nodesMap,找 `agentMode='auto' && status='draft' && (deps all done)`,把 status 改为 `'pending'`
158+
2. **lease 超时回收**:上面 §6 的 reset
159+
3. **环检测**(防 agent 互相派单死循环):当一条边 from A.outputs to B.inputs 但 B 的 outputs 又指向 A 时,把 A.B 双方设为 `failed` 并写 error
160+
161+
dispatcher 不做"派给谁" — 那是节点 `assignedTo` 字段决定的(人或上游 agent 写)。
162+
163+
---
164+
165+
## 8. agent 命名规范
166+
167+
- `hermes` — Hermes Kanban worker
168+
- `claude-cli` — 调本地 claude CLI
169+
- `feishu-bot` — 飞书机器人
170+
- `human:<username>` — 派给具体的人(agent 跳过;人在画布上手动改 status)
171+
172+
worker 启动时声明自己叫什么,只接 `assignedTo` 等于自己名的任务。
173+
174+
---
175+
176+
## 9. 与已有 hermes-proxy 的关系
177+
178+
`server/hermes-proxy.js` 是 HTTP 代理,给浏览器 manual 派单用 — **保留不动**。
179+
180+
`server/orchestra-hermes-worker.js` 是 Y.Doc client + Hermes 调用合体:
181+
- import hermes-proxy.js 里的 `hermesCall` 函数(重构出来共用)
182+
- 不走 HTTP 中转,直接调 Hermes API
183+
- 完成后把结果写 Y.Doc,浏览器同步看到
184+
185+
**两条流复用 hermesCall 一份代码**,避免 schema 漂移。
186+
187+
---
188+
189+
## 10. 多 agent 创建 agent 的护栏
190+
191+
防止 agent 自我繁殖爆炸:
192+
193+
- **agent 不能写 status='draft'** 的新节点(draft 只能由人或专门的"分支 agent"创建)
194+
- agent 只能 :
195+
- 改自己 owned 节点的 status / data / outputs
196+
- 创建 type='resultNode' 的结果节点 + 一条 edge 接到自己的 TaskNode
197+
- 违规写入由 dispatcher 检测 + 回滚
198+
199+
最小可演示阶段,agent 行为白名单:`status update only` + `create resultNode + edge`
200+
201+
---
202+
203+
## 11. demo 场景(黑客松级)
204+
205+
人在画布上画:
206+
207+
```
208+
[ConceptNode: 营销主题]
209+
210+
211+
[TaskNode: 调研 5 个竞品]
212+
agentMode: 'auto'
213+
assignedTo: 'hermes'
214+
215+
216+
[TaskNode: 总结成对比表]
217+
agentMode: 'auto'
218+
assignedTo: 'claude-cli'
219+
220+
221+
[ResultNode: 待 agent 填]
222+
```
223+
224+
启动 dispatcher + hermes worker + claude-cli worker:
225+
226+
1. 5 秒内"调研 5 个竞品"被 hermes worker 抢锁,status → running
227+
2. 30 秒后 hermes 完成,写 ResultNode,status → done
228+
3. dispatcher 看到下游"总结对比表"deps OK,把它从 draft 推到 pending
229+
4. claude-cli worker 抢锁,2 分钟跑完,写最终 ResultNode
230+
231+
**全程三人浏览器同步看到节点变色、进度条流动、ResultNode 涌现**。
232+
233+
---
234+
235+
## 12. 失败模式与诊断
236+
237+
| 现象 | 可能原因 | 排查 |
238+
|------|--------|------|
239+
| 节点卡 pending 超过 1 分钟 | 没有匹配 assignedTo 的 worker 在线 | 看 server 日志,启动对应 worker |
240+
| 节点 running 不动 | worker 卡死 / 网络断 | 等 lease 超时(5 分钟)自动回 pending |
241+
| 双跑(结果出现两次) | CAS 二段确认失败但都跑了 | 检查 worker 端是否幂等;查 idempotency_key |
242+
| dispatcher 不推 ready | agent 看到的 status 还在 draft 不是 auto | 检查 `data.agentMode === 'auto'` 是否真的写进 Y.Doc |
243+
244+
dispatcher 自己也写一份运行日志到 `nodesMap.get('__dispatcher_log__')`(一个不渲染的特殊节点 ID),浏览器可以开开发者工具看。
245+
246+
---
247+
248+
## 13. 不在范围内(明确切割)
249+
250+
- ❌ Cron / 定时触发 — 用户启动是手动的
251+
- ❌ 流水线编辑器(visual programming) — 边只是 dependency hint,不传数据
252+
- ❌ 跨房间任务 — 每个 room 是独立的 Y.Doc,agent 只能服务它当前 connect 的 room
253+
- ❌ 鉴权细分 — agent 跟用户在同一房间,写权限相同(黑客松简化;P1 加 ACL)
254+
255+
---
256+
257+
## 14. P0 实现清单(本次会话目标)
258+
259+
- [x] 写 spec(本文档)
260+
- [x] `server/orchestra-base.js` — Y.Doc client 基类
261+
- [x] `server/orchestra-dispatcher.js` — 调度器
262+
- [x] `server/orchestra-hermes-worker.js` — Hermes worker(mock 完成,真 API 路径已写但未连真凭据)
263+
- [x] `server/package.json``dispatcher` / `worker:hermes` / `orchestra:e2e` / `orchestra:race` script
264+
- [x] 端到端 mock demo (`npm run orchestra:e2e`) — draft → running → done + 自动建 ResultNode
265+
- [x] CAS 抢锁竞态测试 (`npm run orchestra:race`) — 3 worker 抢 5 task 不双跑
266+
267+
## 15. 启动 demo 的最小操作
268+
269+
```bash
270+
# 终端 1: y-ws-server (1234)
271+
cd server && npm run yws
272+
273+
# 终端 2: dispatcher
274+
cd server && node orchestra-dispatcher.js demo-room
275+
276+
# 终端 3: hermes worker (默认 mock; 真调 Hermes 需 set HERMES_USER/HERMES_PASS)
277+
cd server && node orchestra-hermes-worker.js demo-room
278+
279+
# 浏览器: 进入 ?room=demo-room, 双击空白加 TaskNode,
280+
# 改 data.agentMode='auto' + data.assignedTo='hermes', dispatcher 5s 内 promote, worker 抢锁跑
281+
```
282+
283+
## 16. 验证命令
284+
285+
```bash
286+
cd server
287+
npm run orchestra:e2e # 单 worker 端到端: draft → done + ResultNode
288+
npm run orchestra:race # 3 副本抢 5 task: 不双跑
289+
```
290+
291+
P1(黑客松后):
292+
- claude-cli worker (复用 server/claude-bridge.js 或直接 spawn)
293+
- 飞书 bot worker (听飞书消息 → 写画布 / 监听画布 → 推飞书群)
294+
- 真接 Hermes API (设 HERMES_USER/HERMES_PASS, 关掉 ORCHESTRA_MOCK)
295+
- 节点 outputs 字段做"上游 result 注入下游 prompt"的数据流
296+
- 环检测 + 黑名单 (防 agent 互相派单死循环)

0 commit comments

Comments
 (0)