chore: snapshot production code before Apple Design UI refinement
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
|
||||
|
||||
const evidencePath = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/mileage-appeal-20260810/evidence.json";
|
||||
const outputDir = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/019fe9b9-eb60-7d22-8c0a-c70851a48083";
|
||||
const outputPath = path.join(outputDir, "15辆车里程核验申诉材料_GB32960日里程及原始报文.xlsx");
|
||||
const previewDir = path.join(outputDir, "preview");
|
||||
const data = JSON.parse(await fs.readFile(evidencePath, "utf8"));
|
||||
|
||||
const workbook = Workbook.create();
|
||||
const summary = workbook.worksheets.add("申诉总览");
|
||||
const notes = workbook.worksheets.add("口径与证据说明");
|
||||
const vehicleSheets = new Map();
|
||||
for (const vehicle of data.vehicles) {
|
||||
const sheetName = `${String(vehicle.seq).padStart(2, "0")}-${vehicle.plate}`;
|
||||
vehicleSheets.set(vehicle.vin, { sheetName, sheet: workbook.worksheets.add(sheetName) });
|
||||
}
|
||||
|
||||
const palette = {
|
||||
navy: "#1F4E78",
|
||||
blue: "#D9EAF7",
|
||||
blue2: "#EAF3F8",
|
||||
yellow: "#FFF2CC",
|
||||
amber: "#FCE4D6",
|
||||
mileageHeader: "#C65911",
|
||||
mileageLight: "#FFF2CC",
|
||||
mileageStrong: "#FFD966",
|
||||
green: "#E2F0D9",
|
||||
gray: "#E7E6E6",
|
||||
dark: "#1F2937",
|
||||
muted: "#5B6573",
|
||||
border: "#B7C9D6",
|
||||
white: "#FFFFFF",
|
||||
};
|
||||
|
||||
const thinBorders = { preset: "all", style: "thin", color: palette.border };
|
||||
const titleFormat = {
|
||||
fill: palette.navy,
|
||||
font: { bold: true, color: palette.white, fontSize: 16, typeface: "Microsoft YaHei" },
|
||||
horizontalAlignment: "left",
|
||||
verticalAlignment: "center",
|
||||
};
|
||||
const sectionFormat = {
|
||||
fill: palette.blue,
|
||||
font: { bold: true, color: palette.dark, fontSize: 11, typeface: "Microsoft YaHei" },
|
||||
horizontalAlignment: "left",
|
||||
verticalAlignment: "center",
|
||||
borders: thinBorders,
|
||||
};
|
||||
const headerFormat = {
|
||||
fill: palette.navy,
|
||||
font: { bold: true, color: palette.white, fontSize: 9, typeface: "Microsoft YaHei" },
|
||||
horizontalAlignment: "center",
|
||||
verticalAlignment: "center",
|
||||
wrapText: true,
|
||||
borders: thinBorders,
|
||||
};
|
||||
const labelFormat = {
|
||||
fill: palette.blue2,
|
||||
font: { bold: true, color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" },
|
||||
horizontalAlignment: "left",
|
||||
verticalAlignment: "center",
|
||||
wrapText: true,
|
||||
borders: thinBorders,
|
||||
};
|
||||
const valueFormat = {
|
||||
font: { color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" },
|
||||
verticalAlignment: "center",
|
||||
wrapText: true,
|
||||
borders: thinBorders,
|
||||
};
|
||||
|
||||
function asDate(day) {
|
||||
return new Date(`${day}T00:00:00Z`);
|
||||
}
|
||||
|
||||
function addDays(day, count) {
|
||||
const date = asDate(day);
|
||||
date.setUTCDate(date.getUTCDate() + count);
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function calendarDays(start, end) {
|
||||
const days = [];
|
||||
for (let day = start; day <= end; day = addDays(day, 1)) days.push(day);
|
||||
return days;
|
||||
}
|
||||
|
||||
function absDelta(a, b) {
|
||||
if (a == null || b == null || Number.isNaN(Number(a)) || Number.isNaN(Number(b))) return null;
|
||||
return Math.abs(Number(a) - Number(b));
|
||||
}
|
||||
|
||||
function rawRange(vehicle) {
|
||||
const rows = vehicle.rawEvidence.filter((row) => row.status === "OK");
|
||||
if (rows.length === 0) return "无";
|
||||
return `${rows[0].day} 至 ${rows.at(-1).day}`;
|
||||
}
|
||||
|
||||
function evidenceLevel(metric, raw) {
|
||||
if (raw?.status === "OK") return "A-原始32960报文";
|
||||
if (metric?.source_code === "legacy_lingniu_prod") return "C-历史日统计迁移(无RAW)";
|
||||
if (metric) return "B-中台32960日统计(无RAW)";
|
||||
return "D-无中台日记录";
|
||||
}
|
||||
|
||||
function rowNote(metric, raw) {
|
||||
const notes = [];
|
||||
if (metric?.source_code === "legacy_lingniu_prod") notes.push("历史生产库 ln_vehicle_day_mileage 迁移记录,不是原始帧");
|
||||
if (raw?.status === "OK" && !metric) notes.push("有原始报文,但中台日统计无记录");
|
||||
if (raw?.status === "OK" && metric && absDelta(metric.latest_total_mileage_km, raw.parsedTotalMileageKm) > 0.1) {
|
||||
notes.push("日统计日末值与本日最后可得RAW读数不一致,详见数据源/时间;可能涉及人工源选举或跨日延迟");
|
||||
}
|
||||
if (raw?.status !== "OK" && metric && metric.source_code !== "legacy_lingniu_prod") notes.push("有中台日统计,但当日未检出含仪表总里程的RAW帧");
|
||||
if (!metric && raw?.status !== "OK") notes.push("当日无中台日记录,且现行RAW历史不可用或未检出");
|
||||
return notes.join(";");
|
||||
}
|
||||
|
||||
function setStandardSheet(sheet) {
|
||||
sheet.showGridLines = false;
|
||||
sheet.freezePanes.freezeRows(13);
|
||||
sheet.freezePanes.freezeColumns(1);
|
||||
}
|
||||
|
||||
for (const vehicle of data.vehicles) {
|
||||
const { sheetName, sheet } = vehicleSheets.get(vehicle.vin);
|
||||
setStandardSheet(sheet);
|
||||
sheet.getRange("A1:P1").merge();
|
||||
sheet.getRange("A1").values = [[`${vehicle.plate} 里程核验申诉明细`]];
|
||||
sheet.getRange("A1:P1").format = titleFormat;
|
||||
sheet.getRange("A1:P1").format.rowHeight = 30;
|
||||
|
||||
sheet.getRange("A2:P2").merge();
|
||||
sheet.getRange("A2").values = [["核验口径:GB/T 32960 车辆仪表累计总里程;原始报文列保存完整HEX,点击单元格可在公式栏复制。相邻自然日均有RAW读数时才计算RAW日差。"]];
|
||||
sheet.getRange("A2:P2").format = { fill: palette.yellow, font: { color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" }, wrapText: true, verticalAlignment: "center", borders: thinBorders };
|
||||
sheet.getRange("A2:P2").format.rowHeight = 30;
|
||||
|
||||
sheet.getRange("A4:P6").format = valueFormat;
|
||||
sheet.getRange("A4").values = [["车牌"]];
|
||||
sheet.getRange("B4:C4").merge(); sheet.getRange("B4").values = [[vehicle.plate]];
|
||||
sheet.getRange("D4").values = [["VIN"]];
|
||||
sheet.getRange("E4:H4").merge(); sheet.getRange("E4").values = [[vehicle.vin]];
|
||||
sheet.getRange("I4").values = [["车型"]];
|
||||
sheet.getRange("J4:K4").merge(); sheet.getRange("J4").values = [[vehicle.model]];
|
||||
sheet.getRange("L4").values = [["协议"]];
|
||||
sheet.getRange("M4:P4").merge(); sheet.getRange("M4").values = [["GB32960(仪表累计总里程)"]];
|
||||
|
||||
sheet.getRange("A5").values = [["核验区间"]];
|
||||
sheet.getRange("B5:C5").merge(); sheet.getRange("B5").values = [["2025-11-28 至 2026-07-31"]];
|
||||
sheet.getRange("D5").values = [["原TBOX合计(km)"]];
|
||||
sheet.getRange("E5:F5").merge(); sheet.getRange("E5").values = [[vehicle.sourceTboxKm]];
|
||||
sheet.getRange("G5").values = [["第三方平台合计(km)"]];
|
||||
sheet.getRange("H5:I5").merge(); sheet.getRange("H5").values = [[vehicle.platformKm]];
|
||||
sheet.getRange("J5").values = [["原表损耗(km)"]];
|
||||
sheet.getRange("K5:L5").merge(); sheet.getRange("K5").values = [[vehicle.lossKm]];
|
||||
sheet.getRange("M5").values = [["原申诉里程(km)"]];
|
||||
sheet.getRange("N5:P5").merge(); sheet.getRange("N5").values = [[vehicle.appealKm]];
|
||||
sheet.getRange("E5:F5").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("H5:I5").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("K5:L5").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("N5:P5").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("K5:L5").format.fill = palette.yellow;
|
||||
|
||||
sheet.getRange("A6").values = [["异常标记"]];
|
||||
sheet.getRange("B6:D6").merge(); sheet.getRange("B6").values = [[vehicle.anomaly]];
|
||||
sheet.getRange("E6").values = [["原排查结论"]];
|
||||
sheet.getRange("F6:P6").merge(); sheet.getRange("F6").values = [[vehicle.note]];
|
||||
for (const address of ["A4", "A5", "A6", "D4", "D5", "E6", "G5", "I4", "J5", "L4", "M5"]) sheet.getRange(address).format = labelFormat;
|
||||
|
||||
sheet.getRange("A8:P8").format = valueFormat;
|
||||
sheet.getRange("A8").values = [["原车辆统计合计(km)"]];
|
||||
sheet.getRange("B8:C8").merge(); sheet.getRange("B8").values = [[vehicle.sourceTboxKm]];
|
||||
sheet.getRange("D8").values = [["车辆数据中台统计合计(km)"]];
|
||||
sheet.getRange("E8:F8").merge(); sheet.getRange("E8").formulas = [["=SUM(B14:B259)"]];
|
||||
sheet.getRange("G8").values = [["中台日统计有值天数"]];
|
||||
sheet.getRange("H8:I8").merge(); sheet.getRange("H8").formulas = [["=COUNT(B14:B259)"]];
|
||||
sheet.getRange("J8").values = [["RAW仪表证据天数"]];
|
||||
sheet.getRange("K8:L8").merge(); sheet.getRange("K8").formulas = [["=COUNT(F14:F259)"]];
|
||||
sheet.getRange("M8").values = [["RAW证据日期范围"]];
|
||||
sheet.getRange("N8:P8").merge(); sheet.getRange("N8").values = [[rawRange(vehicle)]];
|
||||
for (const address of ["A8", "D8", "G8", "J8", "M8"]) sheet.getRange(address).format = labelFormat;
|
||||
sheet.getRange("B8:C8").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("E8:F8").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("B8:C8").format.fill = palette.blue2;
|
||||
sheet.getRange("E8:F8").format.fill = palette.mileageStrong;
|
||||
sheet.getRange("E8:F8").format.font = { bold: true, color: "#7F6000", fontSize: 10, typeface: "Microsoft YaHei" };
|
||||
sheet.getRange("A8:P8").format.rowHeight = 38;
|
||||
|
||||
sheet.getRange("A9").values = [["证据限制"]];
|
||||
sheet.getRange("B9:P9").merge();
|
||||
sheet.getRange("B9").values = [["现行TDengine RAW历史自2026-07-01起可查询;此前仅有历史生产库日统计迁移记录。材料已按A/B/C/D分级,未将统计记录伪称为原始报文。"]];
|
||||
sheet.getRange("A9").format = labelFormat;
|
||||
sheet.getRange("B9:P9").format = { ...valueFormat, fill: palette.amber };
|
||||
sheet.getRange("A9:P9").format.rowHeight = 30;
|
||||
|
||||
sheet.getRange("A11:P11").merge();
|
||||
sheet.getRange("A11").values = [["每日核验明细(完整日历)"]];
|
||||
sheet.getRange("A11:P11").format = sectionFormat;
|
||||
|
||||
sheet.getRange("A12:D12").merge();
|
||||
sheet.getRange("A12").values = [["车辆数据中台区间里程总计"]];
|
||||
sheet.getRange("E12:F12").merge();
|
||||
sheet.getRange("E12").formulas = [["=SUM(B14:B259)"]];
|
||||
sheet.getRange("G12:P12").merge();
|
||||
sheet.getRange("G12").values = [["计算范围:B14:B259(下方“中台当日里程(km)”列)"]];
|
||||
sheet.getRange("A12:D12").format = {
|
||||
...headerFormat,
|
||||
fill: palette.mileageHeader,
|
||||
horizontalAlignment: "left",
|
||||
};
|
||||
sheet.getRange("E12:F12").format = {
|
||||
...valueFormat,
|
||||
fill: palette.mileageStrong,
|
||||
font: { bold: true, color: "#7F6000", fontSize: 11, typeface: "Microsoft YaHei" },
|
||||
numberFormat: "#,##0.0",
|
||||
horizontalAlignment: "right",
|
||||
};
|
||||
sheet.getRange("G12:P12").format = {
|
||||
...valueFormat,
|
||||
fill: palette.mileageLight,
|
||||
font: { color: palette.textMuted, fontSize: 9, typeface: "Microsoft YaHei" },
|
||||
};
|
||||
sheet.getRange("A12:P12").format.rowHeight = 30;
|
||||
|
||||
const headers = ["日期", "中台当日里程(km)", "中台日末仪表总里程(km)", "日统计数据源", "日统计来源端点", "RAW解析仪表总里程(km)", "RAW相邻日差(km)", "日统计-RAW差(km)", "证据等级", "报文事件时间", "中台接收时间", "RAW来源端点", "Event ID", "Frame ID", "原始报文大小(bytes)", "原始GB32960报文HEX(完整)"];
|
||||
sheet.getRange("A13:P13").values = [headers];
|
||||
sheet.getRange("A13:P13").format = headerFormat;
|
||||
sheet.getRange("A13:P13").format.rowHeight = 42;
|
||||
|
||||
const metricByDay = new Map(vehicle.dailyMetrics.map((row) => [row.stat_date, row]));
|
||||
const rawByDay = new Map(vehicle.rawEvidence.map((row) => [row.day, row]));
|
||||
const days = calendarDays(data.period.start, data.period.end);
|
||||
const rows = days.map((day) => {
|
||||
const metric = metricByDay.get(day);
|
||||
const raw = rawByDay.get(day);
|
||||
return [
|
||||
asDate(day),
|
||||
metric?.daily_mileage_km ?? null,
|
||||
metric?.latest_total_mileage_km ?? null,
|
||||
metric ? `${metric.platform_name || ""} / ${metric.source_code || ""}` : "",
|
||||
metric?.latest_source_endpoint ?? "",
|
||||
raw?.status === "OK" ? raw.parsedTotalMileageKm : null,
|
||||
null,
|
||||
null,
|
||||
evidenceLevel(metric, raw),
|
||||
raw?.status === "OK" ? raw.eventTime : "",
|
||||
raw?.status === "OK" ? raw.receivedAt : "",
|
||||
raw?.status === "OK" ? raw.sourceEndpoint : "",
|
||||
raw?.status === "OK" ? raw.eventId : "",
|
||||
raw?.status === "OK" ? raw.frameId : "",
|
||||
raw?.status === "OK" ? raw.rawSizeBytes : null,
|
||||
raw?.status === "OK" ? raw.rawHex : "",
|
||||
];
|
||||
});
|
||||
sheet.getRange("A14:P259").values = rows;
|
||||
sheet.getRange("A14:A259").format.numberFormat = "yyyy-mm-dd";
|
||||
sheet.getRange("B14:C259").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("F14:H259").format.numberFormat = "#,##0.0";
|
||||
sheet.getRange("O14:O259").format.numberFormat = "#,##0";
|
||||
sheet.getRange("A14:P259").format = {
|
||||
font: { color: palette.dark, fontSize: 8, typeface: "Microsoft YaHei" },
|
||||
verticalAlignment: "center",
|
||||
borders: { insideHorizontal: { style: "thin", color: "#D9E2E8" }, bottom: { style: "thin", color: palette.border } },
|
||||
};
|
||||
sheet.getRange("A14:A259").format.horizontalAlignment = "center";
|
||||
sheet.getRange("B14:C259").format.horizontalAlignment = "right";
|
||||
sheet.getRange("F14:H259").format.horizontalAlignment = "right";
|
||||
sheet.getRange("I14:O259").format.wrapText = true;
|
||||
sheet.getRange("P14:P259").format = { font: { color: palette.muted, fontSize: 7, typeface: "Consolas" }, wrapText: true, verticalAlignment: "top" };
|
||||
sheet.getRange("A14:P259").format.rowHeight = 34;
|
||||
sheet.getRange("G14").values = [[null]];
|
||||
for (let row = 15; row <= 259; row += 1) {
|
||||
sheet.getRange(`G${row}`).formulas = [[`=IF(OR(F${row}="",F${row - 1}=""),"",F${row}-F${row - 1})`]];
|
||||
}
|
||||
for (let row = 14; row <= 259; row += 1) {
|
||||
sheet.getRange(`H${row}`).formulas = [[`=IF(OR(C${row}="",F${row}=""),"",C${row}-F${row})`]];
|
||||
}
|
||||
|
||||
sheet.getRange("I14:I259").conditionalFormats.add("containsText", { text: "A-", format: { fill: palette.green, font: { color: "#375623", bold: true } } });
|
||||
sheet.getRange("I14:I259").conditionalFormats.add("containsText", { text: "C-", format: { fill: palette.yellow, font: { color: "#7F6000" } } });
|
||||
sheet.getRange("I14:I259").conditionalFormats.add("containsText", { text: "D-", format: { fill: palette.gray, font: { color: palette.muted } } });
|
||||
sheet.getRange("H14:H259").conditionalFormats.addCustom("=ABS($H14)>0.1", { fill: palette.amber, font: { color: "#9C0006", bold: true } });
|
||||
|
||||
const table = sheet.tables.add("A13:P259", true, `Vehicle${String(vehicle.seq).padStart(2, "0")}Evidence`);
|
||||
table.style = "TableStyleMedium2";
|
||||
table.showBandedColumns = false;
|
||||
table.showFilterButton = true;
|
||||
sheet.getRange("B13").format = { ...headerFormat, fill: palette.mileageHeader };
|
||||
sheet.getRange("B14:B259").format.fill = palette.mileageLight;
|
||||
sheet.getRange("B14:B259").conditionalFormats.add("cellIs", {
|
||||
operator: "greaterThan",
|
||||
formula: 0,
|
||||
format: { fill: palette.mileageStrong, font: { bold: true, color: "#7F6000" } },
|
||||
});
|
||||
|
||||
const widths = [12, 17, 20, 24, 26, 20, 17, 18, 25, 20, 20, 22, 34, 34, 14, 72];
|
||||
for (let col = 0; col < widths.length; col += 1) sheet.getRangeByIndexes(0, col, 259, 1).format.columnWidth = widths[col];
|
||||
}
|
||||
|
||||
summary.showGridLines = false;
|
||||
summary.freezePanes.freezeRows(4);
|
||||
summary.getRange("A1:P1").merge();
|
||||
summary.getRange("A1").values = [["15辆车里程核验申诉总览"]];
|
||||
summary.getRange("A1:P1").format = titleFormat;
|
||||
summary.getRange("A1:P1").format.rowHeight = 32;
|
||||
summary.getRange("A2:P2").merge();
|
||||
summary.getRange("A2").values = [["结论:车辆数据中台GB32960日里程复核合计与原异常表TBOX区间里程逐车一致;第三方平台合计显著偏低。RAW原始帧按现有可查历史提供,早期记录已明确标注证据限制。"]];
|
||||
summary.getRange("A2:P2").format = { fill: palette.green, font: { bold: true, color: "#375623", fontSize: 10, typeface: "Microsoft YaHei" }, wrapText: true, verticalAlignment: "center", borders: thinBorders };
|
||||
summary.getRange("A2:P2").format.rowHeight = 34;
|
||||
|
||||
summary.getRange("A3:B3").merge(); summary.getRange("A3").values = [["全部15辆车·原车辆统计(km)"]];
|
||||
summary.getRange("C3").formulas = [["=F20"]];
|
||||
summary.getRange("D3:E3").merge(); summary.getRange("D3").values = [["车辆数据中台统计(km)"]];
|
||||
summary.getRange("F3").formulas = [["=G20"]];
|
||||
summary.getRange("G3:H3").merge(); summary.getRange("G3").values = [["第三方平台统计(km)"]];
|
||||
summary.getRange("I3").formulas = [["=H20"]];
|
||||
summary.getRange("J3:K3").merge(); summary.getRange("J3").values = [["平台漏计差异(km)"]];
|
||||
summary.getRange("L3").formulas = [["=I20"]];
|
||||
summary.getRange("M3:N3").merge(); summary.getRange("M3").values = [["RAW仪表证据(天)"]];
|
||||
summary.getRange("O3:P3").merge(); summary.getRange("O3").formulas = [["=M20"]];
|
||||
for (const address of ["A3:B3", "D3:E3", "G3:H3", "J3:K3", "M3:N3"]) summary.getRange(address).format = labelFormat;
|
||||
for (const address of ["C3", "F3", "I3", "L3", "O3:P3"]) summary.getRange(address).format = { ...valueFormat, fill: palette.mileageLight, font: { bold: true, color: "#7F6000", fontSize: 10, typeface: "Microsoft YaHei" }, horizontalAlignment: "right" };
|
||||
summary.getRange("C3:L3").format.numberFormat = "#,##0.0";
|
||||
summary.getRange("O3:P3").format.numberFormat = "#,##0";
|
||||
summary.getRange("A3:P3").format.rowHeight = 34;
|
||||
|
||||
const summaryHeaders = ["序号", "车型", "车牌", "VIN", "核验区间", "原TBOX合计(km)", "中台复核合计(km)", "第三方平台合计(km)", "损耗(km)", "损耗率", "申诉里程(km)", "日统计天数", "RAW证据天数", "RAW证据范围", "核验结论", "原排查结论"];
|
||||
summary.getRange("A4:P4").values = [summaryHeaders];
|
||||
summary.getRange("A4:P4").format = headerFormat;
|
||||
summary.getRange("A4:P4").format.rowHeight = 40;
|
||||
const summaryRows = data.vehicles.map((vehicle) => [
|
||||
vehicle.seq, vehicle.model, vehicle.plate, vehicle.vin, "2025-11-28 至 2026-07-31", vehicle.sourceTboxKm, null, vehicle.platformKm, null, null, vehicle.appealKm, null, null, rawRange(vehicle), null, vehicle.note,
|
||||
]);
|
||||
summary.getRange("A5:P19").values = summaryRows;
|
||||
for (let index = 0; index < data.vehicles.length; index += 1) {
|
||||
const row = 5 + index;
|
||||
const { sheetName } = vehicleSheets.get(data.vehicles[index].vin);
|
||||
summary.getRange(`G${row}`).formulas = [[`='${sheetName}'!E8`]];
|
||||
summary.getRange(`I${row}`).formulas = [[`=F${row}-H${row}`]];
|
||||
summary.getRange(`J${row}`).formulas = [[`=IF(F${row}=0,"",I${row}/F${row})`]];
|
||||
summary.getRange(`L${row}`).formulas = [[`='${sheetName}'!H8`]];
|
||||
summary.getRange(`M${row}`).formulas = [[`='${sheetName}'!K8`]];
|
||||
summary.getRange(`O${row}`).formulas = [[`=IF(ABS(G${row}-F${row})<0.1,"中台复核合计与原TBOX一致","需复核中台合计")`]];
|
||||
}
|
||||
summary.getRange("A20:E20").merge(); summary.getRange("A20").values = [["合计"]];
|
||||
summary.getRange("F20").formulas = [["=SUM(F5:F19)"]];
|
||||
summary.getRange("G20").formulas = [["=SUM(G5:G19)"]];
|
||||
summary.getRange("H20").formulas = [["=SUM(H5:H19)"]];
|
||||
summary.getRange("I20").formulas = [["=SUM(I5:I19)"]];
|
||||
summary.getRange("J20").formulas = [["=IF(F20=0,\"\",I20/F20)"]];
|
||||
summary.getRange("K20").formulas = [["=SUM(K5:K19)"]];
|
||||
summary.getRange("L20").formulas = [["=SUM(L5:L19)"]];
|
||||
summary.getRange("M20").formulas = [["=SUM(M5:M19)"]];
|
||||
summary.getRange("N20:P20").merge(); summary.getRange("N20").values = [["15车合计;RAW证据为现行历史可查询范围"]];
|
||||
summary.getRange("A20:P20").format = { fill: palette.blue, font: { bold: true, color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" }, borders: thinBorders, verticalAlignment: "center", wrapText: true };
|
||||
summary.getRange("A5:P19").format = { font: { color: palette.dark, fontSize: 9, typeface: "Microsoft YaHei" }, verticalAlignment: "center", wrapText: true, borders: { insideHorizontal: { style: "thin", color: "#D9E2E8" }, bottom: { style: "thin", color: palette.border } } };
|
||||
summary.getRange("F5:I20").format.numberFormat = "#,##0.0";
|
||||
summary.getRange("J5:J20").format.numberFormat = "0.00%";
|
||||
summary.getRange("K5:K20").format.numberFormat = "#,##0.0";
|
||||
summary.getRange("L5:M20").format.numberFormat = "#,##0";
|
||||
summary.getRange("I5:I19").format.fill = palette.yellow;
|
||||
summary.getRange("O5:O19").conditionalFormats.add("containsText", { text: "一致", format: { fill: palette.green, font: { color: "#375623", bold: true } } });
|
||||
const summaryTable = summary.tables.add("A4:P19", true, "AppealSummary");
|
||||
summaryTable.style = "TableStyleMedium2";
|
||||
summaryTable.showFilterButton = true;
|
||||
const summaryWidths = [8, 9, 14, 22, 22, 16, 18, 18, 15, 12, 16, 13, 14, 24, 26, 54];
|
||||
for (let col = 0; col < summaryWidths.length; col += 1) summary.getRangeByIndexes(0, col, 20, 1).format.columnWidth = summaryWidths[col];
|
||||
summary.getRange("A5:P19").format.rowHeight = 42;
|
||||
|
||||
notes.showGridLines = false;
|
||||
notes.getRange("A1:H1").merge();
|
||||
notes.getRange("A1").values = [["核验口径与证据说明"]];
|
||||
notes.getRange("A1:H1").format = titleFormat;
|
||||
notes.getRange("A1:H1").format.rowHeight = 32;
|
||||
notes.getRange("A3:H3").merge(); notes.getRange("A3").values = [["1. 核验口径"]]; notes.getRange("A3:H3").format = sectionFormat;
|
||||
notes.getRange("A4:B8").values = [
|
||||
["协议", "GB/T 32960"],
|
||||
["仪表总里程字段", "gb32960.vehicle.total_mileage_km"],
|
||||
["仪表总里程含义", "车辆仪表盘累计总里程,单位 km;不同协议累计值不可拼接。"],
|
||||
["中台当日里程", "生产 vehicle_daily_mileage 的 GB32960 日统计值;本材料逐车求和后与原异常表TBOX合计核对。"],
|
||||
["RAW相邻日差", "仅相邻两个自然日都存在RAW仪表读数时计算;缺任一日则留空,不把跨日缺口跳变塞进单日。"],
|
||||
];
|
||||
notes.getRange("A4:A8").format = labelFormat;
|
||||
notes.getRange("B4:H8").merge(true);
|
||||
notes.getRange("B4:H8").format = valueFormat;
|
||||
notes.getRange("A10:H10").merge(); notes.getRange("A10").values = [["2. 证据分级"]]; notes.getRange("A10:H10").format = sectionFormat;
|
||||
notes.getRange("A11:B14").values = [
|
||||
["A-原始32960报文", "中台TDengine保存的完整GB32960十六进制报文,且解析结果明确包含仪表总里程字段。"],
|
||||
["B-中台32960日统计", "中台日统计存在,但当日未检出含仪表总里程的RAW帧。"],
|
||||
["C-历史日统计迁移", "来自历史生产库 lingniu_prod.ln_vehicle_day_mileage 的迁移记录,不是原始帧。"],
|
||||
["D-无中台日记录", "当日中台日统计无记录,且现行RAW历史不可用或未检出。"],
|
||||
];
|
||||
notes.getRange("A11:A14").format = labelFormat;
|
||||
notes.getRange("B11:H14").merge(true);
|
||||
notes.getRange("B11:H14").format = valueFormat;
|
||||
notes.getRange("A11:H11").format.fill = palette.green;
|
||||
notes.getRange("A13:H13").format.fill = palette.yellow;
|
||||
notes.getRange("A14:H14").format.fill = palette.gray;
|
||||
|
||||
notes.getRange("A16:H16").merge(); notes.getRange("A16").values = [["3. 数据来源与限制"]]; notes.getRange("A16:H16").format = sectionFormat;
|
||||
notes.getRange("A17:B21").values = [
|
||||
["原异常表", "/Users/lingniu/Documents/里程异常统计表_20251128-20260731-回复.xlsx"],
|
||||
["中台日统计API", "http://115.29.187.205:20200/api/stats/daily-metrics"],
|
||||
["中台RAW历史API", "http://115.29.187.205:20200/api/history/raw-frames"],
|
||||
["RAW可查范围", "本次提取范围2026-07-01至2026-07-31;15车共取得334个含仪表总里程字段的完整原始帧。"],
|
||||
["重要限制", "2026-07-01之前现行TDengine没有可查询RAW历史;只能提供历史生产库日统计迁移记录。对外申诉时应如实说明,并向原TSP/车厂补调更早期原始报文归档。"],
|
||||
];
|
||||
notes.getRange("A17:A21").format = labelFormat;
|
||||
notes.getRange("B17:H21").merge(true);
|
||||
notes.getRange("B17:H21").format = valueFormat;
|
||||
notes.getRange("A21:H21").format.fill = palette.amber;
|
||||
|
||||
notes.getRange("A23:H23").merge(); notes.getRange("A23").values = [["4. 使用建议"]]; notes.getRange("A23:H23").format = sectionFormat;
|
||||
notes.getRange("A24:H27").values = [
|
||||
["① 先提交“申诉总览”,证明15车中台GB32960日里程合计与原TBOX合计一致。", null, null, null, null, null, null, null],
|
||||
["② 每车附对应Sheet,重点引用A类RAW证据、事件时间、接收时间和完整HEX。", null, null, null, null, null, null, null],
|
||||
["③ 对第三方缺失日,用完整日历中的D级空缺、前后仪表累计变化和原排查结论说明漏传。", null, null, null, null, null, null, null],
|
||||
["④ 申诉若要求全区间逐日原始报文,需由原TSP/车厂补调2026-07-01之前归档;本材料未虚构缺失证据。", null, null, null, null, null, null, null],
|
||||
];
|
||||
for (let row = 24; row <= 27; row += 1) notes.getRange(`A${row}:H${row}`).merge();
|
||||
notes.getRange("A24:H27").format = { ...valueFormat, fill: palette.blue2 };
|
||||
notes.getRange("A4:H27").format.rowHeight = 30;
|
||||
notes.getRange("A1:A27").format.columnWidth = 22;
|
||||
for (let col = 1; col < 8; col += 1) notes.getRangeByIndexes(0, col, 27, 1).format.columnWidth = 18;
|
||||
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.mkdir(previewDir, { recursive: true });
|
||||
|
||||
const inspection = {};
|
||||
inspection.summary = (await workbook.inspect({ kind: "table", range: "申诉总览!A1:P20", include: "values,formulas", tableMaxRows: 24, tableMaxCols: 16, maxChars: 22000 })).ndjson;
|
||||
inspection.formulaErrors = (await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "final formula error scan" })).ndjson;
|
||||
await fs.writeFile(path.join(outputDir, "verification.ndjson"), `${inspection.summary}\n${inspection.formulaErrors}\n`, "utf8");
|
||||
|
||||
const previewSheets = ["申诉总览", "口径与证据说明", ...data.vehicles.map((vehicle) => vehicleSheets.get(vehicle.vin).sheetName)];
|
||||
for (let index = 0; index < previewSheets.length; index += 1) {
|
||||
const sheetName = previewSheets[index];
|
||||
const range = sheetName === "申诉总览" ? "A1:P20" : sheetName === "口径与证据说明" ? "A1:H27" : "A1:P24";
|
||||
const preview = await workbook.render({ sheetName, range, scale: 1, format: "png" });
|
||||
const safeName = sheetName.replaceAll(/[\\/:*?\[\]]/g, "_");
|
||||
await fs.writeFile(path.join(previewDir, `${String(index + 1).padStart(2, "0")}-${safeName}.png`), new Uint8Array(await preview.arrayBuffer()));
|
||||
}
|
||||
|
||||
const output = await SpreadsheetFile.exportXlsx(workbook);
|
||||
await output.save(outputPath);
|
||||
console.log(JSON.stringify({ outputPath, sheets: previewSheets.length, previewDir }, null, 2));
|
||||
@@ -0,0 +1,158 @@
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
const baseUrl = "http://115.29.187.205:20200";
|
||||
const outputPath = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/mileage-appeal-20260810/evidence.json";
|
||||
const vehicles = [
|
||||
{ seq: 1, model: "18T", plate: "粤A05995F", vin: "LNXNEGRR7SR321370", sourceTboxKm: 26180.6, platformKm: 4208.9, lossKm: 21971.7, lossRatePct: 83.92, anomaly: "里程损失,tbox异常", appealKm: 41497.1, note: "区间内仪表盘开始里程是576.6,结束里程是41497.1,差值40920.5,小于申诉里程;2026-05-11到2026-07-28间的数据没上传,仪表盘里程差了36306.6,2025-12-25到2026-01-01间还存在里程跳变导致里程缺失" },
|
||||
{ seq: 2, model: "18T", plate: "粤A05839F", vin: "LNXNEGRR8SR321376", sourceTboxKm: 41030.3, platformKm: 19763.6, lossKm: 21266.7, lossRatePct: 51.83, anomaly: "里程损失", appealKm: 41030.3, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,特别是7月份仪表里程从7月1号的16341.2到7月31号的41848.9,相差25507.7,计算的总里程只有8470" },
|
||||
{ seq: 3, model: "18T", plate: "粤A09369F", vin: "LNXNEGRR6SR321473", sourceTboxKm: 40381.5, platformKm: 24994.0, lossKm: 15387.5, lossRatePct: 38.11, anomaly: "里程损失", appealKm: 40381.5, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 4, model: "18T", plate: "粤A06658F", vin: "LNXNEGRR5SR321397", sourceTboxKm: 52386.5, platformKm: 33064.5, lossKm: 19322.0, lossRatePct: 36.88, anomaly: "里程损失", appealKm: 52386.5, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 5, model: "18T", plate: "粤A05700F", vin: "LNXNEGRR8SR321393", sourceTboxKm: 60054.3, platformKm: 41352.9, lossKm: 18701.4, lossRatePct: 31.14, anomaly: "里程损失", appealKm: 60054.3, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 6, model: "18T", plate: "粤A05906F", vin: "LNXNEGRR2SR319462", sourceTboxKm: 37216.8, platformKm: 26711.4, lossKm: 10505.4, lossRatePct: 28.23, anomaly: "里程损失", appealKm: 37216.8, note: "最新数据只到2026-06-30,仪表盘最后里程为28638.5,没有7月份的数据;也存在很多数据缺失导致的里程缺失,例如2026-03-26到2026-04-01,2026-04-21到2026-05-01间没上传数据" },
|
||||
{ seq: 7, model: "4.5T", plate: "粤AGP9346", vin: "LB9A32A28R0LS1722", sourceTboxKm: 32337.2, platformKm: 28685.4, lossKm: 3651.8, lossRatePct: 11.29, anomaly: "里程损失", appealKm: 32337.2, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 8, model: "4.5T", plate: "粤AGP2017", vin: "LB9A32A28R0LS1445", sourceTboxKm: 48164.9, platformKm: 44357.1, lossKm: 3807.8, lossRatePct: 7.91, anomaly: "里程损失", appealKm: 48164.9, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,而且区间内最后的仪表里程是2026-07-31的47337.5,小于申诉里程" },
|
||||
{ seq: 9, model: "4.5T", plate: "粤AGP5165", vin: "LB9A32A25R0LS1385", sourceTboxKm: 32906.4, platformKm: 30302.3, lossKm: 2604.1, lossRatePct: 7.91, anomaly: "里程损失", appealKm: 32906.4, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,而且区间内最后的仪表里程是2026-07-30的31994.6,2026-07-31没有数据上传" },
|
||||
{ seq: 10, model: "4.5T", plate: "粤AGP4355", vin: "LB9A32A26R0LS1606", sourceTboxKm: 55151.0, platformKm: 51525.8, lossKm: 3625.2, lossRatePct: 6.57, anomaly: "里程损失", appealKm: 55151.0, note: "区间内仪表盘开始里程是1898.6,结束里程是55229.6,差值53331,小于申诉里程,也存在很多数据缺失导致的里程缺失" },
|
||||
{ seq: 11, model: "4.5T", plate: "粤AGP5646", vin: "LB9A32A22R0LS1702", sourceTboxKm: 39935.0, platformKm: 37319.8, lossKm: 2615.2, lossRatePct: 6.55, anomaly: "里程损失", appealKm: 39935.0, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 12, model: "4.5T", plate: "粤AGP3692", vin: "LB9A32A27R0LS1727", sourceTboxKm: 64195.0, platformKm: 60242.6, lossKm: 3952.4, lossRatePct: 6.16, anomaly: "里程损失", appealKm: 64195.0, note: "存在很多数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 13, model: "4.5T", plate: "粤AGP9782", vin: "LB9A32A28R0LS1705", sourceTboxKm: 50216.4, platformKm: 47123.8, lossKm: 3092.6, lossRatePct: 6.16, anomaly: "里程损失", appealKm: 50216.4, note: "存在很多数据缺失导致的里程跳变,统计里程缺失,如:2026-05-21数据缺失导致里程少了1300多" },
|
||||
{ seq: 14, model: "4.5T", plate: "粤AGP3027", vin: "LB9A32A22R0LS1599", sourceTboxKm: 31180.9, platformKm: 29260.1, lossKm: 1920.8, lossRatePct: 6.16, anomaly: "里程损失", appealKm: 31180.9, note: "区间内仪表盘开始里程是933.5,结束里程是31245.3,差值30311.8,小于申诉里程,也存在一些数据缺失导致的里程跳变,统计里程缺失" },
|
||||
{ seq: 15, model: "4.5T", plate: "粤AGP9751", vin: "LB9A32A28R0LS1574", sourceTboxKm: 17944.6, platformKm: 16861.0, lossKm: 1083.6, lossRatePct: 6.04, anomaly: "里程损失", appealKm: 17944.6, note: "区间内仪表盘开始里程是205.2,结束里程是17796.8,差值17591.6,小于申诉里程,也存在一些数据缺失导致的里程缺失" },
|
||||
];
|
||||
|
||||
function dateString(date) {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function addDays(day, count) {
|
||||
const date = new Date(`${day}T00:00:00Z`);
|
||||
date.setUTCDate(date.getUTCDate() + count);
|
||||
return dateString(date);
|
||||
}
|
||||
|
||||
function dateRange(start, end) {
|
||||
const out = [];
|
||||
for (let day = start; day <= end; day = addDays(day, 1)) out.push(day);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function fetchJson(url, attempts = 4) {
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
||||
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function mapConcurrent(items, concurrency, worker) {
|
||||
const results = new Array(items.length);
|
||||
let cursor = 0;
|
||||
async function run() {
|
||||
while (true) {
|
||||
const index = cursor;
|
||||
cursor += 1;
|
||||
if (index >= items.length) return;
|
||||
results[index] = await worker(items[index], index);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, run));
|
||||
return results;
|
||||
}
|
||||
|
||||
async function fetchDaily(vehicle) {
|
||||
const params = new URLSearchParams({
|
||||
vin: vehicle.vin,
|
||||
protocol: "GB32960",
|
||||
dateFrom: "2025-11-28",
|
||||
dateTo: "2026-07-31",
|
||||
limit: "500",
|
||||
includeTotal: "true",
|
||||
});
|
||||
const data = await fetchJson(`${baseUrl}/api/stats/daily-metrics?${params}`);
|
||||
return data.items ?? [];
|
||||
}
|
||||
|
||||
async function fetchRawEvidence({ vehicle, day }) {
|
||||
const params = new URLSearchParams({
|
||||
protocol: "GB32960",
|
||||
vin: vehicle.vin,
|
||||
dateFrom: `${day} 00:00:00`,
|
||||
dateTo: `${addDays(day, 1)} 00:00:00`,
|
||||
orderBy: "eventTime",
|
||||
limit: "20",
|
||||
includeFields: "true",
|
||||
includePayload: "true",
|
||||
fields: "gb32960.vehicle.total_mileage_km",
|
||||
});
|
||||
let data = await fetchJson(`${baseUrl}/api/history/raw-frames?${params}`);
|
||||
let evidence = (data.items ?? []).find((item) => item?.parsed_fields?.["gb32960.vehicle.total_mileage_km"] != null && String(item.event_time ?? "").startsWith(day));
|
||||
if (!evidence && (data.items ?? []).length === 20) {
|
||||
params.set("limit", "100");
|
||||
data = await fetchJson(`${baseUrl}/api/history/raw-frames?${params}`);
|
||||
evidence = (data.items ?? []).find((item) => item?.parsed_fields?.["gb32960.vehicle.total_mileage_km"] != null && String(item.event_time ?? "").startsWith(day));
|
||||
}
|
||||
if (!evidence) return { day, status: "NO_RAW_ODOMETER_FRAME" };
|
||||
return {
|
||||
day,
|
||||
status: "OK",
|
||||
ts: evidence.ts ?? "",
|
||||
eventTime: evidence.event_time ?? "",
|
||||
receivedAt: evidence.received_at ?? "",
|
||||
frameId: evidence.frame_id ?? "",
|
||||
eventId: evidence.event_id ?? "",
|
||||
messageIdHex: evidence.message_id_hex ?? "",
|
||||
rawSizeBytes: evidence.raw_size_bytes ?? null,
|
||||
rawHex: evidence.raw_hex ?? "",
|
||||
parseStatus: evidence.parse_status ?? "",
|
||||
parseError: evidence.parse_error ?? "",
|
||||
sourceEndpoint: evidence.source_endpoint ?? "",
|
||||
parsedTotalMileageKm: Number(evidence.parsed_fields["gb32960.vehicle.total_mileage_km"]),
|
||||
};
|
||||
}
|
||||
|
||||
const dailyLists = await mapConcurrent(vehicles, 6, fetchDaily);
|
||||
const rawDays = dateRange("2026-07-01", "2026-07-31");
|
||||
const rawTasks = vehicles.flatMap((vehicle) => rawDays.map((day) => ({ vehicle, day })));
|
||||
let completed = 0;
|
||||
const rawResults = await mapConcurrent(rawTasks, 10, async (task) => {
|
||||
const result = await fetchRawEvidence(task);
|
||||
completed += 1;
|
||||
if (completed % 50 === 0 || completed === rawTasks.length) console.log(`raw evidence ${completed}/${rawTasks.length}`);
|
||||
return { vin: task.vehicle.vin, ...result };
|
||||
});
|
||||
|
||||
const byVinRaw = new Map();
|
||||
for (const item of rawResults) {
|
||||
if (!byVinRaw.has(item.vin)) byVinRaw.set(item.vin, []);
|
||||
byVinRaw.get(item.vin).push(item);
|
||||
}
|
||||
|
||||
const result = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
baseUrl,
|
||||
period: { start: "2025-11-28", end: "2026-07-31" },
|
||||
protocol: "GB32960",
|
||||
parsedMileageField: "gb32960.vehicle.total_mileage_km",
|
||||
rawEvidenceAvailability: { start: "2026-07-01", end: "2026-07-31", note: "现行TDengine RAW历史自2026-07-01起;此前中台仅保留历史生产库日统计迁移记录,不能伪称原始帧。" },
|
||||
vehicles: vehicles.map((vehicle, index) => ({
|
||||
...vehicle,
|
||||
dailyMetrics: dailyLists[index],
|
||||
rawEvidence: (byVinRaw.get(vehicle.vin) ?? []).sort((a, b) => a.day.localeCompare(b.day)),
|
||||
})),
|
||||
};
|
||||
|
||||
await fs.writeFile(outputPath, JSON.stringify(result, null, 2), "utf8");
|
||||
console.log(JSON.stringify({
|
||||
outputPath,
|
||||
vehicles: result.vehicles.length,
|
||||
dailyRows: result.vehicles.reduce((sum, item) => sum + item.dailyMetrics.length, 0),
|
||||
rawEvidenceRows: result.vehicles.reduce((sum, item) => sum + item.rawEvidence.filter((row) => row.status === "OK").length, 0),
|
||||
rawMissingDays: result.vehicles.reduce((sum, item) => sum + item.rawEvidence.filter((row) => row.status !== "OK").length, 0),
|
||||
}, null, 2));
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
|
||||
const inputPath = "/Users/lingniu/Documents/里程异常统计表_20251128-20260731-回复.xlsx";
|
||||
const outputDir = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/mileage-appeal-20260810/source-preview";
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const input = await FileBlob.load(inputPath);
|
||||
const workbook = await SpreadsheetFile.importXlsx(input);
|
||||
|
||||
const overview = await workbook.inspect({
|
||||
kind: "workbook,sheet,table,drawing",
|
||||
maxChars: 20000,
|
||||
tableMaxRows: 12,
|
||||
tableMaxCols: 20,
|
||||
tableMaxCellChars: 180,
|
||||
});
|
||||
await fs.writeFile(path.join(outputDir, "overview.ndjson"), overview.ndjson, "utf8");
|
||||
|
||||
const sheets = [];
|
||||
for (let i = 0; ; i += 1) {
|
||||
let sheet;
|
||||
try {
|
||||
sheet = workbook.worksheets.getItemAt(i);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
if (!sheet) break;
|
||||
const name = sheet.name;
|
||||
const used = sheet.getUsedRange();
|
||||
const usedAddress = used?.address ?? null;
|
||||
const info = await workbook.inspect({
|
||||
kind: "region,computedStyle,formula",
|
||||
sheetId: name,
|
||||
range: usedAddress ?? "A1:Z60",
|
||||
maxChars: 30000,
|
||||
tableMaxRows: 80,
|
||||
tableMaxCols: 30,
|
||||
tableMaxCellChars: 240,
|
||||
options: { maxResults: 200 },
|
||||
});
|
||||
await fs.writeFile(path.join(outputDir, `${String(i + 1).padStart(2, "0")}-${name.replaceAll(/[\\/:*?\[\]]/g, "_")}.ndjson`), info.ndjson, "utf8");
|
||||
const preview = await workbook.render({ sheetName: name, autoCrop: "all", scale: 1, format: "png" });
|
||||
const previewPath = path.join(outputDir, `${String(i + 1).padStart(2, "0")}-${name.replaceAll(/[\\/:*?\[\]]/g, "_")}.png`);
|
||||
await fs.writeFile(previewPath, new Uint8Array(await preview.arrayBuffer()));
|
||||
sheets.push({ index: i, name, usedAddress, previewPath });
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(outputDir, "sheets.json"), JSON.stringify(sheets, null, 2), "utf8");
|
||||
console.log(JSON.stringify(sheets, null, 2));
|
||||
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
source = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/019fe9b9-eb60-7d22-8c0a-c70851a48083/preview")
|
||||
output = source / "contact-sheet.jpg"
|
||||
files = sorted(source.glob("*.png"))
|
||||
thumb_w, thumb_h = 620, 360
|
||||
label_h = 34
|
||||
cols = 2
|
||||
rows = (len(files) + cols - 1) // cols
|
||||
canvas = Image.new("RGB", (cols * thumb_w, rows * (thumb_h + label_h)), "white")
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
font = ImageFont.load_default()
|
||||
|
||||
for index, file in enumerate(files):
|
||||
image = Image.open(file).convert("RGB")
|
||||
image.thumbnail((thumb_w - 12, thumb_h - 12))
|
||||
x0 = (index % cols) * thumb_w
|
||||
y0 = (index // cols) * (thumb_h + label_h)
|
||||
x = x0 + (thumb_w - image.width) // 2
|
||||
y = y0 + 6
|
||||
canvas.paste(image, (x, y))
|
||||
draw.rectangle((x0, y0, x0 + thumb_w - 1, y0 + thumb_h + label_h - 1), outline="#B7C9D6", width=1)
|
||||
draw.text((x0 + 8, y0 + thumb_h + 8), file.stem, fill="#1F2937", font=font)
|
||||
|
||||
canvas.save(output, quality=88)
|
||||
print(output)
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
|
||||
const workbookPath = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/019fe9b9-eb60-7d22-8c0a-c70851a48083/15辆车里程核验申诉材料_GB32960日里程及原始报文.xlsx";
|
||||
const outputDir = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/019fe9b9-eb60-7d22-8c0a-c70851a48083/final-qa";
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(workbookPath));
|
||||
|
||||
const sheets = await workbook.inspect({ kind: "sheet", include: "id,name", maxChars: 8000 });
|
||||
const summary = await workbook.inspect({ kind: "table", range: "申诉总览!A1:P20", include: "values,formulas", tableMaxRows: 22, tableMaxCols: 16, maxChars: 18000 });
|
||||
const vehicleTop = await workbook.inspect({ kind: "table", range: "02-粤A05839F!A4:P14", include: "values,formulas", tableMaxRows: 14, tableMaxCols: 16, maxChars: 12000 });
|
||||
const errors = await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "reimported final formula error scan" });
|
||||
await fs.writeFile(path.join(outputDir, "final-inspect.ndjson"), `${sheets.ndjson}\n${summary.ndjson}\n${vehicleTop.ndjson}\n${errors.ndjson}\n`, "utf8");
|
||||
|
||||
const vehicleSheetNames = [
|
||||
"01-粤A05995F", "02-粤A05839F", "03-粤A09369F", "04-粤A06658F", "05-粤A05700F",
|
||||
"06-粤A05906F", "07-粤AGP9346", "08-粤AGP2017", "09-粤AGP5165", "10-粤AGP4355",
|
||||
"11-粤AGP5646", "12-粤AGP3692", "13-粤AGP9782", "14-粤AGP3027", "15-粤AGP9751",
|
||||
];
|
||||
for (let index = 0; index < vehicleSheetNames.length; index += 1) {
|
||||
const sheetName = vehicleSheetNames[index];
|
||||
const preview = await workbook.render({ sheetName, range: "A232:P259", scale: 1, format: "png" });
|
||||
await fs.writeFile(path.join(outputDir, `${String(index + 1).padStart(2, "0")}-${sheetName}.png`), new Uint8Array(await preview.arrayBuffer()));
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ workbookPath, outputDir, sheetCount: 17, formulaErrorScan: errors.ndjson }, null, 2));
|
||||
Reference in New Issue
Block a user