feat: 前端跑通 React Flow 画布→DSL→提交→SSE + 偏好记忆面板 (④)

桌面端前端从骨架变为可用:画布编排 Agent → 导出 DSL → 提交 Gateway →
逐 token 流式展示;偏好记忆面板让用户登记画像(→ memory_upsert)。

- lib/api.ts: submitTask(POST) / streamTokens(EventSource SSE token/done) / setMemory(PUT)
- canvas/AgentCanvas: 加节点(提示词)/连线/运行(导出DSL交上层), React Flow 工具栏
- panels/MemoryPanel: 登记偏好(key/value)→PUT /api/v1/memory
- App: 身份(user/session)+记忆面板+SSE 输出面板,串起提交→流式
- postcss.config + vite-env.d.ts(import.meta.env) 补齐构建;删 WikiPanel(stale Qdrant)
- Gateway: 加 CORS 中间件(放开跨源 + X-User-ID/X-Session-ID 头 + OPTIONS)
- Makefile: web 目标(Vite dev); .claude/launch.json(preview 配置), 忽略 settings.local

验证: npm run build(tsc+vite)✓; 真实浏览器跑通——加节点→运行→SSE 流出含
注入画像(称呼:老王)的回答, 第2轮 UI 显示'已有1轮历史'(短期历史经 Eino 图回灌)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-10 14:32:15 +08:00
parent 4928ffc0f7
commit 7d2891d88a
12 changed files with 3183 additions and 35 deletions
+78 -7
View File
@@ -1,15 +1,86 @@
import { AgentCanvas } from "./canvas/AgentCanvas";
import { WikiPanel } from "./wiki/WikiPanel";
import { useCallback, useRef, useState } from "react";
// UI Representation Layer —— 顶层布局:左侧编排画布 + 右侧 Wiki 面板。
import { AgentCanvas } from "./canvas/AgentCanvas";
import { MemoryPanel } from "./panels/MemoryPanel";
import { submitTask, streamTokens, type Identity } from "./lib/api";
import type { TaskDsl } from "./lib/dsl";
// 顶层布局:左侧 React Flow 编排画布 + 右侧 身份 / 偏好记忆 / 运行输出。
export default function App() {
const [identity, setIdentity] = useState<Identity>({ userId: "wt", sessionId: "sess-ui" });
const [output, setOutput] = useState("");
const [status, setStatus] = useState("就绪");
const [running, setRunning] = useState(false);
const closeRef = useRef<(() => void) | null>(null);
const onRun = useCallback(
async (dsl: TaskDsl) => {
closeRef.current?.();
setOutput("");
setRunning(true);
setStatus("提交任务…");
try {
const taskId = await submitTask(dsl, identity);
setStatus(`流式中 · ${taskId}`);
closeRef.current = streamTokens(
taskId,
(t) => setOutput((o) => o + t),
() => {
setStatus("完成 ✓");
setRunning(false);
},
() => {
setStatus("连接中断");
setRunning(false);
},
);
} catch (e) {
setStatus(`${(e as Error).message}`);
setRunning(false);
}
},
[identity],
);
return (
<div className="flex h-screen w-screen">
<div className="flex h-screen w-screen text-gray-900">
<main className="flex-1 border-r">
<AgentCanvas />
<AgentCanvas onRun={onRun} running={running} />
</main>
<aside className="w-96 overflow-auto">
<WikiPanel />
<aside className="flex w-[26rem] flex-col overflow-auto">
<section className="border-b p-4">
<h2 className="mb-2 text-sm font-semibold text-gray-700"> / </h2>
<div className="flex gap-2">
<label className="flex-1 text-xs text-gray-500">
<input
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
value={identity.userId}
onChange={(e) => setIdentity((id) => ({ ...id, userId: e.target.value }))}
/>
</label>
<label className="flex-1 text-xs text-gray-500">
<input
className="mt-1 w-full rounded border px-2 py-1 text-sm text-gray-900"
value={identity.sessionId}
onChange={(e) => setIdentity((id) => ({ ...id, sessionId: e.target.value }))}
/>
</label>
</div>
</section>
<MemoryPanel identity={identity} />
<section className="flex flex-1 flex-col p-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-sm font-semibold text-gray-700">SSE Token </h2>
<span className="text-xs text-gray-400">{status}</span>
</div>
<pre className="flex-1 whitespace-pre-wrap rounded bg-gray-900 p-3 text-xs leading-relaxed text-emerald-300">
{output || "在左侧加节点 → 运行,模型会注入你的偏好与历史后流式作答。"}
</pre>
</section>
</aside>
</div>
);