feat: align staged fault details artifact flow
Tighten the staged fault-details skill contract, add artifact-focused tests, and align the packaged collector with the canonical detail and summary output expected by the staged scene metadata. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
"name": "故障明细",
|
||||
"category": "report",
|
||||
"entry": "index.html",
|
||||
"summary": "查询故障明细行并生成包含明细与汇总分区的结构化报表产物。",
|
||||
"summary": "查询故障明细行,生成包含明细与汇总分区的结构化报表产物,并记录导出与报表历史结果。",
|
||||
"inputs": ["period"],
|
||||
"outputs": ["report-artifact"],
|
||||
"dependencies": ["browser", "report-history", "local-report-service"],
|
||||
"actions": ["query", "collect-report", "build-summary-section"],
|
||||
"actions": ["query", "collect-report", "build-summary-section", "attempt-export", "record-report-log"],
|
||||
"tags": ["fault", "report", "xlsx", "details", "browser"],
|
||||
"skill": {
|
||||
"package": "fault-details-report",
|
||||
|
||||
@@ -32,9 +32,12 @@ Do not use this skill for:
|
||||
1. Read the selected start and end time from the page date-range control.
|
||||
2. Collect raw fault-detail rows from the source page's repair-order query.
|
||||
3. Normalize rows into the canonical detail-column order defined by the page export schema.
|
||||
4. Derive the summary rows used by the secondary summary sheet.
|
||||
5. Return the structured artifact before any prose summary.
|
||||
6. If required columns are missing or summary derivation is incomplete, mark the result as partial.
|
||||
4. Apply the original classification table and `qxxcjl`-based reason heuristics to fill `sxfl1`, `sxfl2`, `sxfl3`, `gzsb`, and `gzyy`.
|
||||
5. Derive the summary rows used by the secondary summary sheet.
|
||||
6. Attempt localhost export through `faultDetailsExportXLSXS`.
|
||||
7. Attempt report-history write through `setReportLog` when export returns a path.
|
||||
8. Return the structured artifact before any prose summary.
|
||||
9. If required columns are missing or summary derivation is incomplete, mark the result as partial or error according to the artifact contract.
|
||||
|
||||
## Runtime Contract
|
||||
|
||||
@@ -57,7 +60,11 @@ Do not use this skill for:
|
||||
{
|
||||
"type": "report-artifact",
|
||||
"report_name": "fault-details-report",
|
||||
"period": "",
|
||||
"period": "2026-03",
|
||||
"selected_range": {
|
||||
"start": "2026-03-08 16:00:00",
|
||||
"end": "2026-03-09 16:00:00"
|
||||
},
|
||||
"columns": [
|
||||
"qxdbh",
|
||||
"gssgs",
|
||||
@@ -114,8 +121,24 @@ Do not use this skill for:
|
||||
"rows": []
|
||||
}
|
||||
],
|
||||
"status": "ok",
|
||||
"partial_reasons": []
|
||||
"counts": {
|
||||
"detail_rows": 0,
|
||||
"summary_rows": 0
|
||||
},
|
||||
"status": "partial",
|
||||
"partial_reasons": ["report_log_failed"],
|
||||
"downstream": {
|
||||
"export": {
|
||||
"attempted": true,
|
||||
"success": true,
|
||||
"path": "http://localhost/export.xlsx"
|
||||
},
|
||||
"report_log": {
|
||||
"attempted": true,
|
||||
"success": false,
|
||||
"error": "500"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[skill]
|
||||
name = "fault-details-report"
|
||||
description = "Use when the user wants to collect fault-detail rows and export a structured fault report artifact or spreadsheet."
|
||||
description = "Use when the user wants to collect fault-detail rows from the business page, derive the summary sheet, attempt localhost export, and record report history in one structured report artifact workflow."
|
||||
version = "0.1.0"
|
||||
author = "sgclaw"
|
||||
tags = ["fault", "report", "xlsx", "details", "browser"]
|
||||
@@ -11,6 +11,6 @@ prompts = [
|
||||
|
||||
[[tools]]
|
||||
name = "collect_fault_details"
|
||||
description = "Collect raw fault-detail rows and prepare the detail-plus-summary report artifact shell from the source business page."
|
||||
description = "Collect fault-detail rows from the source business page, normalize canonical detail fields, derive summary rows, attempt localhost export/report-log, and return one structured report artifact."
|
||||
kind = "browser_script"
|
||||
command = "scripts/collect_fault_details.js"
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
## First-pass Collection Steps
|
||||
|
||||
1. Open or attach to the source page.
|
||||
2. Read the selected start and end datetime.
|
||||
2. Read the selected start and end datetime from the page controls; keep this as the source-of-truth collection range.
|
||||
3. Ensure the D4 business page session is available.
|
||||
4. Trigger the repair-order query used by the page logic.
|
||||
5. Collect raw fault-detail rows.
|
||||
6. Normalize rows using the canonical column order declared in `excleIni[0].cols`.
|
||||
7. Derive the summary-sheet rows from grouped detail rows.
|
||||
8. Return a structured artifact before any export-file or prose step.
|
||||
6. Filter out rows with `statusName == "回退营销"`.
|
||||
7. Normalize rows using the canonical column order declared in `excleIni[0].cols`.
|
||||
8. Apply the original classification table and `qxxcjl` heuristics to derive `sxfl1`, `sxfl2`, `sxfl3`, `gzsb`, and `gzyy`.
|
||||
9. Derive the summary-sheet rows from grouped detail rows.
|
||||
10. Attempt localhost export through `faultDetailsExportXLSXS`.
|
||||
11. If export returns a path, attempt report-history write through `setReportLog`.
|
||||
12. Return one structured artifact describing collection plus downstream outcomes.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@@ -71,6 +71,25 @@ These may be blank in some rows but should still be preserved when present:
|
||||
- `gzyy`
|
||||
- `bz`
|
||||
|
||||
## Classification And Reason Rules
|
||||
|
||||
The staged skill must port the original package classification table in full. That table maps `sjflMc` + `ejflMc` combinations into:
|
||||
|
||||
- `sxfl1`
|
||||
- `sxfl2`
|
||||
- `sxfl3`
|
||||
- `gzsb`
|
||||
- `dwcFl`
|
||||
|
||||
It also uses `qxxcjl` text heuristics to derive `gzyy`, including direct customer-cause overrides such as:
|
||||
|
||||
- `客户原因导致表计烧损`
|
||||
- `客户原因导致表前线断线`
|
||||
- `客户原因导致表前开关跳闸`
|
||||
- `客户表后线烧损`
|
||||
|
||||
When no direct override matches, reason extraction should fall back to the original `故障现象是...` / `经查...` text slicing behavior.
|
||||
|
||||
## Summary-Derivation Expectations
|
||||
|
||||
The first-pass package should also be able to derive summary rows keyed around:
|
||||
@@ -85,11 +104,24 @@ The first-pass package should also be able to derive summary rows keyed around:
|
||||
- `sbdSbCount`
|
||||
- `gyGzCount`
|
||||
- `dyGzCount`
|
||||
- `tqdzCount`
|
||||
- `tqbxCount`
|
||||
- `dyxlCount`
|
||||
- `bqxCount`
|
||||
- `jllCount`
|
||||
- `bhxCount`
|
||||
- `qftdCount`
|
||||
|
||||
## Partial Rules
|
||||
`allCount` follows the original package rule:
|
||||
|
||||
- If detail rows are collected but one or more required detail columns cannot be mapped, set `status` to `partial`.
|
||||
- `allCount = wxCount + khcCount + gyGzCount + dyGzCount + sbdSbCount`
|
||||
|
||||
## Partial And Error Rules
|
||||
|
||||
- If detail rows are collected but some rows fail normalization while the canonical detail table remains usable, set `status` to `partial` and record the degraded stage.
|
||||
- If required fields are missing so broadly that canonical detail rows cannot be produced at all, set `status` to `error`.
|
||||
- If the detail table is available but the summary sheet cannot be derived completely, set `status` to `partial`.
|
||||
- If summary derivation cannot start because normalized rows are structurally unusable, set `status` to `error`.
|
||||
- If export generation or report-log writing fails after collection succeeds, keep the artifact and record the downstream failure in `partial_reasons`.
|
||||
- Do not silently drop required columns.
|
||||
|
||||
|
||||
@@ -1,75 +1,791 @@
|
||||
const REPORT_NAME = 'fault-details-report';
|
||||
const REPORT_LABEL = '故障明细';
|
||||
const GSSGS = '甘肃省电力公司';
|
||||
|
||||
const DETAIL_COLUMNS = [
|
||||
"qxdbh",
|
||||
"gssgs",
|
||||
"sgs",
|
||||
"gddw",
|
||||
"gds",
|
||||
"slsj",
|
||||
"yjflMc",
|
||||
"ejflMc",
|
||||
"sjflMc",
|
||||
"gzms",
|
||||
"yhbh",
|
||||
"yhmc",
|
||||
"lxr",
|
||||
"gzdd",
|
||||
"lxdh",
|
||||
"bxsj",
|
||||
"gdsj",
|
||||
"clzt",
|
||||
"qxxcjl",
|
||||
"bdz",
|
||||
"line",
|
||||
"pb",
|
||||
"sxfl1",
|
||||
"sxfl2",
|
||||
"sxfl3",
|
||||
"gzsb",
|
||||
"gzyy",
|
||||
"bz"
|
||||
'qxdbh',
|
||||
'gssgs',
|
||||
'sgs',
|
||||
'gddw',
|
||||
'gds',
|
||||
'slsj',
|
||||
'yjflMc',
|
||||
'ejflMc',
|
||||
'sjflMc',
|
||||
'gzms',
|
||||
'yhbh',
|
||||
'yhmc',
|
||||
'lxr',
|
||||
'gzdd',
|
||||
'lxdh',
|
||||
'bxsj',
|
||||
'gdsj',
|
||||
'clzt',
|
||||
'qxxcjl',
|
||||
'bdz',
|
||||
'line',
|
||||
'pb',
|
||||
'sxfl1',
|
||||
'sxfl2',
|
||||
'sxfl3',
|
||||
'gzsb',
|
||||
'gzyy',
|
||||
'bz'
|
||||
];
|
||||
|
||||
const SUMMARY_COLUMNS = [
|
||||
"index",
|
||||
"gsName",
|
||||
"fwDept",
|
||||
"className",
|
||||
"allCount",
|
||||
"wxCount",
|
||||
"khcCount",
|
||||
"sbdSbCount",
|
||||
"gyGzCount",
|
||||
"dyGzCount",
|
||||
"tqdzCount",
|
||||
"tqbxCount",
|
||||
"dyxlCount",
|
||||
"bqxCount",
|
||||
"jllCount",
|
||||
"bhxCount",
|
||||
"qftdCount"
|
||||
'index',
|
||||
'gsName',
|
||||
'fwDept',
|
||||
'className',
|
||||
'allCount',
|
||||
'wxCount',
|
||||
'khcCount',
|
||||
'sbdSbCount',
|
||||
'gyGzCount',
|
||||
'dyGzCount',
|
||||
'tqdzCount',
|
||||
'tqbxCount',
|
||||
'dyxlCount',
|
||||
'bqxCount',
|
||||
'jllCount',
|
||||
'bhxCount',
|
||||
'qftdCount'
|
||||
];
|
||||
|
||||
function collectFaultDetails(input = {}) {
|
||||
return {
|
||||
type: "report-artifact",
|
||||
report_name: "fault-details-report",
|
||||
period: input.period || "",
|
||||
columns: DETAIL_COLUMNS,
|
||||
rows: [],
|
||||
const CLASSIFICATION_RULES = [
|
||||
{
|
||||
name: '欠费停电',
|
||||
reList: ['欠费', '余额不足'],
|
||||
type: '用户侧',
|
||||
dwcFl: ''
|
||||
},
|
||||
{
|
||||
name: '客户内部故障',
|
||||
reList: ['客户内部故障'],
|
||||
type: '用户侧',
|
||||
dwcFl: ''
|
||||
},
|
||||
{
|
||||
name: '表后线',
|
||||
reList: ['表后线'],
|
||||
type: '用户侧',
|
||||
dwcFl: ''
|
||||
},
|
||||
{
|
||||
name: '输变电设备',
|
||||
reList: ['输变电设备'],
|
||||
type: '电网侧',
|
||||
dwcFl: '输变电设备'
|
||||
},
|
||||
{
|
||||
name: '高压故障',
|
||||
reList: [
|
||||
'电杆(塔)',
|
||||
'杆(塔)基础',
|
||||
'连接线-架空线路',
|
||||
'"连接线-高压计量设备"',
|
||||
'柱上隔离开关-架空线路',
|
||||
'横担-架空线路',
|
||||
'绝缘子-架空线路',
|
||||
'导线-架空线路',
|
||||
'跌落式熔断器',
|
||||
'柱上断路器',
|
||||
'二次及自动化装置',
|
||||
'柱上负荷开关',
|
||||
'金具-架空线路',
|
||||
'高压电容器',
|
||||
'避雷装置-架空线路',
|
||||
'避雷装置-电缆线路',
|
||||
'避雷装置-配电站房设备',
|
||||
'接地装置-架空线路',
|
||||
'接地装置-电缆线路',
|
||||
'接地装置-配电站房设备',
|
||||
'电缆本体-电缆线路',
|
||||
'电缆终端头-电缆线路',
|
||||
'电缆中间接头-电缆线路',
|
||||
'油浸式变压器',
|
||||
'干式变压器',
|
||||
'隔离开关-配电站房设备',
|
||||
'基础-配电站房设备',
|
||||
'端子排-配电站房设备',
|
||||
'电流互感器-配电站房设备',
|
||||
'电流互感器-高压计量设备',
|
||||
'电压互感器',
|
||||
'穿墙套管',
|
||||
'母排-配电站房设备',
|
||||
'熔断器-配电站房设备',
|
||||
'断路器-配电站房设备',
|
||||
'负荷开关',
|
||||
'计量表计-高压计量设备',
|
||||
'接线端子盒',
|
||||
'计量柜'
|
||||
],
|
||||
type: '电网侧',
|
||||
dwcFl: '高压故障'
|
||||
},
|
||||
{
|
||||
name: '台区刀闸',
|
||||
reList: ['柱上隔离开关-低压架空线路', '电缆终端头', '隔离开关-低压设备'],
|
||||
type: '电网侧',
|
||||
dwcFl: '低压故障'
|
||||
},
|
||||
{
|
||||
name: '台区保险',
|
||||
reList: ['绝缘子-低压架空线路', '熔断器-低压设备', '断路器-低压设备', '漏电保护器'],
|
||||
type: '电网侧',
|
||||
dwcFl: '低压故障'
|
||||
},
|
||||
{
|
||||
name: '低压线路',
|
||||
reList: [
|
||||
'连接线-低压架空线路',
|
||||
'导线-低压架空线路',
|
||||
'横担-低压架空线路',
|
||||
'金具-低压架空线路',
|
||||
'避雷装置-低压架空线路',
|
||||
'避雷装置-低压电缆线路路',
|
||||
'避雷装置-低压设备',
|
||||
'接地装置-低压架空线路',
|
||||
'接地装置-低压设备',
|
||||
'电缆本体-低压电缆线路路',
|
||||
'电缆终端头-低压电缆线路路',
|
||||
'电缆中间接头-低压电缆线路路',
|
||||
'基础-低压架空线路',
|
||||
'基础-低压设备',
|
||||
'端子排-低压设备',
|
||||
'电流互感器-低压设备',
|
||||
'母排-低压设备',
|
||||
'接户线',
|
||||
'电杆',
|
||||
'连接装置',
|
||||
'电容器',
|
||||
'交流接触器',
|
||||
'低压母线槽',
|
||||
'频率异常',
|
||||
'谐波异常',
|
||||
'电压异常'
|
||||
],
|
||||
type: '电网侧',
|
||||
dwcFl: '低压故障'
|
||||
},
|
||||
{
|
||||
name: '表前线',
|
||||
reList: ['连接线-低压计量设备', '接线端子', '主干线', '表前线'],
|
||||
type: '电网侧',
|
||||
dwcFl: '低压故障'
|
||||
},
|
||||
{
|
||||
name: '计量类',
|
||||
reList: ['电流互感器-低压计量设备', '计量表计-低压计量设备', '计量箱(柜)', '表前开关(熔丝)'],
|
||||
type: '电网侧',
|
||||
dwcFl: '低压故障'
|
||||
}
|
||||
];
|
||||
|
||||
function isNonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim() !== '';
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(...values) {
|
||||
for (const value of values) {
|
||||
if (isNonEmptyString(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractFaultReason(qxxcjl) {
|
||||
const text = normalizeText(qxxcjl);
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const directReasons = [
|
||||
['客户原因导致表计烧损', '表计烧损'],
|
||||
['客户原因导致表前线断线', '表前线断线'],
|
||||
['客户原因导致表前开关跳闸', '表前开关跳闸'],
|
||||
['客户表后线烧损', '表后线烧损'],
|
||||
['表后线烧损', '表后线烧损']
|
||||
];
|
||||
for (const [needle, reason] of directReasons) {
|
||||
if (text.includes(needle)) {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
const markers = [
|
||||
['故障现象是', [',', '。', ';', ';']],
|
||||
['经查', ['。', ',', ';', ';']]
|
||||
];
|
||||
for (const [marker, stops] of markers) {
|
||||
const index = text.indexOf(marker);
|
||||
if (index === -1) {
|
||||
continue;
|
||||
}
|
||||
let content = text.slice(index + marker.length).trim();
|
||||
content = content.replace(/^[::,,\s]+/, '');
|
||||
let end = content.length;
|
||||
for (const stop of stops) {
|
||||
const stopIndex = content.indexOf(stop);
|
||||
if (stopIndex !== -1 && stopIndex < end) {
|
||||
end = stopIndex;
|
||||
}
|
||||
}
|
||||
const reason = content.slice(0, end).trim();
|
||||
if (reason) {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function matchClassification(raw) {
|
||||
const sjflMc = normalizeText(raw.sjflMc);
|
||||
const ejflMc = normalizeText(raw.ejflMc);
|
||||
const qxxcjl = normalizeText(raw.qxxcjl);
|
||||
|
||||
let result = {
|
||||
sxfl1: '无效',
|
||||
sxfl2: '',
|
||||
sxfl3: '',
|
||||
gzsb: '',
|
||||
gzyy: '',
|
||||
fl4: '',
|
||||
dwcFl: ''
|
||||
};
|
||||
|
||||
if (!sjflMc || sjflMc === '无故障') {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const rule of CLASSIFICATION_RULES) {
|
||||
for (const matcher of rule.reList) {
|
||||
if (matcher.includes('-')) {
|
||||
const [threeName, twoName] = matcher.split('-', 2);
|
||||
if (!sjflMc.includes(threeName) || ejflMc !== twoName) {
|
||||
continue;
|
||||
}
|
||||
} else if (!sjflMc.includes(matcher)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result = {
|
||||
sxfl1: '有效',
|
||||
sxfl2: rule.type,
|
||||
sxfl3: rule.dwcFl,
|
||||
gzsb: rule.name,
|
||||
gzyy: extractFaultReason(qxxcjl),
|
||||
fl4: rule.name,
|
||||
dwcFl: rule.dwcFl
|
||||
};
|
||||
|
||||
if (sjflMc.includes('计量表计') && ejflMc === '低压计量设备' && qxxcjl.includes('客户原因导致表计烧损')) {
|
||||
result.sxfl2 = '用户侧';
|
||||
result.fl4 = '客户内部故障';
|
||||
}
|
||||
if (sjflMc.includes('表前线') && ejflMc === '低压计量设备' && qxxcjl.includes('客户原因导致表前线断线')) {
|
||||
result.sxfl2 = '用户侧';
|
||||
result.fl4 = '客户内部故障';
|
||||
}
|
||||
if (matcher === '表前开关(熔丝)' && qxxcjl.includes('客户原因导致表前开关跳闸')) {
|
||||
result.sxfl2 = '用户侧';
|
||||
result.fl4 = '客户内部故障';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeDetailRow(raw, context = {}) {
|
||||
const qxdbh = pickFirstNonEmpty(raw.qxdbh, raw.id);
|
||||
const bxsj = pickFirstNonEmpty(raw.bxsj);
|
||||
if (!qxdbh || !bxsj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const companyName = pickFirstNonEmpty(context.companyName, raw.cityName);
|
||||
const sgs = companyName.includes('嘉峪关')
|
||||
? companyName
|
||||
: pickFirstNonEmpty(raw.cityName, companyName);
|
||||
const classification = matchClassification(raw);
|
||||
|
||||
const row = {
|
||||
qxdbh,
|
||||
gssgs: GSSGS,
|
||||
sgs,
|
||||
gddw: pickFirstNonEmpty(raw.maintOrgName),
|
||||
gds: pickFirstNonEmpty(raw.maintGroupName),
|
||||
slsj: bxsj,
|
||||
yjflMc: pickFirstNonEmpty(raw.yjflMc),
|
||||
ejflMc: pickFirstNonEmpty(raw.ejflMc),
|
||||
sjflMc: pickFirstNonEmpty(raw.sjflMc),
|
||||
gzms: pickFirstNonEmpty(raw.gzms),
|
||||
yhbh: pickFirstNonEmpty(raw.yhbh, raw.consNo),
|
||||
yhmc: pickFirstNonEmpty(raw.yhmc, raw.consName),
|
||||
lxr: pickFirstNonEmpty(raw.lxr, raw.linkMan),
|
||||
gzdd: pickFirstNonEmpty(raw.gzdd, raw.gzAddress),
|
||||
lxdh: pickFirstNonEmpty(raw.lxdh, raw.linkPhone),
|
||||
bxsj,
|
||||
gdsj: pickFirstNonEmpty(raw.gdsj),
|
||||
clzt: '处理完成',
|
||||
qxxcjl: pickFirstNonEmpty(raw.qxxcjl),
|
||||
bdz: pickFirstNonEmpty(raw.bdzMc),
|
||||
line: pickFirstNonEmpty(raw.xlmc10),
|
||||
pb: pickFirstNonEmpty(raw.byqmc),
|
||||
sxfl1: classification.sxfl1,
|
||||
sxfl2: classification.sxfl2,
|
||||
sxfl3: classification.sxfl3,
|
||||
gzsb: classification.gzsb,
|
||||
gzyy: classification.gzyy,
|
||||
bz: pickFirstNonEmpty(raw.bz),
|
||||
fl4: classification.fl4,
|
||||
dwcFl: classification.dwcFl,
|
||||
f14: classification.gzsb
|
||||
};
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function deriveSummaryRows(detailRows, context = {}) {
|
||||
const grouped = new Map();
|
||||
for (const row of detailRows) {
|
||||
const key = pickFirstNonEmpty(row.gds, '未分组');
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
grouped.get(key).push(row);
|
||||
}
|
||||
|
||||
const companyName = pickFirstNonEmpty(context.companyName);
|
||||
return Array.from(grouped.entries()).map(([groupName, rows], index) => {
|
||||
const summaryRow = {
|
||||
index: index + 1,
|
||||
gsName: companyName || pickFirstNonEmpty(rows[0]?.sgs),
|
||||
fwDept: companyName === '国网嘉峪关供电公司' ? '供电服务部' : pickFirstNonEmpty(rows[0]?.gddw),
|
||||
className: companyName === '国网嘉峪关供电公司' ? '急修班' : groupName,
|
||||
wxCount: rows.filter((item) => item.sxfl1 === '无效').length,
|
||||
khcCount: rows.filter((item) => item.sxfl2 === '用户侧' && pickFirstNonEmpty(item.f14, item.gzsb) !== '表后线' && pickFirstNonEmpty(item.fl4, item.gzsb) !== '欠费停电').length,
|
||||
sbdSbCount: rows.filter((item) => item.sxfl2 === '电网侧' && item.dwcFl === '输变电设备').length,
|
||||
gyGzCount: rows.filter((item) => item.sxfl2 === '电网侧' && item.dwcFl === '高压故障').length,
|
||||
dyGzCount: rows.filter((item) => item.sxfl2 === '电网侧' && item.dwcFl === '低压故障').length,
|
||||
tqdzCount: rows.filter((item) => item.gzsb === '台区刀闸').length,
|
||||
tqbxCount: rows.filter((item) => item.gzsb === '台区保险').length,
|
||||
dyxlCount: rows.filter((item) => item.gzsb === '低压线路').length,
|
||||
bqxCount: rows.filter((item) => item.gzsb === '表前线').length,
|
||||
jllCount: rows.filter((item) => item.gzsb === '计量类').length,
|
||||
bhxCount: rows.filter((item) => item.gzsb === '表后线').length,
|
||||
qftdCount: rows.filter((item) => item.gzsb === '欠费停电').length
|
||||
};
|
||||
summaryRow.allCount = summaryRow.wxCount + summaryRow.khcCount + summaryRow.sbdSbCount + summaryRow.gyGzCount + summaryRow.dyGzCount;
|
||||
return summaryRow;
|
||||
});
|
||||
}
|
||||
|
||||
function determineArtifactStatus({ blockedReason, fatalError, partialReasons = [], detailRows = [] }) {
|
||||
if (blockedReason) {
|
||||
return 'blocked';
|
||||
}
|
||||
if (fatalError) {
|
||||
return 'error';
|
||||
}
|
||||
if (partialReasons.length > 0) {
|
||||
return 'partial';
|
||||
}
|
||||
if (!detailRows.length) {
|
||||
return 'empty';
|
||||
}
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function buildFaultDetailsArtifact({
|
||||
period = '',
|
||||
selectedRange,
|
||||
detailRows = [],
|
||||
summaryRows = [],
|
||||
partialReasons = [],
|
||||
blockedReason = '',
|
||||
fatalError = '',
|
||||
downstream
|
||||
}) {
|
||||
const reasons = Array.from(new Set([
|
||||
...partialReasons.filter(Boolean),
|
||||
blockedReason || null,
|
||||
fatalError || null
|
||||
].filter(Boolean)));
|
||||
const status = determineArtifactStatus({
|
||||
blockedReason,
|
||||
fatalError,
|
||||
partialReasons: reasons,
|
||||
detailRows
|
||||
});
|
||||
|
||||
const artifact = {
|
||||
type: 'report-artifact',
|
||||
report_name: REPORT_NAME,
|
||||
period,
|
||||
columns: DETAIL_COLUMNS.slice(),
|
||||
rows: detailRows,
|
||||
sections: [
|
||||
{
|
||||
name: "summary-sheet",
|
||||
columns: SUMMARY_COLUMNS,
|
||||
rows: []
|
||||
name: 'summary-sheet',
|
||||
columns: SUMMARY_COLUMNS.slice(),
|
||||
rows: summaryRows
|
||||
}
|
||||
],
|
||||
status: "ok",
|
||||
partial_reasons: []
|
||||
counts: {
|
||||
detail_rows: detailRows.length,
|
||||
summary_rows: summaryRows.length
|
||||
},
|
||||
status,
|
||||
partial_reasons: reasons
|
||||
};
|
||||
|
||||
if (selectedRange) {
|
||||
artifact.selected_range = {
|
||||
start: pickFirstNonEmpty(selectedRange.start),
|
||||
end: pickFirstNonEmpty(selectedRange.end)
|
||||
};
|
||||
}
|
||||
if (downstream) {
|
||||
artifact.downstream = downstream;
|
||||
}
|
||||
|
||||
return artifact;
|
||||
}
|
||||
|
||||
function buildExportTitles(monthLabel) {
|
||||
const suffix = monthLabel ? `(${monthLabel})` : '';
|
||||
return [
|
||||
{
|
||||
sheet: `${REPORT_LABEL}${suffix}`,
|
||||
cols: DETAIL_COLUMNS.map((key) => [key, key])
|
||||
},
|
||||
{
|
||||
sheet: `汇总${suffix}`,
|
||||
cols: SUMMARY_COLUMNS.map((key) => [key, key])
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function buildEmptyExportRow() {
|
||||
return { qxdbh: '今日无数据' };
|
||||
}
|
||||
|
||||
function buildExportPayload({ detailRows, summaryRows, context, selectedRange }) {
|
||||
const end = pickFirstNonEmpty(selectedRange?.end);
|
||||
const monthLabel = end && /^\d{4}-\d{2}/.test(end) ? end.slice(5, 7) : '';
|
||||
const reportDay = end ? end.slice(5, 10).replace('-', '月') + '日' : '';
|
||||
const companyName = pickFirstNonEmpty(context.companyName, '国网甘肃供电公司');
|
||||
return {
|
||||
titles: buildExportTitles(monthLabel),
|
||||
date: `${companyName}故障报修明细表(${reportDay})`,
|
||||
datas: [detailRows.length > 0 ? detailRows : [buildEmptyExportRow()], summaryRows]
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DETAIL_COLUMNS,
|
||||
SUMMARY_COLUMNS,
|
||||
collectFaultDetails
|
||||
};
|
||||
async function postJson(url, payload) {
|
||||
if (typeof fetch === 'function') {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${url} responded with ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const jq = globalThis.$;
|
||||
if (jq && typeof jq.ajax === 'function') {
|
||||
return new Promise((resolve, reject) => {
|
||||
jq.ajax({
|
||||
url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
crossDomain: true,
|
||||
data: JSON.stringify(payload),
|
||||
contentType: 'application/json',
|
||||
success: resolve,
|
||||
error: (_xhr, _status, err) => reject(err || new Error(`request failed: ${url}`))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`no http client available for ${url}`);
|
||||
}
|
||||
|
||||
function defaultBrowserDeps() {
|
||||
return {
|
||||
async readSelectedRange() {
|
||||
if (Array.isArray(globalThis.mac?.dayRange) && globalThis.mac.dayRange.length >= 2) {
|
||||
return {
|
||||
start: pickFirstNonEmpty(globalThis.mac.dayRange[0]),
|
||||
end: pickFirstNonEmpty(globalThis.mac.dayRange[1])
|
||||
};
|
||||
}
|
||||
|
||||
const startInput = globalThis.document?.querySelector?.('input[placeholder*="开始"], input[placeholder*="start"]');
|
||||
const endInput = globalThis.document?.querySelector?.('input[placeholder*="结束"], input[placeholder*="end"]');
|
||||
const start = pickFirstNonEmpty(startInput?.value);
|
||||
const end = pickFirstNonEmpty(endInput?.value);
|
||||
if (start && end) {
|
||||
return { start, end };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
readCompanyContext() {
|
||||
return {
|
||||
companyName: pickFirstNonEmpty(globalThis.mac?.infoObj?.cityName, globalThis.cityName)
|
||||
};
|
||||
},
|
||||
|
||||
async queryFaultRows() {
|
||||
if (Array.isArray(globalThis.__faultDetailsRawRows)) {
|
||||
return globalThis.__faultDetailsRawRows;
|
||||
}
|
||||
if (Array.isArray(globalThis.mac?.rawFaultRows)) {
|
||||
return globalThis.mac.rawFaultRows;
|
||||
}
|
||||
if (Array.isArray(globalThis.mac?.fdList)) {
|
||||
return globalThis.mac.fdList;
|
||||
}
|
||||
throw new Error('fault_row_query_unavailable');
|
||||
},
|
||||
|
||||
async exportWorkbook(payload) {
|
||||
const result = await postJson(
|
||||
'http://localhost:13313/SurfaceServices/personalBread/export/faultDetailsExportXLSXS',
|
||||
payload
|
||||
);
|
||||
return {
|
||||
path: pickFirstNonEmpty(result?.data?.data, result?.data?.path, result?.path)
|
||||
};
|
||||
},
|
||||
|
||||
async writeReportLog(reportName, path) {
|
||||
const account = pickFirstNonEmpty(globalThis.localStorage?.userinfo ? JSON.parse(globalThis.localStorage.userinfo).account : '');
|
||||
const payload = {
|
||||
account,
|
||||
type: 'fault-details-report',
|
||||
reportName,
|
||||
reportInfo: '',
|
||||
path
|
||||
};
|
||||
const result = await postJson('http://localhost:13313/ReportServices/Api/setReportLog', payload);
|
||||
return {
|
||||
success: result?.msg === 'success' || result?.success === true || result?.code === 200,
|
||||
raw: result
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildReportName(context, selectedRange) {
|
||||
const companyName = pickFirstNonEmpty(context.companyName, '国网甘肃供电公司');
|
||||
const end = pickFirstNonEmpty(selectedRange?.end);
|
||||
const reportDay = end ? end.slice(5, 10).replace('-', '月') + '日' : '';
|
||||
return `${companyName}故障报修明细表(${reportDay})`;
|
||||
}
|
||||
|
||||
async function buildBrowserEntrypointResult(input = {}, deps = defaultBrowserDeps()) {
|
||||
const period = pickFirstNonEmpty(input.period);
|
||||
const partialReasons = [];
|
||||
let blockedReason = '';
|
||||
let fatalError = '';
|
||||
let selectedRange;
|
||||
let detailRows = [];
|
||||
let summaryRows = [];
|
||||
let downstream;
|
||||
|
||||
try {
|
||||
const context = deps.readCompanyContext ? await deps.readCompanyContext(input) : {};
|
||||
selectedRange = deps.readSelectedRange ? await deps.readSelectedRange(input, context) : null;
|
||||
if (!selectedRange || !pickFirstNonEmpty(selectedRange.start) || !pickFirstNonEmpty(selectedRange.end)) {
|
||||
blockedReason = 'selected_range_unavailable';
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
partialReasons,
|
||||
blockedReason
|
||||
});
|
||||
}
|
||||
|
||||
let rawRows;
|
||||
try {
|
||||
rawRows = deps.queryFaultRows
|
||||
? await deps.queryFaultRows({ input, selectedRange, context })
|
||||
: [];
|
||||
} catch (error) {
|
||||
const message = pickFirstNonEmpty(error?.message, String(error));
|
||||
if (message === 'fault_row_query_unavailable') {
|
||||
blockedReason = 'fault_row_query_unavailable';
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
partialReasons,
|
||||
blockedReason
|
||||
});
|
||||
}
|
||||
fatalError = 'fault_row_query_failed';
|
||||
partialReasons.push('fault_row_query_failed');
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
partialReasons,
|
||||
fatalError
|
||||
});
|
||||
}
|
||||
|
||||
const filteredRows = Array.isArray(rawRows)
|
||||
? rawRows.filter((item) => item?.statusName !== '回退营销')
|
||||
: [];
|
||||
if (!Array.isArray(rawRows)) {
|
||||
fatalError = 'fault_row_payload_invalid';
|
||||
partialReasons.push('fault_row_payload_invalid');
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
partialReasons,
|
||||
fatalError
|
||||
});
|
||||
}
|
||||
|
||||
let invalidCount = 0;
|
||||
detailRows = filteredRows
|
||||
.map((row) => {
|
||||
const normalized = normalizeDetailRow(row, context);
|
||||
if (!normalized) {
|
||||
invalidCount += 1;
|
||||
}
|
||||
return normalized;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (filteredRows.length > 0 && detailRows.length === 0) {
|
||||
fatalError = 'detail_normalization_failed';
|
||||
partialReasons.push('detail_normalization_failed');
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
partialReasons,
|
||||
fatalError,
|
||||
detailRows,
|
||||
summaryRows
|
||||
});
|
||||
}
|
||||
if (invalidCount > 0) {
|
||||
partialReasons.push('detail_normalization_partial');
|
||||
}
|
||||
|
||||
try {
|
||||
summaryRows = deriveSummaryRows(detailRows, context);
|
||||
} catch (_error) {
|
||||
if (detailRows.length === 0) {
|
||||
fatalError = 'summary_derivation_failed';
|
||||
} else {
|
||||
partialReasons.push('summary_derivation_failed');
|
||||
summaryRows = [];
|
||||
}
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
detailRows,
|
||||
summaryRows,
|
||||
partialReasons,
|
||||
fatalError
|
||||
});
|
||||
}
|
||||
|
||||
if (deps.exportWorkbook) {
|
||||
downstream = {
|
||||
export: { attempted: true, success: false },
|
||||
report_log: { attempted: false, success: false }
|
||||
};
|
||||
try {
|
||||
const exportPayload = buildExportPayload({ detailRows, summaryRows, context, selectedRange });
|
||||
const exportResult = await deps.exportWorkbook(exportPayload, { input, selectedRange, context, detailRows, summaryRows });
|
||||
downstream.export.success = true;
|
||||
downstream.export.path = pickFirstNonEmpty(exportResult?.path);
|
||||
|
||||
if (deps.writeReportLog) {
|
||||
downstream.report_log.attempted = true;
|
||||
try {
|
||||
const reportName = buildReportName(context, selectedRange);
|
||||
const reportLogResult = await deps.writeReportLog(reportName, downstream.export.path, {
|
||||
input,
|
||||
selectedRange,
|
||||
context,
|
||||
detailRows,
|
||||
summaryRows
|
||||
});
|
||||
downstream.report_log.success = reportLogResult?.success !== false;
|
||||
downstream.report_log.report_name = reportName;
|
||||
downstream.report_log.path = downstream.export.path;
|
||||
if (!downstream.report_log.success) {
|
||||
partialReasons.push('report_log_failed');
|
||||
downstream.report_log.error = pickFirstNonEmpty(reportLogResult?.error, 'report_log_failed');
|
||||
}
|
||||
} catch (error) {
|
||||
partialReasons.push('report_log_failed');
|
||||
downstream.report_log.success = false;
|
||||
downstream.report_log.error = pickFirstNonEmpty(error?.message, String(error));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
partialReasons.push('export_failed');
|
||||
downstream.export.success = false;
|
||||
downstream.export.error = pickFirstNonEmpty(error?.message, String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
detailRows,
|
||||
summaryRows,
|
||||
partialReasons,
|
||||
downstream
|
||||
});
|
||||
} catch (error) {
|
||||
fatalError = pickFirstNonEmpty(error?.message, 'fault_details_unhandled_error');
|
||||
if (!partialReasons.includes(fatalError)) {
|
||||
partialReasons.push(fatalError);
|
||||
}
|
||||
return buildFaultDetailsArtifact({
|
||||
period,
|
||||
selectedRange,
|
||||
detailRows,
|
||||
summaryRows,
|
||||
partialReasons,
|
||||
fatalError
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
DETAIL_COLUMNS,
|
||||
SUMMARY_COLUMNS,
|
||||
CLASSIFICATION_RULES,
|
||||
extractFaultReason,
|
||||
normalizeDetailRow,
|
||||
deriveSummaryRows,
|
||||
determineArtifactStatus,
|
||||
buildFaultDetailsArtifact,
|
||||
buildExportPayload,
|
||||
buildBrowserEntrypointResult
|
||||
};
|
||||
} else {
|
||||
return buildBrowserEntrypointResult(args);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
DETAIL_COLUMNS,
|
||||
SUMMARY_COLUMNS,
|
||||
normalizeDetailRow,
|
||||
deriveSummaryRows,
|
||||
determineArtifactStatus,
|
||||
buildFaultDetailsArtifact,
|
||||
buildBrowserEntrypointResult
|
||||
} = require('./collect_fault_details.js');
|
||||
|
||||
test('exports canonical detail and summary columns', () => {
|
||||
assert.ok(Array.isArray(DETAIL_COLUMNS));
|
||||
assert.ok(Array.isArray(SUMMARY_COLUMNS));
|
||||
assert.ok(DETAIL_COLUMNS.includes('qxdbh'));
|
||||
assert.ok(DETAIL_COLUMNS.includes('gzyy'));
|
||||
assert.ok(SUMMARY_COLUMNS.includes('allCount'));
|
||||
assert.ok(SUMMARY_COLUMNS.includes('qftdCount'));
|
||||
});
|
||||
|
||||
test('normalizeDetailRow maps canonical detail fields from raw repair rows', () => {
|
||||
const row = normalizeDetailRow(
|
||||
{
|
||||
qxdbh: 'QX-1',
|
||||
bxsj: '2026-03-09 08:00:00',
|
||||
cityName: '国网兰州供电公司',
|
||||
maintOrgName: '城关供电服务班',
|
||||
maintGroupName: '抢修一班',
|
||||
bdzMc: '110kV东岗变',
|
||||
xlmc10: '10kV东岗线',
|
||||
byqmc: '东岗1号变',
|
||||
yjflMc: '电网故障',
|
||||
ejflMc: '线路故障',
|
||||
sjflMc: '低压线路',
|
||||
qxxcjl: '现场检查:低压线路断线,已处理完成',
|
||||
gzms: '客户报修停电'
|
||||
},
|
||||
{
|
||||
companyName: '国网兰州供电公司'
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(row.slsj, '2026-03-09 08:00:00');
|
||||
assert.equal(row.gssgs, '甘肃省电力公司');
|
||||
assert.equal(row.gddw, '城关供电服务班');
|
||||
assert.equal(row.gds, '抢修一班');
|
||||
assert.equal(row.clzt, '处理完成');
|
||||
assert.equal(row.bdz, '110kV东岗变');
|
||||
assert.equal(row.line, '10kV东岗线');
|
||||
assert.equal(row.pb, '东岗1号变');
|
||||
});
|
||||
|
||||
test('deriveSummaryRows groups normalized rows by gds and computes counters', () => {
|
||||
const rows = [
|
||||
{
|
||||
gds: '抢修一班',
|
||||
gddw: '城关供电服务班',
|
||||
sgs: '国网兰州供电公司',
|
||||
sxfl1: '无效',
|
||||
sxfl2: '无效',
|
||||
gzsb: ''
|
||||
},
|
||||
{
|
||||
gds: '抢修一班',
|
||||
gddw: '城关供电服务班',
|
||||
sgs: '国网兰州供电公司',
|
||||
sxfl1: '有效',
|
||||
sxfl2: '用户侧',
|
||||
gzsb: '表后线'
|
||||
},
|
||||
{
|
||||
gds: '抢修一班',
|
||||
gddw: '城关供电服务班',
|
||||
sgs: '国网兰州供电公司',
|
||||
sxfl1: '有效',
|
||||
sxfl2: '电网侧',
|
||||
dwcFl: '低压故障',
|
||||
gzsb: '低压线路'
|
||||
}
|
||||
];
|
||||
|
||||
const summaryRows = deriveSummaryRows(rows, { companyName: '国网兰州供电公司' });
|
||||
assert.equal(summaryRows.length, 1);
|
||||
assert.equal(summaryRows[0].className, '抢修一班');
|
||||
assert.equal(summaryRows[0].allCount, 2);
|
||||
assert.equal(summaryRows[0].wxCount, 1);
|
||||
assert.equal(summaryRows[0].khcCount, 0);
|
||||
assert.equal(summaryRows[0].dyGzCount, 1);
|
||||
assert.equal(summaryRows[0].dyxlCount, 1);
|
||||
assert.equal(summaryRows[0].bhxCount, 1);
|
||||
});
|
||||
|
||||
test('determineArtifactStatus follows blocked > error > partial > empty > ok precedence', () => {
|
||||
assert.equal(
|
||||
determineArtifactStatus({
|
||||
blockedReason: 'missing_session',
|
||||
fatalError: null,
|
||||
partialReasons: [],
|
||||
detailRows: [{}]
|
||||
}),
|
||||
'blocked'
|
||||
);
|
||||
assert.equal(
|
||||
determineArtifactStatus({
|
||||
blockedReason: null,
|
||||
fatalError: 'parse_failed',
|
||||
partialReasons: [],
|
||||
detailRows: [{}]
|
||||
}),
|
||||
'error'
|
||||
);
|
||||
assert.equal(
|
||||
determineArtifactStatus({
|
||||
blockedReason: null,
|
||||
fatalError: null,
|
||||
partialReasons: ['export_failed'],
|
||||
detailRows: [{}]
|
||||
}),
|
||||
'partial'
|
||||
);
|
||||
assert.equal(
|
||||
determineArtifactStatus({
|
||||
blockedReason: null,
|
||||
fatalError: null,
|
||||
partialReasons: [],
|
||||
detailRows: []
|
||||
}),
|
||||
'empty'
|
||||
);
|
||||
assert.equal(
|
||||
determineArtifactStatus({
|
||||
blockedReason: null,
|
||||
fatalError: null,
|
||||
partialReasons: [],
|
||||
detailRows: [{}]
|
||||
}),
|
||||
'ok'
|
||||
);
|
||||
});
|
||||
|
||||
test('buildFaultDetailsArtifact keeps canonical fields, selected range, counts, and downstream results', () => {
|
||||
const artifact = buildFaultDetailsArtifact({
|
||||
period: '2026-03',
|
||||
selectedRange: { start: '2026-03-08 16:00:00', end: '2026-03-09 16:00:00' },
|
||||
detailRows: [{ qxdbh: 'QX-1' }],
|
||||
summaryRows: [{ index: 1 }],
|
||||
partialReasons: ['report_log_failed'],
|
||||
downstream: {
|
||||
export: { attempted: true, success: true, path: 'http://localhost/export.xlsx' },
|
||||
report_log: { attempted: true, success: false, error: '500' }
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(artifact.type, 'report-artifact');
|
||||
assert.equal(artifact.status, 'partial');
|
||||
assert.deepEqual(artifact.selected_range, {
|
||||
start: '2026-03-08 16:00:00',
|
||||
end: '2026-03-09 16:00:00'
|
||||
});
|
||||
assert.equal(artifact.counts.detail_rows, 1);
|
||||
assert.equal(artifact.counts.summary_rows, 1);
|
||||
assert.deepEqual(artifact.partial_reasons, ['report_log_failed']);
|
||||
});
|
||||
|
||||
test('buildFaultDetailsArtifact keeps required top-level fields for blocked artifact', () => {
|
||||
const artifact = buildFaultDetailsArtifact({
|
||||
period: '2026-03',
|
||||
blockedReason: 'selected_range_unavailable',
|
||||
partialReasons: ['selected_range_unavailable']
|
||||
});
|
||||
|
||||
assert.equal(artifact.type, 'report-artifact');
|
||||
assert.equal(artifact.report_name, 'fault-details-report');
|
||||
assert.equal(artifact.period, '2026-03');
|
||||
assert.equal(artifact.status, 'blocked');
|
||||
assert.deepEqual(artifact.partial_reasons, ['selected_range_unavailable']);
|
||||
assert.equal('downstream' in artifact, false);
|
||||
});
|
||||
|
||||
test('buildFaultDetailsArtifact keeps known selected range and counts on late error', () => {
|
||||
const artifact = buildFaultDetailsArtifact({
|
||||
period: '2026-03',
|
||||
selectedRange: { start: '2026-03-08 16:00:00', end: '2026-03-09 16:00:00' },
|
||||
detailRows: [],
|
||||
summaryRows: [],
|
||||
fatalError: 'summary_failed',
|
||||
partialReasons: ['summary_failed']
|
||||
});
|
||||
|
||||
assert.equal(artifact.status, 'error');
|
||||
assert.deepEqual(artifact.selected_range, {
|
||||
start: '2026-03-08 16:00:00',
|
||||
end: '2026-03-09 16:00:00'
|
||||
});
|
||||
assert.equal(artifact.counts.detail_rows, 0);
|
||||
assert.equal(artifact.counts.summary_rows, 0);
|
||||
});
|
||||
|
||||
test('normalizeDetailRow derives gzyy from qxxcjl text heuristics', () => {
|
||||
const row = normalizeDetailRow(
|
||||
{
|
||||
qxdbh: 'QX-2',
|
||||
bxsj: '2026-03-09 09:00:00',
|
||||
ejflMc: '客户侧故障',
|
||||
sjflMc: '表后线',
|
||||
qxxcjl: '现场检查:客户表后线烧损,已恢复送电'
|
||||
},
|
||||
{ companyName: '国网兰州供电公司' }
|
||||
);
|
||||
|
||||
assert.equal(row.gzsb, '表后线');
|
||||
assert.equal(row.gzyy, '表后线烧损');
|
||||
});
|
||||
|
||||
test('buildBrowserEntrypointResult returns partial when export fails after detail collection succeeds', async () => {
|
||||
const artifact = await buildBrowserEntrypointResult(
|
||||
{ period: '2026-03' },
|
||||
{
|
||||
readSelectedRange: async () => ({ start: '2026-03-08 16:00:00', end: '2026-03-09 16:00:00' }),
|
||||
queryFaultRows: async () => [
|
||||
{
|
||||
qxdbh: 'QX-1',
|
||||
bxsj: '2026-03-09 08:00:00',
|
||||
maintGroupName: '抢修一班',
|
||||
maintOrgName: '城关供电服务班',
|
||||
cityName: '国网兰州供电公司'
|
||||
}
|
||||
],
|
||||
readCompanyContext: () => ({ companyName: '国网兰州供电公司' }),
|
||||
exportWorkbook: async () => {
|
||||
throw new Error('export_failed');
|
||||
},
|
||||
writeReportLog: async () => ({ success: true })
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(artifact.status, 'partial');
|
||||
assert.ok(artifact.partial_reasons.includes('export_failed'));
|
||||
assert.equal(artifact.counts.detail_rows, 1);
|
||||
assert.equal(artifact.downstream.export.attempted, true);
|
||||
assert.equal(artifact.downstream.export.success, false);
|
||||
});
|
||||
|
||||
test('buildBrowserEntrypointResult returns error when normalized detail rows cannot be produced', async () => {
|
||||
const artifact = await buildBrowserEntrypointResult(
|
||||
{ period: '2026-03' },
|
||||
{
|
||||
readSelectedRange: async () => ({ start: '2026-03-08 16:00:00', end: '2026-03-09 16:00:00' }),
|
||||
queryFaultRows: async () => [{ qxdbh: '', bxsj: '' }],
|
||||
readCompanyContext: () => ({ companyName: '国网兰州供电公司' })
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(artifact.status, 'error');
|
||||
assert.ok(artifact.partial_reasons.includes('detail_normalization_failed'));
|
||||
});
|
||||
|
||||
test('buildBrowserEntrypointResult keeps canonical rows empty for empty result and omits downstream before attempts', async () => {
|
||||
const artifact = await buildBrowserEntrypointResult(
|
||||
{ period: '2026-03' },
|
||||
{
|
||||
readSelectedRange: async () => ({ start: '2026-03-08 16:00:00', end: '2026-03-09 16:00:00' }),
|
||||
queryFaultRows: async () => [],
|
||||
readCompanyContext: () => ({ companyName: '国网兰州供电公司' })
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(artifact.status, 'empty');
|
||||
assert.deepEqual(artifact.rows, []);
|
||||
assert.equal('downstream' in artifact, false);
|
||||
});
|
||||
|
||||
test('buildBrowserEntrypointResult returns blocked artifact when selected range is unavailable', async () => {
|
||||
const artifact = await buildBrowserEntrypointResult(
|
||||
{
|
||||
period: '2026-03'
|
||||
},
|
||||
{
|
||||
readSelectedRange: async () => null
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(artifact.status, 'blocked');
|
||||
assert.ok(artifact.partial_reasons.includes('selected_range_unavailable'));
|
||||
});
|
||||
Reference in New Issue
Block a user