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));