import fs from "node:fs/promises"; import path from "node:path"; const baseUrl = process.env.GB32960_BASE_URL || "http://115.29.187.205:20200"; const vin = process.env.GB32960_VIN || "LB9A32A24R0LS1720"; const plate = process.env.GB32960_PLATE || "粤AGP4377"; const exportDate = process.env.GB32960_DATE || "2026-08-17"; const dateFrom = `${exportDate} 00:00:00`; const dateTo = `${exportDate} 23:59:59`; const limit = 500; const outputPath = process.env.GB32960_DATA_PATH || new URL("./raw-frames.json", import.meta.url).pathname; async function fetchPage(offset, includeTotal = false) { const params = new URLSearchParams({ protocol: "GB32960", vin, dateFrom, dateTo, orderBy: "eventTime", limit: String(limit), offset: String(offset), includeFields: "true", includePayload: "true", includeTotal: includeTotal ? "true" : "false", }); const response = await fetch(`${baseUrl}/api/history/raw-frames?${params}`, { headers: { "Accept-Encoding": "gzip" }, signal: AbortSignal.timeout(60000), }); if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`); return response.json(); } const first = await fetchPage(0, true); const total = Number(first.total ?? 0); const offsets = []; for (let offset = limit; offset < total; offset += limit) offsets.push(offset); const remaining = await Promise.all(offsets.map((offset) => fetchPage(offset))); const items = [...(first.items ?? []), ...remaining.flatMap((page) => page.items ?? [])]; const frameIds = new Set(items.map((item) => item.frame_id)); const eventIds = new Set(items.map((item) => item.event_id)); const fieldKeys = [...new Set(items.flatMap((item) => Object.keys(item.parsed_fields ?? {})))].sort(); const messageCounts = Object.fromEntries( [...items.reduce((map, item) => map.set(item.message_id_hex, (map.get(item.message_id_hex) ?? 0) + 1), new Map())] .sort(([a], [b]) => a.localeCompare(b)), ); if (items.length !== total) throw new Error(`row count mismatch: fetched=${items.length} total=${total}`); if (frameIds.size !== items.length) throw new Error(`duplicate frame_id: unique=${frameIds.size} rows=${items.length}`); if (eventIds.size !== items.length) throw new Error(`duplicate event_id: unique=${eventIds.size} rows=${items.length}`); for (const item of items) { if (item.protocol !== "GB32960" || item.vin !== vin) throw new Error(`scope mismatch at ${item.frame_id}`); if (item.ts < dateFrom || item.ts > dateTo) throw new Error(`time out of range at ${item.frame_id}: ${item.ts}`); } const output = { exportedAt: new Date().toISOString(), source: `${baseUrl}/api/history/raw-frames`, query: { plate, vin, protocol: "GB32960", dateFrom, dateTo, orderBy: "eventTime" }, verification: { apiTotal: total, fetchedRows: items.length, uniqueFrameIds: frameIds.size, uniqueEventIds: eventIds.size, earliestTs: items.reduce((min, item) => !min || item.ts < min ? item.ts : min, ""), latestTs: items.reduce((max, item) => !max || item.ts > max ? item.ts : max, ""), parsedFieldCount: fieldKeys.length, messageCounts, }, fieldKeys, items, }; await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, JSON.stringify(output), "utf8"); console.log(JSON.stringify({ outputPath, ...output.verification }, null, 2));