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:
@@ -0,0 +1,68 @@
|
||||
// Gateway HTTP/SSE 客户端:提交 DSL 任务、订阅 Token 流、登记偏好记忆。
|
||||
import type { TaskDsl } from "./dsl";
|
||||
|
||||
// 开发期直连 Gateway;Wails 打包后可改为本地后端地址或经 Go 绑定。
|
||||
export const GATEWAY: string =
|
||||
(import.meta.env.VITE_GATEWAY as string | undefined) ?? "http://localhost:8080";
|
||||
|
||||
export interface Identity {
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
// submitTask: POST /api/v1/tasks,返回 task_id。
|
||||
export async function submitTask(dsl: TaskDsl, id: Identity): Promise<string> {
|
||||
const res = await fetch(`${GATEWAY}/api/v1/tasks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-User-ID": id.userId,
|
||||
"X-Session-ID": id.sessionId,
|
||||
},
|
||||
body: JSON.stringify(dsl),
|
||||
});
|
||||
if (!res.ok) throw new Error(`submit failed: ${res.status} ${await res.text()}`);
|
||||
const data = (await res.json()) as { task_id: string };
|
||||
return data.task_id;
|
||||
}
|
||||
|
||||
// streamTokens: 订阅 SSE /api/v1/tasks/:id/stream,逐 token 回调,done 收尾。
|
||||
// 返回关闭函数。注意 EventSource 无法带请求头,但流按 task_id 寻址,无需身份头。
|
||||
export function streamTokens(
|
||||
taskId: string,
|
||||
onToken: (t: string) => void,
|
||||
onDone: () => void,
|
||||
onError?: (e: unknown) => void,
|
||||
): () => void {
|
||||
const es = new EventSource(`${GATEWAY}/api/v1/tasks/${taskId}/stream`);
|
||||
es.addEventListener("token", (e) => onToken((e as MessageEvent).data));
|
||||
es.addEventListener("done", () => {
|
||||
es.close();
|
||||
onDone();
|
||||
});
|
||||
es.onerror = (e) => {
|
||||
es.close();
|
||||
onError?.(e);
|
||||
};
|
||||
return () => es.close();
|
||||
}
|
||||
|
||||
// setMemory: PUT /api/v1/memory,登记一条用户偏好(→ mcp-go memory_upsert)。
|
||||
export async function setMemory(
|
||||
id: Identity,
|
||||
key: string,
|
||||
value: string,
|
||||
): Promise<string> {
|
||||
const res = await fetch(`${GATEWAY}/api/v1/memory`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-User-ID": id.userId,
|
||||
"X-Session-ID": id.sessionId,
|
||||
},
|
||||
body: JSON.stringify({ key, value }),
|
||||
});
|
||||
const data = (await res.json()) as { message?: string; error?: string };
|
||||
if (!res.ok) throw new Error(data.error ?? `memory failed: ${res.status}`);
|
||||
return data.message ?? "ok";
|
||||
}
|
||||
Reference in New Issue
Block a user