Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 审核队列:
Expand Down
37 changes: 35 additions & 2 deletions scripts/serve-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions scripts/smoke-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 示例需要包含标准表头");
Expand Down Expand Up @@ -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 });
Expand Down