Skip to content

Commit a564e70

Browse files
committed
feat(canvas): Aletheia 本体拆解 + 反驳引擎 (融入元认知 + Hermes 合作)
来源: 飞书 wiki 0501-黑客松比赛-Aletheia. 画布从"协作知识图谱"升级为"本体拆解+对抗推理"决策引擎. 新增: - aiService: decomposeToOntology (一句话→Goal/Entity/Constraint/Assumption) challengeNode (6 种 Devil's Advocate: 资源/外部/逻辑/反例/逆向/二阶) - OntologyNode: 4 variant (建筑极简风, 暖色点缀), 每个底部 2 按钮 - ChallengeNode: 红/黄/灰 severity 反驳卡, 虚线连线 - store: addOntologyFramework / promoteOntologyToTask / dispatchChallenge - BottomAIBar: 新增 "一句话生成框架" 模式, 默认即此 体验: 输入"在上海开咖啡馆" → 自动建多节点框架 → 点节点"派 Hermes →" 派单走原 hermes-proxy / 点"反驳 ⚔"生成 N 个反驳节点. 修 bug: - TaskNode.jsx:36 无限递归 (handleUpdate=>handleUpdate), 改为 updateNode(id, patch) 兼容性: 不碰 [orchestra-cc] 的 server/orchestra-* 文件. 新建 TaskNode 默认走 manual 流 (旧逻辑), 不抢 dispatcher. [ui-cc] 2026-05-02
1 parent 87da330 commit a564e70

8 files changed

Lines changed: 718 additions & 7 deletions

File tree

docs/CC-HANDOFF.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@
4949

5050
## 当前文件锁
5151

52-
(空 — 每个 cc 开工前往这里加一行)
52+
- `server/orchestra-base.js` (新): claimed by [orchestra-cc] at 2026-05-02 17:56, until 2026-05-02 21:00
53+
- `server/orchestra-dispatcher.js` (新): claimed by [orchestra-cc] at 2026-05-02 17:56, until 2026-05-02 21:00
54+
- `server/orchestra-hermes-worker.js` (新): claimed by [orchestra-cc] at 2026-05-02 17:56, until 2026-05-02 21:00
55+
- `docs/orchestra-blackboard-spec.md` (新): claimed by [orchestra-cc] at 2026-05-02 17:56, until 2026-05-02 21:00
56+
- `server/package.json` (小改 scripts): claimed by [orchestra-cc] at 2026-05-02 17:56, until 2026-05-02 18:30
5357

5458
---
5559

@@ -291,6 +295,77 @@ P1 实施时, ui-cc 决定具体后端栈, 选哪个 boss 都接受。
291295

292296
---
293297

298+
## 2026-05-02 17:56 [orchestra-cc] 入场公告:画布作为多 agent 黑板
299+
300+
我是新加入的 cc, 负责把画布从"单纯的协作知识图谱"升级为**多 agent 协作黑板**
301+
302+
我做了:
303+
- 读完 CC-HANDOFF + hermes-integration-spec + 现有 src/collab/* 与 server/* 全套实现
304+
- 跟 boss 对齐了架构方向 (画布做"任务图编辑器", server/orchestra-dispatcher 做"轻量调度", agent worker 做"听调度+跑活")
305+
- 写下 `docs/orchestra-blackboard-spec.md` 详述节点 schema 扩展 + CAS 抢锁协议 + 与现有 manual TaskNode 流的兼容关系
306+
307+
我要做 (不碰 ui-cc 在改的文件):
308+
- `server/orchestra-base.js` - agent worker 基类 (Y.Doc client + observe + CAS 抢锁 + lease 心跳)
309+
- `server/orchestra-dispatcher.js` - 调度器 (拓扑解析 ready-set + lease 超时回收)
310+
- `server/orchestra-hermes-worker.js` - hermes 专用 worker (复用 hermes-proxy 的 hermesCall, 但不通过 HTTP 而是直接 import)
311+
- `docs/orchestra-blackboard-spec.md`
312+
- `server/package.json` 加几个 npm script
313+
314+
**不会碰** (这些 ui-cc 在改):
315+
- `src/components/canvas/KnowledgeCanvas.jsx` (双击 + zoomOnDoubleClick)
316+
- `src/pages/panels/BottomAIBar.jsx` (Aletheia 框架生成 + 节点派分支)
317+
- `src/components/canvas/TaskNode.jsx` (UI 显示, ui-cc 可能动)
318+
- `src/stores/useCanvasStore.js` 的 dispatchTaskNode + 派分支相关 action
319+
320+
兼容性保证:
321+
- 现有 manual 流 (浏览器点 "派给 Hermes" 按钮 → fetch hermes-proxy:17081) **不动**
322+
- 新加字段 `data.agentMode='auto' | 'manual' (默认 manual)``data.assignedTo``data.claimedAt`
323+
- agent 只接 `agentMode === 'auto'` 的节点, manual 节点完全不碰 — 旁路引入, 零回归风险
324+
325+
需要 [boss]:
326+
- 读 spec 后回 `GO` / `CHANGE: ...`
327+
- 决定: orchestra worker 启动方式 — 加进 `npm run all` 还是单独 `npm run orchestra`? (我先做后者)
328+
329+
需要 [ui-cc]:
330+
- 看到本签字后, 在 TaskNode UI 上**可选**: 增加一个小切换 "auto / manual" 显示 `data.agentMode`. 不强求, 我可以先用 prompt 默认值跑通 demo
331+
- 如果你重设计 useCanvasStore 的 addTaskNode 默认 schema, 请给新字段留位 (agentMode/assignedTo/claimedAt) — 不留也行, agent 自己会补
332+
333+
阻塞: 无, 直接动手
334+
335+
下一步:
336+
1. 写完 spec 文档 (本次会话内)
337+
2. 实现 orchestra-base + orchestra-dispatcher + orchestra-hermes-worker (mock 模式优先)
338+
3. 跑一个端到端 demo: 浏览器画 [TaskNode auto, assignedTo=hermes] → dispatcher 派单 → mock worker 抢锁 → 4 秒后 status=done + 自动建 ResultNode
339+
4. 真接 Hermes (调 hermes-proxy.js 的 hermesCall, 或直接复用其逻辑)
340+
341+
---
342+
343+
## 2026-05-02 21:30 [ui-cc] Aletheia 框架 + 双击修复 + TaskNode 递归 bug 修复
344+
345+
我是 ui-cc, 这一波做了:
346+
347+
1. **fix: 双击空白唤起菜单失效** — React Flow 默认 `zoomOnDoubleClick=true` 消费 dblclick, 给 ReactFlow 加 `zoomOnDoubleClick={false}`. (commit 87da330, 已上线 VPS 87da330)
348+
349+
2. **fix: TaskNode 无限递归 bug**`TaskNode.jsx:36` 原代码 `const handleUpdate = (patch) => handleUpdate(patch)` 是死循环, 改为 `(patch) => updateNode(id, patch)`. 之前 draft 状态编辑 title/body/assignee 必崩.
350+
351+
3. **feat: Aletheia 本体拆解 + 反驳引擎** (融入飞书 wiki 0501-黑客松比赛-Aletheia 概念)
352+
- `src/services/aiService.js`: 加 `decomposeToOntology(sentence)` (Onto-Parser) + `challengeNode(node)` (Antithesis Engine, 6 种 Devil's Advocate)
353+
- `src/components/canvas/OntologyNode.jsx` (新): 4 variant — goal / entity / constraint / assumption, 每个底部 2 按钮: "派 Hermes →" / "反驳 ⚔"
354+
- `src/components/canvas/ChallengeNode.jsx` (新): 红/黄/灰 severity 显示反驳论点
355+
- `src/stores/useCanvasStore.js`: 加 `addOntologyFramework(sentence)` / `promoteOntologyToTask(id)` / `dispatchChallenge(id)` 三个 action
356+
- `src/pages/panels/BottomAIBar.jsx`: 新增 "一句话生成框架" 模式 (默认), 调 `addOntologyFramework`. 输入"在上海开咖啡馆"→ 自动建 Goal+Entity+Constraint+Assumption 的多节点框架, 每节点可派 Hermes 执行 / 派反驳
357+
- `src/components/canvas/KnowledgeCanvas.jsx`: 注册 `ontologyNode` + `challengeNode` 到 nodeTypes
358+
359+
兼容性:
360+
-[orchestra-cc]`agentMode` 字段不冲突 — 我建的 `ontologyNode` / `challengeNode` 是新类型, `promoteOntologyToTask` 创建的 TaskNode 默认 `agentMode='manual'` (即旧流), 立刻调 dispatchTaskNode 走 hermes-proxy. 不抢 orchestra 调度器的活.
361+
- TaskNode 的递归 bug 修复跟 [orchestra-cc] 添加 agentMode 字段无冲突, 我没动 schema, 只修了一行函数赋值
362+
363+
下一步 (留给 [orchestra-cc] 决定):
364+
- OntologyNode 的"派 Hermes →"按钮, 是否要给一个 `agentMode='auto'` 选项? 现在硬编码 manual. 你要 auto 我可以加 toggle, 也可以你直接改 promoteOntologyToTask 的 data.agentMode 默认值.
365+
- ChallengeNode 现在用同步调用 `aiService.challengeNode` (走客户端 LLM provider). 如果你想让 challenge 也走 orchestra 调度 + worker 池, 我把它从 store 里的 `dispatchChallenge` 改成"建一个 type=challenge 的 TaskNode"再让 dispatcher 派.
366+
367+
---
368+
294369
## 还需协调的事 (滚动清单, 任何 cc 发现了就追加)
295370

296371
| # | 事项 | 谁负责 | 状态 |
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* ChallengeNode — Aletheia 反驳节点 (Devil's Advocate)
3+
*
4+
* 由 OntologyNode 的"反驳 ⚔"按钮触发, 自动建在源节点右侧.
5+
* 显示:
6+
* - 攻击角度 (6 类: 资源短缺/外部风险/逻辑矛盾/反例/逆向激励/二阶效应)
7+
* - claim 反驳论点
8+
* - severity 严重度 (high/medium/low → 红/黄/灰)
9+
*
10+
* 不可编辑 (read-only), 用户用它来收敛/修正源节点.
11+
*/
12+
13+
import { memo } from 'react'
14+
import { Handle, Position } from 'reactflow'
15+
16+
const SEVERITY_META = {
17+
high: { label: '严重', color: '#b27c8b', bg: '#fbf1f3' },
18+
medium: { label: '中等', color: '#c8a882', bg: '#f5f0eb' },
19+
low: { label: '轻微', color: '#888', bg: '#f5f5f5' },
20+
}
21+
22+
function ChallengeNodeImpl({ data, selected }) {
23+
const angle = data.angle || '反驳'
24+
const claim = data.claim || ''
25+
const severity = data.severity || 'medium'
26+
const sourceTitle = data.source_title || ''
27+
const meta = SEVERITY_META[severity] || SEVERITY_META.medium
28+
29+
return (
30+
<div
31+
className="relative shadow-sm transition-all duration-300"
32+
style={{
33+
width: 240,
34+
background: '#fafafa',
35+
border: `${selected ? '2px' : '1px'} solid ${meta.color}`,
36+
borderRadius: 4,
37+
}}
38+
>
39+
<Handle type="target" position={Position.Left} style={{ background: meta.color }} />
40+
41+
{/* 顶部色条 */}
42+
<div style={{ height: 3, background: meta.color }} />
43+
44+
<div className="px-3 py-2.5">
45+
{/* 顶部标签行 */}
46+
<div className="flex items-center justify-between mb-2">
47+
<span
48+
className="text-[9px] font-semibold"
49+
style={{ color: meta.color, letterSpacing: '0.2em' }}
50+
>
51+
⚔ DEVIL'S ADVOCATE
52+
</span>
53+
<span
54+
className="text-[9px] px-1.5 py-0.5 rounded-sm"
55+
style={{ color: meta.color, background: meta.bg, border: `1px solid ${meta.color}` }}
56+
>
57+
{meta.label}
58+
</span>
59+
</div>
60+
61+
{/* 攻击角度 */}
62+
<div className="text-[11px] font-medium mb-1.5" style={{ color: '#1a1a1a' }}>
63+
{angle}
64+
</div>
65+
66+
{/* 反驳论点 */}
67+
<div
68+
className="text-[11px] leading-relaxed italic"
69+
style={{ color: '#3a3a3a', borderLeft: `2px solid ${meta.color}`, paddingLeft: 8 }}
70+
>
71+
{claim}
72+
</div>
73+
74+
{/* 攻击对象 (源节点) */}
75+
{sourceTitle && (
76+
<div className="text-[9px] mt-2 pt-1.5" style={{ color: '#888', borderTop: '1px dashed #e8e8e8' }}>
77+
针对: <span className="font-medium" style={{ color: '#555' }}>{sourceTitle}</span>
78+
</div>
79+
)}
80+
</div>
81+
</div>
82+
)
83+
}
84+
85+
export default memo(ChallengeNodeImpl)

src/components/canvas/KnowledgeCanvas.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import NoteNode from './NoteNode'
3030
import GroupNode from './GroupNode'
3131
import TaskNode from './TaskNode'
3232
import ResultNode from './ResultNode'
33+
import OntologyNode from './OntologyNode'
34+
import ChallengeNode from './ChallengeNode'
3335
import SelectionToolbar from './SelectionToolbar'
3436
import NodePropertyPanel from './NodePropertyPanel'
3537

@@ -46,6 +48,9 @@ const nodeTypes = {
4648
// metahermes 集成: Hermes 任务派单 + 结果回流
4749
taskNode: TaskNode,
4850
resultNode: ResultNode,
51+
// Aletheia 集成: 本体拆解 + 反驳引擎
52+
ontologyNode: OntologyNode,
53+
challengeNode: ChallengeNode,
4954
}
5055

5156
// 知识关系类型
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* OntologyNode — Aletheia 本体节点
3+
*
4+
* 4 种 variant (从飞书 wiki Aletheia 设计):
5+
* - goal: 顶层目标 (黑底 + 暖色边)
6+
* - entity: 核心实体 (白底 + 暖色细线)
7+
* - constraint: 硬约束 (暖色背景 + 重边)
8+
* - assumption: 隐含假设 (灰背景 + 虚线, 等待验证)
9+
*
10+
* 每个节点底部 2 个动作按钮:
11+
* - "派 Hermes →" → 把节点转为 TaskNode 派单 (调研/执行)
12+
* - "反驳 ⚔" → 调反驳引擎生成 ChallengeNode (Devil's Advocate)
13+
*/
14+
15+
import { memo } from 'react'
16+
import { Handle, Position } from 'reactflow'
17+
import useCanvasStore from '../../stores/useCanvasStore'
18+
19+
const VARIANT_META = {
20+
goal: {
21+
label: 'GOAL',
22+
bg: '#1a1a1a',
23+
color: '#fafafa',
24+
border: '#c8a882',
25+
accent: '#c8a882',
26+
width: 280,
27+
},
28+
entity: {
29+
label: 'ENTITY',
30+
bg: '#fafafa',
31+
color: '#1a1a1a',
32+
border: '#e8e8e8',
33+
accent: '#c8a882',
34+
width: 220,
35+
},
36+
constraint: {
37+
label: 'CONSTRAINT',
38+
bg: '#f5f0eb',
39+
color: '#1a1a1a',
40+
border: '#c8a882',
41+
accent: '#c8a882',
42+
width: 220,
43+
},
44+
assumption: {
45+
label: 'ASSUMPTION',
46+
bg: '#fafafa',
47+
color: '#555',
48+
border: '#bbb',
49+
borderStyle: 'dashed',
50+
accent: '#888',
51+
width: 220,
52+
},
53+
}
54+
55+
function OntologyNodeImpl({ id, data, selected }) {
56+
const promoteToTask = useCanvasStore((s) => s.promoteOntologyToTask)
57+
const challenge = useCanvasStore((s) => s.dispatchChallenge)
58+
const updateNode = useCanvasStore((s) => s.updateNode)
59+
60+
const variant = data.variant || 'entity'
61+
const meta = VARIANT_META[variant] || VARIANT_META.entity
62+
const title = data.title || ''
63+
const description = data.description || ''
64+
const isChallenging = data.challenging === true
65+
66+
const onPromoteToTask = (e) => {
67+
e.stopPropagation()
68+
promoteToTask(id).catch((err) => {
69+
console.error('[OntologyNode] promote failed:', err)
70+
})
71+
}
72+
73+
const onChallenge = (e) => {
74+
e.stopPropagation()
75+
if (isChallenging) return
76+
updateNode(id, { challenging: true })
77+
challenge(id)
78+
.catch((err) => console.error('[OntologyNode] challenge failed:', err))
79+
.finally(() => updateNode(id, { challenging: false }))
80+
}
81+
82+
return (
83+
<div
84+
className="relative shadow-sm transition-all duration-300"
85+
style={{
86+
width: meta.width,
87+
background: meta.bg,
88+
color: meta.color,
89+
border: `${selected ? '2px' : '1px'} ${meta.borderStyle || 'solid'} ${selected ? '#c8a882' : meta.border}`,
90+
borderRadius: 4,
91+
}}
92+
>
93+
<Handle type="target" position={Position.Top} style={{ background: meta.accent }} />
94+
<Handle type="source" position={Position.Bottom} style={{ background: meta.accent }} />
95+
96+
<div className="px-4 py-3">
97+
{/* 顶部标签 */}
98+
<div className="flex items-center justify-between mb-2">
99+
<span
100+
className="text-[9px] font-semibold"
101+
style={{ color: meta.accent, letterSpacing: '0.25em' }}
102+
>
103+
{meta.label}
104+
</span>
105+
{variant === 'assumption' && (
106+
<span className="text-[9px]" style={{ color: meta.accent }}>未验证</span>
107+
)}
108+
</div>
109+
110+
{/* 标题 */}
111+
<input
112+
type="text"
113+
className="w-full text-sm font-medium bg-transparent border-none outline-none"
114+
style={{ color: meta.color, fontFamily: variant === 'goal' ? 'var(--font-serif), Georgia, serif' : 'inherit' }}
115+
placeholder="节点标题…"
116+
value={title}
117+
onChange={(e) => updateNode(id, { title: e.target.value })}
118+
/>
119+
120+
{/* 描述 */}
121+
{description && (
122+
<div className="text-[11px] mt-1.5 leading-relaxed" style={{ color: meta.color, opacity: 0.7 }}>
123+
{description}
124+
</div>
125+
)}
126+
127+
{/* 动作按钮 — goal 不可派 (太抽象), 其他 3 类都可 */}
128+
{variant !== 'goal' && (
129+
<div className="flex gap-1.5 mt-3">
130+
<button
131+
onClick={onPromoteToTask}
132+
disabled={!title.trim()}
133+
className="flex-1 text-[10px] py-1 px-2 rounded-sm border transition-all"
134+
style={{
135+
borderColor: title.trim() ? '#c8a882' : '#e5e5e5',
136+
color: title.trim() ? '#1a1a1a' : '#bbb',
137+
background: title.trim() ? 'rgba(245,240,235,0.6)' : 'transparent',
138+
cursor: title.trim() ? 'pointer' : 'not-allowed',
139+
}}
140+
title="转为 Hermes 任务节点 (执行/调研)"
141+
>
142+
派 Hermes →
143+
</button>
144+
<button
145+
onClick={onChallenge}
146+
disabled={!title.trim() || isChallenging}
147+
className="flex-1 text-[10px] py-1 px-2 rounded-sm border transition-all"
148+
style={{
149+
borderColor: title.trim() ? '#b27c8b' : '#e5e5e5',
150+
color: title.trim() ? '#7a3a4a' : '#bbb',
151+
background: title.trim() ? 'rgba(245,235,237,0.6)' : 'transparent',
152+
cursor: title.trim() && !isChallenging ? 'pointer' : 'not-allowed',
153+
}}
154+
title="生成 Devil's Advocate 反驳论点"
155+
>
156+
{isChallenging ? '反驳中…' : '反驳 ⚔'}
157+
</button>
158+
</div>
159+
)}
160+
161+
{/* goal 节点显示提示 */}
162+
{variant === 'goal' && (
163+
<div className="text-[10px] mt-2 opacity-50" style={{ color: meta.color }}>
164+
↓ 已自动拆解为下方实体 / 约束 / 假设
165+
</div>
166+
)}
167+
</div>
168+
</div>
169+
)
170+
}
171+
172+
export default memo(OntologyNodeImpl)

src/components/canvas/TaskNode.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function TaskNodeImpl({ id, data, selected }) {
3333
const dispatchTaskNode = useCanvasStore((s) => s.dispatchTaskNode)
3434
const updateNode = useCanvasStore((s) => s.updateNode)
3535

36-
const handleUpdate = (patch) => handleUpdate(patch)
36+
const handleUpdate = (patch) => updateNode(id, patch)
3737

3838
const status = data.status || TASK_NODE_STATUS.DRAFT
3939
const meta = STATUS_META[status] || STATUS_META[TASK_NODE_STATUS.DRAFT]

0 commit comments

Comments
 (0)