|
|
@@ -44,6 +44,7 @@ import {
|
|
|
emptyKeyTurbinePlaceholder,
|
|
|
formatPercent,
|
|
|
formatDetectorRowMetric,
|
|
|
+ formatSensorTypeText,
|
|
|
formatSummaryMetricValue,
|
|
|
isPoAnomaly,
|
|
|
mapAnomalyCoverFields,
|
|
|
@@ -308,11 +309,10 @@ function sortTurbineDetailImages(images) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-const FARM_OVERVIEW_TEMPLATE_KEYS = new Set([
|
|
|
- "ctrl_power_quality",
|
|
|
- "pitch_regulation",
|
|
|
-]);
|
|
|
-
|
|
|
+/**
|
|
|
+ * 所有检测项的全场总图都优先读 getAnomalyOverview 里的算法预聚合总图,
|
|
|
+ * 与页面总图 Tab 用同一份数据。读不到、不是 JSON、或出图为空时,再回退单机拼接。
|
|
|
+ */
|
|
|
async function loadOverviewFarmJson(overview, templateKey) {
|
|
|
const source = collectFarmGraphPaths(overview)[templateKey];
|
|
|
const parsed = parseFarmGraphSource(source);
|
|
|
@@ -320,7 +320,15 @@ async function loadOverviewFarmJson(overview, templateKey) {
|
|
|
if (parsed.kind !== "url" || !parsed.url) return null;
|
|
|
try {
|
|
|
const media = await fetchRemoteMedia(parsed.url);
|
|
|
- return media?.kind === "json" ? media.data : null;
|
|
|
+ if (media?.kind !== "json") {
|
|
|
+ console.warn(
|
|
|
+ "[anomaly-report] 全场总图非 JSON,回退单机拼接:",
|
|
|
+ templateKey,
|
|
|
+ media?.kind || "empty",
|
|
|
+ );
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return media.data;
|
|
|
} catch (error) {
|
|
|
console.warn(
|
|
|
"[anomaly-report] 全场总图读取失败:",
|
|
|
@@ -346,6 +354,12 @@ function createDetectorState() {
|
|
|
rateCount: 0,
|
|
|
turbineNames: [],
|
|
|
hasChartData: false,
|
|
|
+ // 诊断计数:定位「总图为什么没出来」
|
|
|
+ poMissing: 0,
|
|
|
+ graphPathMissing: 0,
|
|
|
+ mediaImage: 0,
|
|
|
+ mediaFailed: 0,
|
|
|
+ jsonNoData: 0,
|
|
|
};
|
|
|
});
|
|
|
return state;
|
|
|
@@ -456,6 +470,7 @@ export async function generateAnomalyReport(req) {
|
|
|
if (!cfg.poKey) continue;
|
|
|
const po = pickPo(moduleBundle, cfg.poKey);
|
|
|
const state = detectorState[cfg.templateKey];
|
|
|
+ if (!po) state.poMissing += 1;
|
|
|
const points = poAnomalyPoints(po);
|
|
|
const rate = poAnomalyRate(po);
|
|
|
const abnormal = isPoAnomaly(po) || points > 0;
|
|
|
@@ -472,6 +487,7 @@ export async function generateAnomalyReport(req) {
|
|
|
turbine_name: engineName,
|
|
|
anomaly_points: String(points),
|
|
|
anomaly_rate: formatDetectorRowMetric(cfg.templateKey, rate),
|
|
|
+ sensor_anomaly_type: formatSensorTypeText(po),
|
|
|
comment: buildDetectorComment(po, cfg.templateKey),
|
|
|
});
|
|
|
perTurbine.anomalyDetectorCount += 1;
|
|
|
@@ -485,13 +501,20 @@ export async function generateAnomalyReport(req) {
|
|
|
}
|
|
|
|
|
|
const graphPath = po?.graphPath;
|
|
|
- if (!graphPath) continue;
|
|
|
+ if (!graphPath) {
|
|
|
+ state.graphPathMissing += 1;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
const media = await loadMedia(graphPath);
|
|
|
const imageKey = `anomaly_${String(engineId).replace(/[^\w-]/g, "_")}_${
|
|
|
cfg.templateKey
|
|
|
}`;
|
|
|
+ if (!media) {
|
|
|
+ state.mediaFailed += 1;
|
|
|
+ }
|
|
|
if (media?.kind === "image" && media.buffer?.length) {
|
|
|
registerImageBuffer(imageBufferMap, imageKey, media.buffer);
|
|
|
+ state.mediaImage += 1;
|
|
|
state.turbineImages.push({
|
|
|
engineName,
|
|
|
image: imageKey,
|
|
|
@@ -506,6 +529,7 @@ export async function generateAnomalyReport(req) {
|
|
|
});
|
|
|
} else if (media?.kind === "json") {
|
|
|
if (!plotJsonHasData(media.data)) {
|
|
|
+ state.jsonNoData += 1;
|
|
|
continue;
|
|
|
}
|
|
|
state.plotJsons.push({
|
|
|
@@ -582,21 +606,21 @@ export async function generateAnomalyReport(req) {
|
|
|
if (!cfg.poKey) continue;
|
|
|
const farmTitle = `全场${cfg.title}检测汇总`;
|
|
|
let farmOptions = [];
|
|
|
+ let farmSource = "none";
|
|
|
try {
|
|
|
- if (FARM_OVERVIEW_TEMPLATE_KEYS.has(cfg.templateKey)) {
|
|
|
- const farmJson = await loadOverviewFarmJson(overview, cfg.templateKey);
|
|
|
- if (farmJson) {
|
|
|
- farmOptions = listFarmOverviewChartOptions(
|
|
|
- farmJson,
|
|
|
- farmTitle,
|
|
|
- cfg.poKey,
|
|
|
- {
|
|
|
- templateKey: cfg.templateKey,
|
|
|
- farmMode: true,
|
|
|
- nameMap: engineNameMap,
|
|
|
- },
|
|
|
- );
|
|
|
- }
|
|
|
+ const farmJson = await loadOverviewFarmJson(overview, cfg.templateKey);
|
|
|
+ if (farmJson) {
|
|
|
+ farmOptions = listFarmOverviewChartOptions(
|
|
|
+ farmJson,
|
|
|
+ farmTitle,
|
|
|
+ cfg.poKey,
|
|
|
+ {
|
|
|
+ templateKey: cfg.templateKey,
|
|
|
+ farmMode: true,
|
|
|
+ nameMap: engineNameMap,
|
|
|
+ },
|
|
|
+ );
|
|
|
+ if (farmOptions.length) farmSource = "overview";
|
|
|
}
|
|
|
if (!farmOptions.length) {
|
|
|
farmOptions = listFarmChartOptions(
|
|
|
@@ -609,6 +633,7 @@ export async function generateAnomalyReport(req) {
|
|
|
nameMap: engineNameMap,
|
|
|
},
|
|
|
);
|
|
|
+ if (farmOptions.length) farmSource = "stitch";
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.warn(
|
|
|
@@ -619,6 +644,19 @@ export async function generateAnomalyReport(req) {
|
|
|
}
|
|
|
farmOptions = farmOptions.filter((option) => optionHasData(option));
|
|
|
if (farmOptions.length) state.hasChartData = true;
|
|
|
+ if (farmOptions.length) {
|
|
|
+ console.log(
|
|
|
+ `[anomaly-report] 全场总图: ${cfg.title} 源=${farmSource} 张数=${farmOptions.length}`,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ console.warn(
|
|
|
+ `[anomaly-report] 全场总图为空: ${cfg.title}(${cfg.templateKey}) ` +
|
|
|
+ `单机JSON=${state.plotJsons.length} 缺po=${state.poMissing} ` +
|
|
|
+ `缺graphPath=${state.graphPathMissing} 下载失败=${state.mediaFailed} ` +
|
|
|
+ `图片型=${state.mediaImage} JSON无数据=${state.jsonNoData} ` +
|
|
|
+ `分图=${state.turbineImages.length}`,
|
|
|
+ );
|
|
|
+ }
|
|
|
if (state.hasChartData && farmOptions.length) {
|
|
|
try {
|
|
|
for (let i = 0; i < farmOptions.length; i += 1) {
|