chore: snapshot production code before Apple Design UI refinement
This commit is contained in:
@@ -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+广东燃料电池协议.json(SHA-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));
|
||||
Reference in New Issue
Block a user