chore: snapshot production code before Apple Design UI refinement

This commit is contained in:
lingniu
2026-09-17 18:05:57 +08:00
parent 9f0a87f49e
commit f7e7176f88
90 changed files with 8990 additions and 328 deletions
@@ -0,0 +1,364 @@
import fs from "node:fs/promises";
import path from "node:path";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
const taskDir = process.env.GB32960_TASK_DIR || "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/gb32960-export-20260817";
const outputDir = process.env.GB32960_OUTPUT_DIR || "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/gb32960-export-20260817-agp4377";
const data = JSON.parse(await fs.readFile(path.join(taskDir, "raw-frames.json"), "utf8"));
const exportDate = data.query.dateFrom.slice(0, 10);
const outputPath = path.join(outputDir, `${data.query.plate}_${exportDate}_GB32960原始数据_中文字段.xlsx`);
const categoryNames = {
alarm: "报警数据",
drive_motor: "驱动电机数据",
engine: "发动机数据",
extreme: "极值数据",
fuel_cell: "燃料电池数据",
gd_fc_air_conditioner: "广东燃料电池空调数据",
gd_fc_auxiliary: "广东燃料电池辅助系统数据",
gd_fc_dcdc: "广东燃料电池 DC/DC 数据",
gd_fc_demo_extension: "广东燃料电池演示扩展数据",
gd_fc_stack: "广东燃料电池电堆数据",
gd_fc_vehicle_info: "广东燃料电池车辆信息数据",
gd_fc_vendor_tlv: "广东燃料电池厂家 TLV 扩展",
header: "报文头",
platform: "平台信息",
position: "车辆位置数据",
temperature: "可充电储能装置温度数据",
vehicle: "整车数据",
voltage: "可充电储能装置电压数据",
};
const fieldNames = {
battery_faults: "可充电储能装置故障码",
engine_faults: "发动机故障码",
general_alarm_flag: "通用报警标志",
max_alarm_level: "最高报警等级",
motor_faults: "驱动电机故障码",
other_faults: "其他故障码",
type: "数据单元类型",
count: "数量",
controller_current_a: "控制器直流母线电流",
controller_temperature_c: "控制器温度",
controller_voltage_v: "控制器输入电压",
motor_temperature_c: "电机温度",
serial_no: "驱动电机序号",
speed_rpm: "转速",
state: "状态",
torque_nm: "转矩",
engine_status: "发动机状态",
fuel_rate: "燃料消耗率",
max_temp_c: "最高温度",
max_temp_probe_no: "最高温度探针序号",
max_temp_subsystem_no: "最高温度子系统号",
max_voltage_cell_no: "最高电压单体序号",
max_voltage_subsystem_no: "最高电压子系统号",
max_voltage_v: "最高电压",
min_temp_c: "最低温度",
min_temp_probe_no: "最低温度探针序号",
min_temp_subsystem_no: "最低温度子系统号",
min_voltage_cell_no: "最低电压单体序号",
min_voltage_subsystem_no: "最低电压子系统号",
min_voltage_v: "最低电压",
dc_dc_status: "高压 DC/DC 状态",
fuel_cell_current_a: "燃料电池电流",
fuel_cell_voltage_v: "燃料电池电压",
hydrogen_consumption_kg_per_100km: "氢耗",
max_hydrogen_concentration_fraction: "最高氢浓度比例",
max_hydrogen_concentration_percent: "最高氢浓度",
max_hydrogen_concentration_ppm: "最高氢浓度原始值",
max_hydrogen_concentration_probe_id: "最高氢浓度探针编号",
max_hydrogen_pressure_mpa: "最高氢压力",
max_hydrogen_pressure_probe_id: "最高氢压探针编号",
max_hydrogen_temperature_c: "氢系统最高温度",
max_hydrogen_temperature_probe_id: "最高氢温探针编号",
temperature_probe_count: "温度探针数量",
temperature_probe_values_c: "温度探针值列表",
status: "状态",
subsystem_count: "子系统数量",
air_compressor_motor_voltage_v: "空气压缩机电机电压",
air_compressor_power_kw: "空气压缩机功率",
low_voltage_battery_voltage_v: "低压蓄电池电压",
water_pump_voltage_v: "水泵电压",
controller_temp_c: "控制器温度",
input_current_a: "输入电流",
input_voltage_v: "输入电压",
output_current_a: "输出电流",
output_voltage_v: "输出电压",
air_compressor_current_a: "空气压缩机电流",
air_compressor_voltage_v: "空气压缩机电压",
declared_length: "声明长度",
hydrogen_pump_current_a: "氢气循环泵电流",
hydrogen_pump_voltage_v: "氢气循环泵电压",
stack_temp_c: "电堆温度",
air_inlet_pressure_kpa: "空气入口压力",
avg_cell_voltage_v: "单体平均电压",
cell_count: "单体总数",
engine_work_state: "发动机工作状态",
frame_cell_voltages_v: "本帧单体电压列表",
hydrogen_inlet_pressure_kpa: "氢气入口压力",
max_cell_voltage_id: "最高电压单体编号",
max_cell_voltage_v: "单体最高电压",
min_cell_voltage_id: "最低电压单体编号",
min_cell_voltage_v: "单体最低电压",
stack_count: "电堆数量",
stack_water_outlet_temp_c: "电堆出水温度",
collision_alarm: "碰撞报警",
hydrogen_mass_kg: "车载氢量",
payload_hex: "原始载荷(十六进制)",
actual_body_length: "实际报文体长度",
body_length: "报文体长度",
command: "命令标识",
encrypt: "加密方式",
response_flag: "应答标志",
version: "协议版本",
vin: "车辆识别代号",
platform_name: "平台名称",
latitude: "纬度",
longitude: "经度",
position_status: "定位状态标志",
accelerator_pct: "加速踏板行程",
brake_pct: "制动踏板状态",
charge_status: "充电状态",
gear: "挡位原始值",
insulation_kohm: "绝缘电阻",
running_mode: "运行模式",
soc_percent: "SOC",
speed_kmh: "车速",
total_current_a: "总电流",
total_mileage_km: "累计里程",
total_voltage_v: "总电压",
vehicle_status: "车辆状态",
max_cell_v: "单体最高电压",
min_cell_v: "单体最低电压",
};
const unitBySuffix = {
controller_current_a: "A", controller_voltage_v: "V", controller_temperature_c: "℃",
motor_temperature_c: "℃", speed_rpm: "rpm", torque_nm: "N·m", fuel_rate: "L/100km",
max_temp_c: "℃", min_temp_c: "℃", max_voltage_v: "V", min_voltage_v: "V",
fuel_cell_current_a: "A", fuel_cell_voltage_v: "V", hydrogen_consumption_kg_per_100km: "kg/100km",
max_hydrogen_concentration_fraction: "比例", max_hydrogen_concentration_percent: "%", max_hydrogen_concentration_ppm: "ppm",
max_hydrogen_pressure_mpa: "MPa", max_hydrogen_temperature_c: "℃", temperature_probe_values_c: "℃",
air_compressor_motor_voltage_v: "V", air_compressor_power_kw: "kW", low_voltage_battery_voltage_v: "V", water_pump_voltage_v: "V",
controller_temp_c: "℃", input_current_a: "A", input_voltage_v: "V", output_current_a: "A", output_voltage_v: "V",
air_compressor_current_a: "A", air_compressor_voltage_v: "V", hydrogen_pump_current_a: "A", hydrogen_pump_voltage_v: "V", stack_temp_c: "℃",
air_inlet_pressure_kpa: "kPa", avg_cell_voltage_v: "V", frame_cell_voltages_v: "V", hydrogen_inlet_pressure_kpa: "kPa",
max_cell_voltage_v: "V", min_cell_voltage_v: "V", stack_water_outlet_temp_c: "℃", hydrogen_mass_kg: "kg",
latitude: "°", longitude: "°", accelerator_pct: "%", brake_pct: "%", insulation_kohm: "kΩ", soc_percent: "%",
speed_kmh: "km/h", total_current_a: "A", total_mileage_km: "km", total_voltage_v: "V", max_cell_v: "V", min_cell_v: "V",
};
const valueMappings = {
"gb32960.vehicle.vehicle_status": { "1": "启动", "2": "熄火", "3": "其他", "254": "异常", "255": "无效" },
"gb32960.vehicle.charge_status": { "1": "停车充电", "2": "行驶充电", "3": "未充电", "4": "充电完成", "254": "异常", "255": "无效" },
"gb32960.vehicle.running_mode": { "1": "纯电", "2": "混合动力", "3": "燃料电池", "254": "异常", "255": "无效" },
"gb32960.vehicle.dc_dc_status": { "1": "工作", "2": "断开", "254": "异常", "255": "无效" },
"gb32960.fuel_cell.dc_dc_status": { "1": "工作", "2": "断开", "254": "异常", "255": "无效" },
"gb32960.drive_motor.motors.motor_1.state": { "1": "耗电", "2": "发电", "3": "关闭", "4": "准备", "254": "异常", "255": "无效" },
"gb32960.alarm.max_alarm_level": { "0": "无故障", "1": "一级故障", "2": "二级故障", "3": "三级故障" },
"gb32960.gd_fc_air_conditioner.status": { "0": "关闭", "1": "启动", "254": "异常", "255": "无效" },
"gb32960.gd_fc_vehicle_info.collision_alarm": { "0": "无碰撞报警", "1": "有碰撞报警", "254": "异常", "255": "无效" },
"gb32960.position.position_status": {
"0": "有效定位 · 北纬 · 东经", "1": "无效定位 · 北纬 · 东经", "2": "有效定位 · 南纬 · 东经", "3": "无效定位 · 南纬 · 东经",
"4": "有效定位 · 北纬 · 西经", "5": "无效定位 · 北纬 · 西经", "6": "有效定位 · 南纬 · 西经", "7": "无效定位 · 南纬 · 西经",
},
};
function fieldParts(key) {
const parts = key.split(".");
const category = parts[1] ?? "";
const suffix = parts.at(-1) ?? key;
const motorMatch = key.match(/\.motors\.motor_(\d+)\./);
const subsystemMatch = key.match(/\.subsystems\.subsystem_(\d+)\./);
const scope = motorMatch ? `电机${motorMatch[1]}-` : subsystemMatch ? `子系统${subsystemMatch[1]}-` : "";
const categoryName = categoryNames[category] ?? "GB/T 32960 扩展数据";
const label = fieldNames[suffix];
if (!label) throw new Error(`missing Chinese field mapping for ${key}`);
const unit = unitBySuffix[suffix] ?? "";
return { category, categoryName, suffix, label: `${categoryName}-${scope}${label}${unit ? `${unit}` : ""}`, unit };
}
function displayValue(key, value) {
if (value == null || value === "") return null;
const raw = String(value);
const mapped = valueMappings[key]?.[raw];
if (mapped) return `${mapped}${raw}`;
if (/^0x[0-9a-f]+$/i.test(raw)) return `十六进制 ${raw}`;
if (/^-?\d+(?:\.\d+)?$/.test(raw) && !key.endsWith(".vin")) return Number(raw);
return raw;
}
function mappingText(key) {
const mapping = valueMappings[key];
return mapping ? Object.entries(mapping).map(([value, label]) => `${value}=${label}`).join("") : "";
}
function columnName(index) {
let value = index + 1;
let out = "";
while (value > 0) {
const remainder = (value - 1) % 26;
out = String.fromCharCode(65 + remainder) + out;
value = Math.floor((value - 1) / 26);
}
return out;
}
const fieldKeys = data.fieldKeys;
const mappedFields = fieldKeys.map((key) => ({ key, ...fieldParts(key) }));
const metadata = [
["序号", (item, index) => index + 1],
["车牌", () => data.query.plate],
["VIN", (item) => item.vin],
["协议", (item) => item.protocol],
["设备时间(北京时间)", (item) => item.event_time],
["接收时间(北京时间)", (item) => item.received_at],
["存储时间(北京时间)", (item) => item.ts],
["消息ID", (item) => item.message_id],
["消息ID(十六进制)", (item) => item.message_id_hex ? `十六进制 ${item.message_id_hex}` : null],
["帧ID", (item) => item.frame_id],
["事件ID", (item) => item.event_id],
["解析状态", (item) => item.parse_status],
["解析错误", (item) => item.parse_error ?? null],
["来源端点", (item) => item.source_endpoint],
["原始报文字节数", (item) => item.raw_size_bytes],
["车辆标识", (item) => item.vehicle_key],
["手机号", (item) => item.phone ?? null],
["设备ID", (item) => item.device_id ?? null],
["原始文本", (item) => item.raw_text ?? null],
["原始报文(十六进制)", (item) => item.raw_hex ?? null],
];
const workbook = Workbook.create();
const summary = workbook.worksheets.add("导出说明");
const detail = workbook.worksheets.add("完整数据");
const mapping = workbook.worksheets.add("字段映射");
summary.showGridLines = false;
summary.getRange("A1:F1").merge();
summary.getRange("A1").values = [[`${data.query.plate} · ${exportDate} · GB/T 32960 原始数据导出`]];
summary.getRange("A1:F1").format = { fill: "#163A5F", font: { bold: true, color: "#FFFFFF", size: 16 }, verticalAlignment: "center" };
summary.getRange("A1:F1").format.rowHeight = 32;
summary.getRange("A3:B15").values = [
["车牌", data.query.plate],
["VIN", data.query.vin],
["协议", data.query.protocol],
["查询日期", `${exportDate}(北京时间)`],
["查询时间窗", `${data.query.dateFrom}${data.query.dateTo}`],
["接口统计总帧数", data.verification.apiTotal],
["实际导出帧数", data.verification.fetchedRows],
["唯一帧ID数", data.verification.uniqueFrameIds],
["唯一事件ID数", data.verification.uniqueEventIds],
["首条存储时间", data.verification.earliestTs],
["末条存储时间", data.verification.latestTs],
["解析字段数", data.verification.parsedFieldCount],
["消息类型统计", Object.entries(data.verification.messageCounts).map(([key, count]) => `${key}: ${count}`).join("")],
];
summary.getRange("A3:A15").format = { fill: "#DCE8F2", font: { bold: true, color: "#163A5F" } };
summary.getRange("A3:B15").format.borders = { preset: "inside", style: "thin", color: "#C8D4E0" };
summary.getRange("A17:F17").merge();
summary.getRange("A17").values = [["口径说明"]];
summary.getRange("A17:F17").format = { fill: "#2B6F91", font: { bold: true, color: "#FFFFFF" } };
summary.getRange("A18:F22").merge(true);
summary.getRange("A18:F22").values = [
[`1. 数据源为生产实时 API 的 GB32960 原始帧历史;导出保留原始报文十六进制、帧/事件标识、解析状态及全部 ${data.verification.parsedFieldCount} 个已出现的解析字段。`],
["2. 完整数据工作表使用中文字段名;字段映射工作表保留原始字段键、单位及状态值映射,便于审计和程序回溯。"],
["3. 状态类字段显示为“中文含义(协议值)”;普通测量值保持数值,列表/位图/十六进制载荷保持原始文本。"],
[`4. 查询边界为北京时间 ${data.query.dateFrom}${data.query.dateTo};该车辆实际存储时间覆盖 ${data.verification.earliestTs}${data.verification.latestTs}`],
["5. 中文术语参考:项目 GB/T 32960 字段展示基准及【字段】32960+广东燃料电池协议.jsonSHA-256: 0f0847b86cc692f2130255babbee37e6cb3cf36d717b18d6698164289f66eb82)。"],
];
summary.getRange("A18:F22").format = { wrapText: true, verticalAlignment: "top", font: { color: "#334155", size: 10 } };
summary.getRange("A18:F22").format.rowHeight = 34;
summary.getRange("A:A").format.columnWidth = 22;
summary.getRange("B:F").format.columnWidth = 18;
summary.getRange("B:B").format.columnWidth = 78;
const detailHeaders = [...metadata.map(([label]) => label), ...mappedFields.map((field) => field.label)];
const detailRows = data.items.map((item, index) => [
...metadata.map(([, getter]) => getter(item, index)),
...mappedFields.map(({ key }) => displayValue(key, item.parsed_fields?.[key])),
]);
const detailMatrix = [detailHeaders, ...detailRows];
const detailEndColumn = columnName(detailHeaders.length - 1);
detail.getRangeByIndexes(0, 0, detailMatrix.length, detailHeaders.length).values = detailMatrix;
detail.showGridLines = false;
detail.freezePanes.freezeRows(1);
detail.freezePanes.freezeColumns(3);
const dataTable = detail.tables.add(`A1:${detailEndColumn}${detailMatrix.length}`, true, "GB32960DataTable");
dataTable.style = "TableStyleMedium2";
dataTable.showFilterButton = true;
dataTable.showBandedColumns = false;
detail.getRangeByIndexes(0, 0, 1, detailHeaders.length).format = {
fill: "#1F4E78", font: { bold: true, color: "#FFFFFF", size: 9 }, wrapText: true,
horizontalAlignment: "center", verticalAlignment: "center",
};
detail.getRangeByIndexes(0, 0, 1, detailHeaders.length).format.rowHeight = 48;
detail.getRangeByIndexes(1, 0, detailRows.length, detailHeaders.length).format.font = { size: 9, color: "#1F2937" };
detail.getRangeByIndexes(0, 0, detailMatrix.length, 1).format.columnWidth = 8;
detail.getRangeByIndexes(0, 1, detailMatrix.length, 1).format.columnWidth = 13;
detail.getRangeByIndexes(0, 2, detailMatrix.length, 1).format.columnWidth = 22;
for (const col of [3, 7, 8, 11, 14]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 14;
for (const col of [4, 5, 6]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 21;
for (const col of [9, 10]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 36;
for (const col of [12, 13, 15, 16, 17]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 20;
detail.getRangeByIndexes(0, 18, detailMatrix.length, 1).format.columnWidth = 28;
detail.getRangeByIndexes(0, 19, detailMatrix.length, 1).format.columnWidth = 44;
for (let col = metadata.length; col < detailHeaders.length; col += 1) {
const key = mappedFields[col - metadata.length].key;
const wide = key.includes("voltages_v") || key.endsWith("faults") || key.endsWith("payload_hex");
detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = wide ? 38 : 18;
}
detail.getRangeByIndexes(1, 0, detailRows.length, 1).format.numberFormat = "0";
detail.getRangeByIndexes(1, 7, detailRows.length, 1).format.numberFormat = "0";
detail.getRangeByIndexes(1, 14, detailRows.length, 1).format.numberFormat = "0";
mapping.showGridLines = false;
mapping.freezePanes.freezeRows(1);
const mappingHeaders = ["序号", "中文字段名", "原始字段键", "数据类别", "单位", "状态值映射", "说明"];
const mappingRows = mappedFields.map((field, index) => [
index + 1,
field.label,
field.key,
field.categoryName,
field.unit,
mappingText(field.key),
valueMappings[field.key] ? "完整数据中显示中文含义并保留括号内协议值。" : "值按协议解析结果原样导出;数值字段保持数值类型。",
]);
mapping.getRangeByIndexes(0, 0, mappingRows.length + 1, mappingHeaders.length).values = [mappingHeaders, ...mappingRows];
const mappingTable = mapping.tables.add(`A1:G${mappingRows.length + 1}`, true, "GB32960FieldMappingTable");
mappingTable.style = "TableStyleMedium2";
mapping.getRange("A1:G1").format = { fill: "#1F4E78", font: { bold: true, color: "#FFFFFF" }, wrapText: true, horizontalAlignment: "center" };
mapping.getRange("A1:G1").format.rowHeight = 30;
mapping.getRange("A:A").format.columnWidth = 8;
mapping.getRange("B:B").format.columnWidth = 42;
mapping.getRange("C:C").format.columnWidth = 58;
mapping.getRange("D:D").format.columnWidth = 30;
mapping.getRange("E:E").format.columnWidth = 12;
mapping.getRange("F:F").format.columnWidth = 68;
mapping.getRange("G:G").format.columnWidth = 48;
mapping.getRange(`B2:G${mappingRows.length + 1}`).format.wrapText = true;
await fs.mkdir(outputDir, { recursive: true });
const summaryInspect = await workbook.inspect({ kind: "table", range: "导出说明!A1:F22", include: "values,formulas", tableMaxRows: 24, tableMaxCols: 8 });
const detailInspect = await workbook.inspect({ kind: "table", range: "完整数据!A1:Z8", include: "values,formulas", tableMaxRows: 8, tableMaxCols: 26 });
const mappingInspect = await workbook.inspect({ kind: "table", range: "字段映射!A1:G16", include: "values,formulas", tableMaxRows: 16, tableMaxCols: 8 });
const errors = await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "final formula error scan" });
await fs.writeFile(path.join(taskDir, "summary-inspect.ndjson"), summaryInspect.ndjson, "utf8");
await fs.writeFile(path.join(taskDir, "detail-inspect.ndjson"), detailInspect.ndjson, "utf8");
await fs.writeFile(path.join(taskDir, "mapping-inspect.ndjson"), mappingInspect.ndjson, "utf8");
await fs.writeFile(path.join(taskDir, "formula-errors.ndjson"), errors.ndjson, "utf8");
for (const [sheetName, range, filename] of [
["导出说明", "A1:F22", "preview-summary.png"],
["完整数据", "A1:T18", "preview-detail.png"],
["字段映射", "A1:G28", "preview-mapping.png"],
]) {
const preview = await workbook.render({ sheetName, range, scale: 1.25, format: "png" });
await fs.writeFile(path.join(taskDir, filename), new Uint8Array(await preview.arrayBuffer()));
}
const xlsx = await SpreadsheetFile.exportXlsx(workbook);
await xlsx.save(outputPath);
console.log(JSON.stringify({ outputPath, sheets: ["导出说明", "完整数据", "字段映射"], rows: detailRows.length, parsedFields: mappedFields.length, columns: detailHeaders.length }, null, 2));
@@ -0,0 +1,77 @@
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));
@@ -0,0 +1,15 @@
import fs from "node:fs/promises";
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
const workbookPath = process.env.GB32960_WORKBOOK_PATH || "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/gb32960-export-20260817-agp4377/粤AGP4377_2026-08-17_GB32960完整数据_中文字段.xlsx";
const lastDataRow = Number(process.env.GB32960_LAST_ROW || "1233");
const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(workbookPath));
const sheets = await workbook.inspect({ kind: "sheet", include: "id,name", maxChars: 3000 });
const summary = await workbook.inspect({ kind: "table", range: "导出说明!A3:B15", include: "values,formulas", tableMaxRows: 20, tableMaxCols: 3 });
const detail = await workbook.inspect({ kind: "table", range: "完整数据!A1:T6", include: "values,formulas", tableMaxRows: 6, tableMaxCols: 20 });
const tail = await workbook.inspect({ kind: "table", range: `完整数据!A${lastDataRow - 4}:F${lastDataRow}`, include: "values,formulas", tableMaxRows: 6, tableMaxCols: 6 });
const mapping = await workbook.inspect({ kind: "table", range: "字段映射!A1:G10", include: "values,formulas", tableMaxRows: 10, tableMaxCols: 7 });
const errors = await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "reopened workbook formula error scan" });
const output = { sheets: sheets.ndjson, summary: summary.ndjson, detail: detail.ndjson, tail: tail.ndjson, mapping: mapping.ndjson, errors: errors.ndjson };
await fs.writeFile(new URL("./reopen-verification.json", import.meta.url), JSON.stringify(output, null, 2), "utf8");
console.log(JSON.stringify({ workbookPath, sizeBytes: (await fs.stat(workbookPath)).size, output }, null, 2));