diff --git a/README.md b/README.md index cfc9fa3..f481b1e 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ npm run api - `GET /api/graph`:读取 `data/snapshots/current.json`,不存在时回退到虚构 demo。 - `GET /api/search?q=光模块`:返回可定位的节点、公司和证据命中。 - `GET /api/node/:id`:返回公司或产业节点详情、映射边和证据摘要。 +- `GET /api/review`:读取本地 JSONL 审核队列,便于脚本或前端复用。 - `POST /api/review`:把本地审核记录追加到 `data/review-queue/local-api-review.jsonl`。 让前端优先读取本地 API,并让人工校正同步写入 API 审核队列: diff --git a/scripts/serve-api.mjs b/scripts/serve-api.mjs index c9d6db4..08cd28d 100644 --- a/scripts/serve-api.mjs +++ b/scripts/serve-api.mjs @@ -78,6 +78,12 @@ export async function handleApiRequest(request, response, context) { return; } + if (request.method === "GET" && url.pathname === "/api/review") { + const records = await readReviewQueue(context); + sendJson(response, 200, { count: records.length, records }); + return; + } + if (request.method === "POST" && url.pathname === "/api/review") { const body = await readRequestJson(request); const record = buildReviewRecord(body); @@ -95,12 +101,12 @@ export async function readGraph(context = {}) { const localSnapshotPath = path.join(resolvedDataDir, "snapshots", "current.json"); try { const snapshot = JSON.parse(await readFile(localSnapshotPath, "utf8")); - return normalizeGraph(snapshot, "local_snapshot"); + return mergeReviewQueue(normalizeGraph(snapshot, "local_snapshot"), await readReviewQueue({ ...context, dataDir: resolvedDataDir })); } catch (error) { if (error.code !== "ENOENT") throw error; } const demo = JSON.parse(await readFile(path.join(resolvedRoot, "src", "data", "demoGraph.json"), "utf8")); - return normalizeGraph(demo, "demo"); + return mergeReviewQueue(normalizeGraph(demo, "demo"), await readReviewQueue({ ...context, dataDir: resolvedDataDir })); } function normalizeGraph(graph, source) { @@ -118,6 +124,33 @@ function normalizeGraph(graph, source) { }; } +export async function readReviewQueue(context = {}) { + const resolvedRoot = context.rootDir || rootDir; + const resolvedDataDir = context.dataDir || path.join(resolvedRoot, "data"); + const reviewPath = path.join(resolvedDataDir, "review-queue", "local-api-review.jsonl"); + let content = ""; + try { + content = await readFile(reviewPath, "utf8"); + } catch (error) { + if (error.code === "ENOENT") return []; + throw error; + } + return content + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line)); +} + +function mergeReviewQueue(graph, localRecords) { + const records = [...(graph.review_queue || []), ...localRecords]; + const byId = new Map(records.map((record) => [record.id, record])); + return { + ...graph, + review_queue: [...byId.values()].sort((a, b) => (b.created_at || "").localeCompare(a.created_at || "")), + }; +} + async function readRequestJson(request) { const chunks = []; let size = 0; diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index 3159335..fdb10de 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -182,6 +182,7 @@ assert.match(serveApi, /\/api\/graph/, "本地 API 需要提供 /api/graph"); assert.match(serveApi, /\/api\/search/, "本地 API 需要提供 /api/search"); assert.match(serveApi, /\/api\/node\/:id/, "本地 API 需要说明 /api/node/:id"); assert.match(serveApi, /\/api\/review/, "本地 API 需要提供 /api/review"); +assert.match(serveApi, /readReviewQueue/, "本地 API 需要能读取 JSONL 审核队列"); assert.match(reviewTransport, /VITE_CHAINGRAPH_API_BASE/, "人工校正同步需要读取本地 API 配置"); assert.match(reviewTransport, /\/api\/review/, "人工校正同步需要调用 /api/review"); assert.match(csvMappingExample, /chain_id,chain_name/, "CSV 示例需要包含标准表头"); @@ -233,6 +234,12 @@ try { ); assert.equal(syncedReview.source, "api", "人工校正同步需要标记 API 来源"); assert.equal(syncedReview.record.status, "pending", "人工校正同步需要返回 API 保存的 pending 记录"); + const apiReviewQueue = await fetchJson(`${apiBase}/api/review`); + assert.equal(apiReviewQueue.count, 2, "本地 API 需要能读取已写入的 JSONL 审核记录"); + assert.ok(apiReviewQueue.records.some((record) => record.id === apiReview.record.id), "GET /api/review 需要返回直接 POST 的审核记录"); + assert.ok(apiReviewQueue.records.some((record) => record.id === syncedReview.record.id), "GET /api/review 需要返回前端 transport 同步的审核记录"); + const apiGraphWithReview = await fetchJson(`${apiBase}/api/graph`); + assert.ok(apiGraphWithReview.review_queue.some((record) => record.id === syncedReview.record.id), "/api/graph 需要合并本地 JSONL 审核队列"); } finally { await new Promise((resolve) => apiServer.close(resolve)); await rm(apiDataDir, { recursive: true, force: true });