Files
lingniu-vehicle-ingest/tmp/hydrogen-audit-100-real/build_real_audit.mjs

96 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs/promises";
import { Workbook, SpreadsheetFile } from "@oai/artifact-tool";
const root="/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest";
const inputDir=`${root}/tmp/hydrogen-audit-100-real/output-v4`;
const outputRoot=`${root}/outputs/019fe9b9-eb60-7d22-8c0a-c70851a48083`;
const packageDir=`${outputRoot}/100个真实车辆日氢耗核验资料包_V3`;
const outputPath=`${packageDir}/100个真实车辆日氢耗人工验算底稿_V3.xlsx`;
const previewDir=`${outputRoot}/preview-real-100-v3`;
const days=JSON.parse(await fs.readFile(`${inputDir}/daily_results.json`,"utf8"));
const C={navy:"#0B3A53",blue:"#0E7490",cyan:"#CDEEF4",pale:"#EAF6F8",white:"#FFFFFF",green:"#DCFCE7",greenText:"#166534",orange:"#FFEDD5",orangeText:"#9A3412",red:"#FEE2E2",redText:"#991B1B"};
const coeffA=[0.05888460,-0.06136111,-0.002650473,0.002731125,0.001802374,-0.001150707,0.00009588528,-0.0000001109040,0.0000000001264403],coeffB=[1.325,1.87,2.5,2.8,2.938,3.14,3.37,3.75,4.0],coeffC=[1,1,2,2,2.42,2.63,3,4,5];
function col(n){let s="";while(n){n--;s=String.fromCharCode(65+n%26)+s;n=Math.floor(n/26)}return s}
function title(sheet,address,text){const r=sheet.getRange(address);r.merge();r.values=[[text]];r.format={fill:C.navy,font:{bold:true,color:C.white,size:16},verticalAlignment:"center",horizontalAlignment:"left"};r.format.rowHeight=34}
function header(r){r.format={fill:C.blue,font:{bold:true,color:C.white},verticalAlignment:"center",horizontalAlignment:"center",wrapText:true,borders:{preset:"all",style:"thin",color:"#B8CDD5"}};r.format.rowHeight=34}
function body(r){r.format={verticalAlignment:"center",borders:{insideHorizontal:{style:"thin",color:"#DDE7EB"},bottom:{style:"thin",color:"#B8CDD5"}}}}
function widths(sheet,arr){arr.forEach((w,i)=>sheet.getRange(`${col(i+1)}:${col(i+1)}`).format.columnWidth=w)}
function val(v){return v===undefined||v===null?null:v}
function zFormula(p,t){return ["1",...Array.from({length:9},(_,i)=>`'参数与公式'!$B$${26+i}*POWER(100/(${t}+273.15),'参数与公式'!$C$${26+i})*POWER(${p},'参数与公式'!$D$${26+i})`)].join("+")}
function massFormula(p,t,v){return `${p}*1000*0.00201588*${v}/(8.314472*(${t}+273.15)*(${zFormula(p,t)}))`}
const wb=Workbook.create();wb.comments.setSelf({displayName:"车辆数据中台"});
const guide=wb.worksheets.add("核验说明"),params=wb.worksheets.add("参数与公式"),summary=wb.worksheets.add("100车辆日结果"),h2Detail=wb.worksheets.add("氢量计算明细"),operation=wb.worksheets.add("运行周期明细"),rawIndex=wb.worksheets.add("原始帧索引"),fields=wb.worksheets.add("原始字段说明");
for(const s of [guide,params,summary,h2Detail,operation,rawIndex,fields])s.showGridLines=false;
title(guide,"A1:H1","100个真实车辆日氢耗人工验算底稿(V3)");guide.getRange("A3:B3").values=[["项目","说明"]];header(guide.getRange("A3:B3"));
const statusCounts=Object.fromEntries(["OK","SUSPECT","NO_DATA"].map(k=>[k,days.filter(d=>d.stat.QualityStatus===k).length]));
const guideRows=[
["样本口径","2026-08-25,固定100个真实车辆日;4.5T普货41辆(380 L)、4.5T冷链59辆(520 L),动力电池均为21.04 kWh。"],
["原始数据","沿用原100个样本的232,291条GB/T 32960原始帧,未更换车辆、日期或报文;每车1个CSV.GZ,保留原始HEX及中文映射字段。"],
["有效分界点","取消上下电前后60秒窗口和首尾中位数,直接取满足车辆启动、字段完整、压力温度有效且不在外部充电/异常压力记录中的有效分界点。"],
["物理用氢量","无加氢:首个有效运行分界点剩余氢量-最后有效运行分界点剩余氢量;有加氢:按加氢事件切段后累计各段氢量下降。充电不切分物理氢量。"],
["充电识别","仅当充电状态=1且车辆状态=2时识别为外部充电;每次充电完成后建立新充电周期。"],
["纯电里程","每个充电周期若启动时为纯电,仅累计首次连续纯电前缀;混动连续3帧且相邻不超过30秒后,从首帧分界点锁定为混动,直至下一次充电。若周期从混动启动,则纯电里程为0。"],
["跨日状态","日界线只用于出报,不重置充电周期。若缺少日初已持久化状态,按已进入混动处理,不虚增日初纯电里程,并标记SUSPECT供复核。"],
["SOC修正","只使用各充电周期混动段的状态边界:ΔSOC=末SOC-初SOC;电等效氢=电池容量×ΔSOC÷100÷16。SOC上升为正并从用氢量扣除;SOC下降为负,扣除负值即补回电池放电能量。"],
["最终公式","修正耗氢量=物理用氢量-用电等效氢;混动里程=总里程-纯电里程;修正百公里氢耗=修正耗氢量÷混动里程×100。"],
["人工核对顺序","①在“100车辆日结果”看系统值与Excel复算差;②在“氢量计算明细”核对加氢分段及压力质量;③在“运行周期明细”核对纯电前缀和混动SOC;④按事件ID回查原始CSV。"],
["重算结果",`OK ${statusCounts.OK}辆日;SUSPECT ${statusCounts.SUSPECT}辆日;NO_DATA ${statusCounts.NO_DATA}辆日。SUSPECT不删除结果,保留原因和原始证据供人工判断。`],
["算法版本","PRESSURE_NIST_VALID_BOUNDARY_CHARGE_CYCLE_V3"]];
guide.getRange(`A4:B${3+guideRows.length}`).values=guideRows;body(guide.getRange(`A4:B${3+guideRows.length}`));guide.getRange(`B4:B${3+guideRows.length}`).format.wrapText=true;widths(guide,[24,112]);guide.freezePanes.freezeRows(3);
title(params,"A1:F1","系统参数、压力—质量公式与业务计算口径");params.getRange("A3:C3").values=[["参数","取值","说明"]];header(params.getRange("A3:C3"));
const pRows=[["4.5T普货储氢总容积(L)",380,"中台VIN参数表"],["4.5T冷链储氢总容积(L)",520,"中台VIN参数表"],["动力电池容量(kWh)",21.04,"两类4.5T车辆一致"],["氢气等效电量(kWh/kg)",16,"SOC能量折算"],["上电/下电时间窗口(s)",0,"直接使用有效分界点"],["加氢压升阈值(MPa)",3,"需持续300秒"],["加氢持续时间(s)",300,"5分钟"],["纯电异常压降(MPa)",5,"30分钟窗口"],["纯电异常窗口(s)",1800,"30分钟"],["混动确认连续帧数",3,"避免单帧运行模式抖动"],["混动确认相邻最大间隔(s)",30,"确认后回溯到首个混动帧"],["最低混动里程(km)",10,"不足标记SUSPECT"]];
params.getRange(`A4:C${3+pRows.length}`).values=pRows;body(params.getRange(`A4:C${3+pRows.length}`));params.getRange(`B4:B${3+pRows.length}`).format.numberFormat="0.000";
params.getRange("A18:F18").merge();params.getRange("A18:F18").values=[["核心公式"]];params.getRange("A18:F18").format={fill:C.cyan,font:{bold:true,color:C.navy}};
const formulas=["Z = 1 + Σ[aᵢ × (100/Tₖ)^bᵢ × P^cᵢ]","剩余氢量 m = P × 1000 × 0.00201588 × V ÷ (8.314472 × Tₖ × Z)","ΔSOC = SOC_end SOC_start;用电等效氢 = 电池容量 × ΔSOC ÷ 100 ÷ 16","修正耗氢量 = 物理用氢量 − 用电等效氢","混动里程 = 总里程 − 纯电里程;百公里氢耗 = 修正耗氢量 ÷ 混动里程 × 100"];
for(let i=0;i<formulas.length;i++){const row=19+i;params.getRange(`A${row}:F${row}`).merge();params.getRange(`A${row}:F${row}`).values=[[formulas[i]]];params.getRange(`A${row}:F${row}`).format={fill:i%2?C.white:C.pale,font:{color:C.navy,bold:i===3},wrapText:true}}
params.getRange("A25:D25").values=[["系数序号","aᵢ","bᵢ","cᵢ"]];header(params.getRange("A25:D25"));params.getRange("A26:D34").values=coeffA.map((a,i)=>[i+1,a,coeffB[i],coeffC[i]]);body(params.getRange("A26:D34"));params.getRange("B26:D34").format.numberFormat="0.0000000000";
wb.comments.addThread({cell:params.getRange("B4")},"普货按中台VIN车型参数取380 L,不与冷链520 L混用。");wb.comments.addThread({cell:params.getRange("B5")},"冷链按中台VIN车型参数取520 L。");wb.comments.addThread({cell:params.getRange("B6")},"4.5T普货和冷链动力电池容量均为21.04 kWh。");wb.comments.addThread({cell:params.getRange("B7")},"业务确认:1 kg氢气等效16 kWh,用于SOC能量折算。");widths(params,[32,18,58,15,15,15]);params.freezePanes.freezeRows(3);
const hHeaders=["样例","车牌","日期","VIN","车型","氢量段号","分段说明","开始时间","结束时间","开始事件ID","结束事件ID","数据源","储氢容积(L)","起始压力(MPa)","结束压力(MPa)","起始温度(℃)","结束温度(℃)","系统起始氢量(kg)","系统结束氢量(kg)","系统分段用氢(kg)","起始里程(km)","结束里程(km)","区间里程(km)","样本数","质量状态","起始氢量复算","结束氢量复算","起始质量差","结束质量差","分段用氢复算","用氢差值","复算结论"];
h2Detail.getRange(`A1:${col(hHeaders.length)}1`).values=[hHeaders];header(h2Detail.getRange(`A1:${col(hHeaders.length)}1`));const hRows=[];for(const d of days)for(const it of d.stat.HydrogenIntervals||[])hRows.push([d.candidate.sample_no,d.candidate.plate,d.candidate.stat_date,d.candidate.vin,d.candidate.model,it.index,it.qualityReason,it.startTime,it.endTime,it.startEventId,it.endEventId,it.source,d.candidate.tank_capacity_l,it.startPressureMpa,it.endPressureMpa,it.startTemperatureC,it.endTemperatureC,it.startMassKg,it.endMassKg,it.rawHydrogenConsumptionKg,val(it.startMileageKm),val(it.endMileageKm),val(it.mileageKm),it.sampleCount,it.qualityStatus]);
h2Detail.getRange(`A2:Y${hRows.length+1}`).values=hRows;for(let r=2;r<=hRows.length+1;r++){h2Detail.getRange(`Z${r}`).formulas=[[`=${massFormula(`N${r}`,`P${r}`,`M${r}`)}`]];h2Detail.getRange(`AA${r}`).formulas=[[`=${massFormula(`O${r}`,`Q${r}`,`M${r}`)}`]];h2Detail.getRange(`AB${r}`).formulas=[[`=Z${r}-R${r}`]];h2Detail.getRange(`AC${r}`).formulas=[[`=AA${r}-S${r}`]];h2Detail.getRange(`AD${r}`).formulas=[[`=Z${r}-AA${r}`]];h2Detail.getRange(`AE${r}`).formulas=[[`=AD${r}-T${r}`]];h2Detail.getRange(`AF${r}`).formulas=[[`=IF(MAX(ABS(AB${r}),ABS(AC${r}),ABS(AE${r}))<=0.002,"一致","复核")`]]}
body(h2Detail.getRange(`A2:AF${hRows.length+1}`));h2Detail.getRange(`M2:AE${hRows.length+1}`).format.numberFormat="0.000000";h2Detail.getRange(`Y2:Y${hRows.length+1}`).conditionalFormats.add("containsText",{text:"SUSPECT",format:{fill:C.orange,font:{color:C.orangeText,bold:true}}});h2Detail.getRange(`AF2:AF${hRows.length+1}`).conditionalFormats.add("containsText",{text:"一致",format:{fill:C.green,font:{color:C.greenText,bold:true}}});h2Detail.getRange(`AF2:AF${hRows.length+1}`).conditionalFormats.add("containsText",{text:"复核",format:{fill:C.red,font:{color:C.redText,bold:true}}});h2Detail.tables.add(`A1:AF${hRows.length+1}`,true,"HydrogenEvidenceV3").style="TableStyleMedium2";h2Detail.freezePanes.freezeRows(1);h2Detail.freezePanes.freezeColumns(7);widths(h2Detail,[8,13,13,22,24,10,34,22,22,34,34,24,14,14,14,14,14,17,17,17,15,15,15,10,13,17,17,14,14,17,14,12]);
const oHeaders=["样例","车牌","日期","VIN","车型","充电周期号","区间类型","区间号","开始时间","结束时间","开始事件ID","结束事件ID","数据源","储氢容积(L)","电池容量(kWh)","起始压力(MPa)","结束压力(MPa)","起始温度(℃)","结束温度(℃)","系统起始氢量","系统结束氢量","区间物理氢量","起始SOC","结束SOC","系统ΔSOC(末-初)","系统电量变化(kWh)","系统电等效氢(kg)","系统段修正氢量","起始里程","结束里程","系统区间里程","系统段百公里","样本数","质量状态","分界说明","起始氢量复算","结束氢量复算","起始质量差","结束质量差","ΔSOC复算","电量变化复算","电等效氢复算","段修正氢量复算","里程复算","纯电里程贡献","混动电等效氢贡献","复算结论"];
operation.getRange(`A1:${col(oHeaders.length)}1`).values=[oHeaders];header(operation.getRange(`A1:${col(oHeaders.length)}1`));const oRows=[];for(const d of days){let cycleNo=0,previous="";for(const it of d.stat.Intervals||[]){if(cycleNo===0||it.type==="PURE_ELECTRIC"||previous==="MIXED")cycleNo++;oRows.push([d.candidate.sample_no,d.candidate.plate,d.candidate.stat_date,d.candidate.vin,d.candidate.model,cycleNo,it.type,it.index,it.startTime,it.endTime,it.startEventId,it.endEventId,it.source,d.candidate.tank_capacity_l,21.04,it.startPressureMpa,it.endPressureMpa,it.startTemperatureC,it.endTemperatureC,it.startMassKg,it.endMassKg,it.rawHydrogenConsumptionKg,val(it.startSocPercent),val(it.endSocPercent),val(it.batterySocDeltaPct),val(it.batteryEnergyChangeKWh),val(it.electricEquivalentHydrogenKg),val(it.correctedHydrogenConsumptionKg),val(it.startMileageKm),val(it.endMileageKm),val(it.mileageKm),val(it.consumptionKgPer100Km),it.sampleCount,it.qualityStatus,it.qualityReason]);previous=it.type}}
operation.getRange(`A2:AI${oRows.length+1}`).values=oRows;for(let r=2;r<=oRows.length+1;r++){operation.getRange(`AJ${r}`).formulas=[[`=${massFormula(`P${r}`,`R${r}`,`N${r}`)}`]];operation.getRange(`AK${r}`).formulas=[[`=${massFormula(`Q${r}`,`S${r}`,`N${r}`)}`]];operation.getRange(`AL${r}`).formulas=[[`=AJ${r}-T${r}`]];operation.getRange(`AM${r}`).formulas=[[`=AK${r}-U${r}`]];operation.getRange(`AN${r}`).formulas=[[`=IF(OR(W${r}="",X${r}=""),"",X${r}-W${r})`]];operation.getRange(`AO${r}`).formulas=[[`=IF(AN${r}="","",O${r}*AN${r}/100)`]];operation.getRange(`AP${r}`).formulas=[[`=IF(AO${r}="","",AO${r}/'参数与公式'!$B$7)`]];operation.getRange(`AQ${r}`).formulas=[[`=IF(G${r}="MIXED",V${r}-AP${r},"")`]];operation.getRange(`AR${r}`).formulas=[[`=IF(OR(AC${r}="",AD${r}=""),"",AD${r}-AC${r})`]];operation.getRange(`AS${r}`).formulas=[[`=IF(G${r}="PURE_ELECTRIC",AR${r},0)`]];operation.getRange(`AT${r}`).formulas=[[`=IF(G${r}="MIXED",AP${r},0)`]];operation.getRange(`AU${r}`).formulas=[[`=IF(MAX(ABS(AL${r}),ABS(AM${r}),ABS(AN${r}-Y${r}),ABS(AP${r}-AA${r}),ABS(AR${r}-AE${r}))<=0.002,"一致","复核")`]]}
body(operation.getRange(`A2:AU${oRows.length+1}`));operation.getRange(`N2:AT${oRows.length+1}`).format.numberFormat="0.000000";operation.getRange(`G2:G${oRows.length+1}`).conditionalFormats.add("containsText",{text:"PURE_ELECTRIC",format:{fill:C.cyan,font:{color:C.navy}}});operation.getRange(`G2:G${oRows.length+1}`).conditionalFormats.add("containsText",{text:"MIXED",format:{fill:C.green,font:{color:C.greenText}}});operation.getRange(`AU2:AU${oRows.length+1}`).conditionalFormats.add("containsText",{text:"一致",format:{fill:C.green,font:{color:C.greenText,bold:true}}});operation.getRange(`AU2:AU${oRows.length+1}`).conditionalFormats.add("containsText",{text:"复核",format:{fill:C.red,font:{color:C.redText,bold:true}}});operation.tables.add(`A1:AU${oRows.length+1}`,true,"OperationCyclesV3").style="TableStyleMedium2";operation.freezePanes.freezeRows(1);operation.freezePanes.freezeColumns(8);widths(operation,[8,13,13,22,24,11,15,9,22,22,34,34,24,14,15,14,14,14,14,16,16,16,12,12,15,17,17,18,15,15,16,17,10,13,42,17,17,14,14,15,17,17,18,15,16,20,12]);
const sHeaders=["样例","日期","车牌","VIN","车型","储氢容积(L)","电池容量(kWh)","原统计日里程(km)","有效起始仪表里程","有效结束仪表里程","总里程复算","API原始帧","算法样本","氢量分段数","运行区间数","加氢次数","充电次数","异常记录","系统物理用氢","物理用氢复算","物理差值","系统ΔSOC","ΔSOC复算","系统电等效氢","电等效氢复算","等效氢差值","系统修正耗氢","修正耗氢复算","修正差值","系统纯电里程","纯电里程复算","系统混动里程","混动里程复算","混动差值","系统物理百公里","物理百公里复算","系统修正百公里","修正百公里复算","百公里差值","质量状态","质量原因","算法版本","原始文件","打开原始帧"];
summary.getRange(`A1:${col(sHeaders.length)}1`).values=[sHeaders];header(summary.getRange(`A1:${col(sHeaders.length)}1`));
const sRows=days.map(d=>{const s=d.stat,c=d.candidate;return[c.sample_no,c.stat_date,c.plate,c.vin,c.model,c.tank_capacity_l,21.04,c.daily_mileage_km,val(s.FirstObservation?.MileageKm),val(s.LastObservation?.MileageKm),null,d.apiRawFrameCount,d.algorithmSamples,(s.HydrogenIntervals||[]).length,(s.Intervals||[]).length,s.RefuelCount,s.ChargeCount,s.InvalidSegmentCount+s.AbnormalDropCount,s.ConsumptionKg,null,null,val(s.BatterySOCDeltaPct),null,val(s.ElectricEquivalentKg),null,null,val(s.CorrectedConsumptionKg),null,null,s.PureElectricMileageKm,null,s.MixedMileageKm,null,null,val(s.ConsumptionKgPer100Km),null,val(s.SOCBalancedKgPer100Km),null,null,s.QualityStatus,s.QualityReason,s.CalculationParameters?.algorithmVersion,d.rawArchiveFile,null]});
summary.getRange(`A2:AR${sRows.length+1}`).values=sRows;const hEnd=hRows.length+1,oEnd=oRows.length+1;
for(let r=2;r<=sRows.length+1;r++){
summary.getRange(`K${r}`).formulas=[[`=IF(OR(I${r}="",J${r}=""),"",J${r}-I${r})`]];
summary.getRange(`T${r}`).formulas=[[`=SUMIFS('氢量计算明细'!$AD$2:$AD$${hEnd},'氢量计算明细'!$D$2:$D$${hEnd},$D${r})`]];summary.getRange(`U${r}`).formulas=[[`=T${r}-S${r}`]];
summary.getRange(`W${r}`).formulas=[[`=SUMIFS('运行周期明细'!$AN$2:$AN$${oEnd},'运行周期明细'!$D$2:$D$${oEnd},$D${r},'运行周期明细'!$G$2:$G$${oEnd},"MIXED")`]];
summary.getRange(`Y${r}`).formulas=[[`=SUMIFS('运行周期明细'!$AT$2:$AT$${oEnd},'运行周期明细'!$D$2:$D$${oEnd},$D${r})`]];summary.getRange(`Z${r}`).formulas=[[`=Y${r}-X${r}`]];
summary.getRange(`AB${r}`).formulas=[[`=T${r}-Y${r}`]];summary.getRange(`AC${r}`).formulas=[[`=AB${r}-AA${r}`]];
summary.getRange(`AE${r}`).formulas=[[`=SUMIFS('运行周期明细'!$AS$2:$AS$${oEnd},'运行周期明细'!$D$2:$D$${oEnd},$D${r})`]];
summary.getRange(`AG${r}`).formulas=[[`=K${r}-AE${r}`]];summary.getRange(`AH${r}`).formulas=[[`=AG${r}-AF${r}`]];
summary.getRange(`AJ${r}`).formulas=[[`=IF(AG${r}>0,T${r}*100/AG${r},"")`]];summary.getRange(`AL${r}`).formulas=[[`=IF(AG${r}>0,AB${r}*100/AG${r},"")`]];summary.getRange(`AM${r}`).formulas=[[`=IF(OR(AK${r}="",AL${r}=""),"",AL${r}-AK${r})`]];
summary.getRange(`AR${r}`).formulas=[[`=HYPERLINK("${days[r-2].rawArchiveFile}","打开")`]];
}
body(summary.getRange(`A2:AR${sRows.length+1}`));summary.getRange(`F2:AM${sRows.length+1}`).format.numberFormat="0.000000";
for(const [text,fill,font] of [["OK",C.green,C.greenText],["SUSPECT",C.orange,C.orangeText],["NO_DATA",C.red,C.redText]])summary.getRange(`AN2:AN${sRows.length+1}`).conditionalFormats.add("containsText",{text,format:{fill,font:{color:font,bold:true}}});
for(const c of ["U","Z","AC","AH","AM"]){summary.getRange(`${c}2:${c}${sRows.length+1}`).conditionalFormats.add("cellIs",{operator:"greaterThan",formula:0.002,format:{fill:C.red,font:{color:C.redText,bold:true}}});summary.getRange(`${c}2:${c}${sRows.length+1}`).conditionalFormats.add("cellIs",{operator:"lessThan",formula:-0.002,format:{fill:C.red,font:{color:C.redText,bold:true}}})}
summary.tables.add(`A1:AR${sRows.length+1}`,true,"VehicleDayResultsV3").style="TableStyleMedium2";summary.freezePanes.freezeRows(1);summary.freezePanes.freezeColumns(5);widths(summary,[8,13,13,22,24,14,14,17,18,18,15,12,12,12,12,11,11,12,16,16,14,14,14,16,16,14,17,17,14,16,16,16,16,14,18,18,18,18,14,13,48,42,58,12]);
const rHeaders=["样例","车牌","VIN","日期","车型","储氢容积(L)","电池容量(kWh)","API原始帧数","唯一帧数","重复帧数","算法样本数","关键字段帧数","原始最早事件时间","原始最晚事件时间","原始文件","打开"];
rawIndex.getRange("A1:P1").values=[rHeaders];header(rawIndex.getRange("A1:P1"));const rRows=days.map(d=>[d.candidate.sample_no,d.candidate.plate,d.candidate.vin,d.candidate.stat_date,d.candidate.model,d.candidate.tank_capacity_l,21.04,d.apiRawFrameCount,d.uniqueFrameCount,d.duplicateFrameCount,d.algorithmSamples,d.criticalFieldRows,d.earliestEventTime,d.latestEventTime,d.rawArchiveFile,null]);rawIndex.getRange(`A2:P${rRows.length+1}`).values=rRows;for(let r=2;r<=rRows.length+1;r++)rawIndex.getRange(`P${r}`).formulas=[[`=HYPERLINK("${days[r-2].rawArchiveFile}","打开")`]];body(rawIndex.getRange(`A2:P${rRows.length+1}`));rawIndex.tables.add(`A1:P${rRows.length+1}`,true,"RawFrameManifestV3").style="TableStyleMedium2";rawIndex.freezePanes.freezeRows(1);rawIndex.freezePanes.freezeColumns(5);widths(rawIndex,[8,13,22,13,24,14,14,14,12,12,14,15,21,21,58,12]);
title(fields,"A1:D1","原始帧字段及人工核验用途");fields.getRange("A3:D3").values=[["中文字段","GB/T 32960解析键/原始属性","用途","校验规则/枚举"]];header(fields.getRange("A3:D3"));
const fRows=[["原始报文HEX","raw_hex","回查原始帧","完整保留,不参与公式"],["事件ID","event_id","连接各明细起止事件","按ID在对应CSV查找"],["事件时间","event_time","确定自然日及排序","以事件时间为准"],["最高氢压(MPa)","gb32960.fuel_cell.max_hydrogen_pressure_mpa","压力—质量换算","0<P≤70"],["最高氢温(℃)","gb32960.fuel_cell.max_hydrogen_temperature_c","压力—质量换算","T>-40"],["电池SOC(%)","gb32960.vehicle.soc_percent","混动边界SOC修正","0100;只取相关状态边界"],["仪表总里程(km)","gb32960.vehicle.total_mileage_km","总/纯电/混动里程","分界点末值-初值"],["车辆状态","gb32960.vehicle.vehicle_status","启动及充电联合判断","1=启动,2=熄火,其他按国标保留"],["充电状态","gb32960.vehicle.charge_status","外部充电联合判断","1=停车充电,2=行驶充电,3=未充电,4=充电完成"],["运行模式","gb32960.vehicle.running_mode","纯电前缀/混动锁定","1=纯电,2=混动,3=燃油,254=异常,255=无效"],["燃料电池工作状态","gb32960.gd_fc_stack.engine_work_state","辅助确认纯电/混动","2=工作;0/1=未工作/停机"],["系统压力换算质量(kg)","工作底稿复算列","氢量端点","按VIN容积+NIST公式"],["计算角色/排除原因","工作底稿标注","定位参与/排除状态","有效分界点、外部充电、异常或字段缺失"]];
fields.getRange(`A4:D${3+fRows.length}`).values=fRows;body(fields.getRange(`A4:D${3+fRows.length}`));fields.getRange(`A4:D${3+fRows.length}`).format.wrapText=true;widths(fields,[28,62,40,54]);fields.freezePanes.freezeRows(3);
await fs.mkdir(packageDir,{recursive:true});
const checks={};for(const [name,range] of [["核验说明","A1:H15"],["参数与公式","A1:F34"],["100车辆日结果","A1:AR12"],["氢量计算明细","A1:AF15"],["运行周期明细","A1:AU15"],["原始帧索引","A1:P15"],["原始字段说明","A1:D16"]])checks[name]=(await wb.inspect({kind:"table",sheetId:name,range,include:"values,formulas",tableMaxRows:18,tableMaxCols:50,maxChars:18000})).ndjson;
const formulaErrors=await wb.inspect({kind:"match",searchTerm:"#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A",options:{useRegex:true,maxResults:300},summary:"final formula error scan",maxChars:10000});
const xlsx=await SpreadsheetFile.exportXlsx(wb);await xlsx.save(outputPath);
await fs.rm(previewDir,{recursive:true,force:true});await fs.mkdir(previewDir,{recursive:true});const renderRanges={"核验说明":"A1:H15","参数与公式":"A1:F34","100车辆日结果":"A1:AR14","氢量计算明细":"A1:AF14","运行周期明细":"A1:AU14","原始帧索引":"A1:P14","原始字段说明":"A1:D16"};for(const [name,range] of Object.entries(renderRanges)){const image=await wb.render({sheetName:name,range,scale:1,format:"png"});await fs.writeFile(`${previewDir}/${name}.png`,new Uint8Array(await image.arrayBuffer()))}
await fs.cp(`${inputDir}/raw`,`${packageDir}/raw`,{recursive:true,force:true});for(const name of ["daily_results.json","raw_manifest.csv","selected_vehicles.json"])await fs.copyFile(`${inputDir}/${name}`,`${packageDir}/${name}`);
await fs.writeFile(`${outputRoot}/verification-real-100-v3.json`,JSON.stringify({outputPath,checks,formulaErrors:formulaErrors.ndjson,days:days.length,hydrogenIntervals:hRows.length,operationIntervals:oRows.length},null,2));console.log(JSON.stringify({outputPath,days:days.length,hydrogenIntervals:hRows.length,operationIntervals:oRows.length,previewDir,formulaErrors:formulaErrors.ndjson},null,2));