feat: 实时入库监控 + 向量拆分可视化(异步入库 + 进度 SSE)

入库从同步改为异步流水线 + 进度回流(复用 token 流 NATS streaming)。
UI 实时看到 解析→切块→向量化(分批)→写入 各阶段 + 拆分块预览。

- shared: contract.IngestEvent(stage/done/total/chunks/error)
- mcp-go: rag.Ingest 加 onProgress + 分批向量化(10/批)逐批回报;kb_ingest 带 job_id
  把进度发到 sundynix.streams.<job_id> + CompleteStream
- gateway: 入库异步返回 job_id,后台 runIngest 发进度;GET /kb/ingest/:id/stream SSE
- frontend: streamIngest(EventSource);KbView 实时进度面板(阶段徽标+进度条+拆分列表)
- 验证: build✓+e2e PASS; 浏览器 12 行→6 阶段点亮+进度条 12/12+拆分 12 块逐条

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-11 10:33:36 +08:00
parent 3550a22557
commit 2d5fd2fca5
8 changed files with 358 additions and 63 deletions
+38 -8
View File
@@ -47,27 +47,57 @@ export function streamTokens(
return () => es.close();
}
// ingestKb: POST /api/v1/kb/ingest,把文本入库(→ mcp-go kb_ingest:切块/embedding/Milvus)。
// 入库进度事件(与后端 contract.IngestEvent 对应)。
export interface IngestEvent {
stage: string;
msg?: string;
done?: number;
total?: number;
chunks?: string[];
error?: string;
}
// ingestKb: POST /api/v1/kb/ingest —— 文本入库(异步,返回 job_id)。
export async function ingestKb(kb: string, text: string): Promise<string> {
const res = await fetch(`${GATEWAY}/api/v1/kb/ingest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kb, text }),
});
const data = (await res.json()) as { message?: string; error?: string };
if (!res.ok) throw new Error(data.error ?? `ingest failed: ${res.status}`);
return data.message ?? "ok";
const data = (await res.json()) as { job_id?: string; error?: string };
if (!res.ok || !data.job_id) throw new Error(data.error ?? `ingest failed: ${res.status}`);
return data.job_id;
}
// ingestFile: POST /api/v1/kb/ingest_filemultipart)—— 上传文件入库(docx/xlsx/pdf… → mcp-py 解析)。
// ingestFile: POST /api/v1/kb/ingest_filemultipart)—— 文件入库(异步,返回 job_id)。
export async function ingestFile(kb: string, file: File): Promise<string> {
const fd = new FormData();
fd.append("kb", kb);
fd.append("file", file);
const res = await fetch(`${GATEWAY}/api/v1/kb/ingest_file`, { method: "POST", body: fd });
const data = (await res.json()) as { message?: string; chars?: number; error?: string };
if (!res.ok) throw new Error(data.error ?? `ingest file failed: ${res.status}`);
return `${file.name}:解析 ${data.chars ?? 0} 字 → ${data.message ?? "ok"}`;
const data = (await res.json()) as { job_id?: string; error?: string };
if (!res.ok || !data.job_id) throw new Error(data.error ?? `ingest file failed: ${res.status}`);
return data.job_id;
}
// streamIngest: SSE 订阅入库进度(/kb/ingest/:id/stream)。返回关闭函数。
export function streamIngest(
jobId: string,
onEvent: (ev: IngestEvent) => void,
onDone: () => void,
onError?: () => void,
): () => void {
const es = new EventSource(`${GATEWAY}/api/v1/kb/ingest/${jobId}/stream`);
es.addEventListener("progress", (e) => onEvent(JSON.parse((e as MessageEvent).data) as IngestEvent));
es.addEventListener("done", () => {
es.close();
onDone();
});
es.onerror = () => {
es.close();
onError?.();
};
return () => es.close();
}
export interface KbHit {