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": "故障明细",
|
"name": "故障明细",
|
||||||
"category": "report",
|
"category": "report",
|
||||||
"entry": "index.html",
|
"entry": "index.html",
|
||||||
"summary": "查询故障明细行并生成包含明细与汇总分区的结构化报表产物。",
|
"summary": "查询故障明细行,生成包含明细与汇总分区的结构化报表产物,并记录导出与报表历史结果。",
|
||||||
"inputs": ["period"],
|
"inputs": ["period"],
|
||||||
"outputs": ["report-artifact"],
|
"outputs": ["report-artifact"],
|
||||||
"dependencies": ["browser", "report-history", "local-report-service"],
|
"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"],
|
"tags": ["fault", "report", "xlsx", "details", "browser"],
|
||||||
"skill": {
|
"skill": {
|
||||||
"package": "fault-details-report",
|
"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.
|
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.
|
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.
|
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.
|
4. Apply the original classification table and `qxxcjl`-based reason heuristics to fill `sxfl1`, `sxfl2`, `sxfl3`, `gzsb`, and `gzyy`.
|
||||||
5. Return the structured artifact before any prose summary.
|
5. Derive the summary rows used by the secondary summary sheet.
|
||||||
6. If required columns are missing or summary derivation is incomplete, mark the result as partial.
|
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
|
## Runtime Contract
|
||||||
|
|
||||||
@@ -57,7 +60,11 @@ Do not use this skill for:
|
|||||||
{
|
{
|
||||||
"type": "report-artifact",
|
"type": "report-artifact",
|
||||||
"report_name": "fault-details-report",
|
"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": [
|
"columns": [
|
||||||
"qxdbh",
|
"qxdbh",
|
||||||
"gssgs",
|
"gssgs",
|
||||||
@@ -114,8 +121,24 @@ Do not use this skill for:
|
|||||||
"rows": []
|
"rows": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "ok",
|
"counts": {
|
||||||
"partial_reasons": []
|
"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]
|
[skill]
|
||||||
name = "fault-details-report"
|
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"
|
version = "0.1.0"
|
||||||
author = "sgclaw"
|
author = "sgclaw"
|
||||||
tags = ["fault", "report", "xlsx", "details", "browser"]
|
tags = ["fault", "report", "xlsx", "details", "browser"]
|
||||||
@@ -11,6 +11,6 @@ prompts = [
|
|||||||
|
|
||||||
[[tools]]
|
[[tools]]
|
||||||
name = "collect_fault_details"
|
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"
|
kind = "browser_script"
|
||||||
command = "scripts/collect_fault_details.js"
|
command = "scripts/collect_fault_details.js"
|
||||||
|
|||||||
@@ -14,13 +14,17 @@
|
|||||||
## First-pass Collection Steps
|
## First-pass Collection Steps
|
||||||
|
|
||||||
1. Open or attach to the source page.
|
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.
|
3. Ensure the D4 business page session is available.
|
||||||
4. Trigger the repair-order query used by the page logic.
|
4. Trigger the repair-order query used by the page logic.
|
||||||
5. Collect raw fault-detail rows.
|
5. Collect raw fault-detail rows.
|
||||||
6. Normalize rows using the canonical column order declared in `excleIni[0].cols`.
|
6. Filter out rows with `statusName == "回退营销"`.
|
||||||
7. Derive the summary-sheet rows from grouped detail rows.
|
7. Normalize rows using the canonical column order declared in `excleIni[0].cols`.
|
||||||
8. Return a structured artifact before any export-file or prose step.
|
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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,25 @@ These may be blank in some rows but should still be preserved when present:
|
|||||||
- `gzyy`
|
- `gzyy`
|
||||||
- `bz`
|
- `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
|
## Summary-Derivation Expectations
|
||||||
|
|
||||||
The first-pass package should also be able to derive summary rows keyed around:
|
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`
|
- `sbdSbCount`
|
||||||
- `gyGzCount`
|
- `gyGzCount`
|
||||||
- `dyGzCount`
|
- `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 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`.
|
- 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.
|
- Do not silently drop required columns.
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +1,791 @@
|
|||||||
|
const REPORT_NAME = 'fault-details-report';
|
||||||
|
const REPORT_LABEL = '故障明细';
|
||||||
|
const GSSGS = '甘肃省电力公司';
|
||||||
|
|
||||||
const DETAIL_COLUMNS = [
|
const DETAIL_COLUMNS = [
|
||||||
"qxdbh",
|
'qxdbh',
|
||||||
"gssgs",
|
'gssgs',
|
||||||
"sgs",
|
'sgs',
|
||||||
"gddw",
|
'gddw',
|
||||||
"gds",
|
'gds',
|
||||||
"slsj",
|
'slsj',
|
||||||
"yjflMc",
|
'yjflMc',
|
||||||
"ejflMc",
|
'ejflMc',
|
||||||
"sjflMc",
|
'sjflMc',
|
||||||
"gzms",
|
'gzms',
|
||||||
"yhbh",
|
'yhbh',
|
||||||
"yhmc",
|
'yhmc',
|
||||||
"lxr",
|
'lxr',
|
||||||
"gzdd",
|
'gzdd',
|
||||||
"lxdh",
|
'lxdh',
|
||||||
"bxsj",
|
'bxsj',
|
||||||
"gdsj",
|
'gdsj',
|
||||||
"clzt",
|
'clzt',
|
||||||
"qxxcjl",
|
'qxxcjl',
|
||||||
"bdz",
|
'bdz',
|
||||||
"line",
|
'line',
|
||||||
"pb",
|
'pb',
|
||||||
"sxfl1",
|
'sxfl1',
|
||||||
"sxfl2",
|
'sxfl2',
|
||||||
"sxfl3",
|
'sxfl3',
|
||||||
"gzsb",
|
'gzsb',
|
||||||
"gzyy",
|
'gzyy',
|
||||||
"bz"
|
'bz'
|
||||||
];
|
];
|
||||||
|
|
||||||
const SUMMARY_COLUMNS = [
|
const SUMMARY_COLUMNS = [
|
||||||
"index",
|
'index',
|
||||||
"gsName",
|
'gsName',
|
||||||
"fwDept",
|
'fwDept',
|
||||||
"className",
|
'className',
|
||||||
"allCount",
|
'allCount',
|
||||||
"wxCount",
|
'wxCount',
|
||||||
"khcCount",
|
'khcCount',
|
||||||
"sbdSbCount",
|
'sbdSbCount',
|
||||||
"gyGzCount",
|
'gyGzCount',
|
||||||
"dyGzCount",
|
'dyGzCount',
|
||||||
"tqdzCount",
|
'tqdzCount',
|
||||||
"tqbxCount",
|
'tqbxCount',
|
||||||
"dyxlCount",
|
'dyxlCount',
|
||||||
"bqxCount",
|
'bqxCount',
|
||||||
"jllCount",
|
'jllCount',
|
||||||
"bhxCount",
|
'bhxCount',
|
||||||
"qftdCount"
|
'qftdCount'
|
||||||
];
|
];
|
||||||
|
|
||||||
function collectFaultDetails(input = {}) {
|
const CLASSIFICATION_RULES = [
|
||||||
return {
|
{
|
||||||
type: "report-artifact",
|
name: '欠费停电',
|
||||||
report_name: "fault-details-report",
|
reList: ['欠费', '余额不足'],
|
||||||
period: input.period || "",
|
type: '用户侧',
|
||||||
columns: DETAIL_COLUMNS,
|
dwcFl: ''
|
||||||
rows: [],
|
},
|
||||||
|
{
|
||||||
|
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: [
|
sections: [
|
||||||
{
|
{
|
||||||
name: "summary-sheet",
|
name: 'summary-sheet',
|
||||||
columns: SUMMARY_COLUMNS,
|
columns: SUMMARY_COLUMNS.slice(),
|
||||||
rows: []
|
rows: summaryRows
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
status: "ok",
|
counts: {
|
||||||
partial_reasons: []
|
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 = {
|
async function postJson(url, payload) {
|
||||||
DETAIL_COLUMNS,
|
if (typeof fetch === 'function') {
|
||||||
SUMMARY_COLUMNS,
|
const response = await fetch(url, {
|
||||||
collectFaultDetails
|
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