fix: resume incomplete SCADA presentations
This commit is contained in:
@@ -55,6 +55,8 @@ SCADA地图定位、等级标记、表格与图表采用同一目标设备集合
|
|||||||
|
|
||||||
同图最多100横轴槽、8组序列,每组与x_data对齐。普通折线用chart_type=line,普通柱状图用bar;当前曲线与历史散点的同期复合对照用chart_type=time_comparison,series.type分别为line和scatter。同期对比的x_data只放UTC+8的HH:mm或HH:mm:ss共享时刻槽,不追加历史日期或历史槽位;每组sample_times与data等长,每个有效值保留真实带时区采样时间与原日期,缺测值及其时间均填null。当前曲线使用同一窗口,历史每组来自此前某一天的对应窗口,历史点填到同一时刻槽;跨午夜时按窗口时间顺序排列。超限按对象分图或时间分段,保留目标设备覆盖;聚合时说明方法,各单位和各类业务含义分别展示。同主题同分组更新同一chart_id,保留图表供查看;报告保留关键结论和来源。
|
同图最多100横轴槽、8组序列,每组与x_data对齐。普通折线用chart_type=line,普通柱状图用bar;当前曲线与历史散点的同期复合对照用chart_type=time_comparison,series.type分别为line和scatter。同期对比的x_data只放UTC+8的HH:mm或HH:mm:ss共享时刻槽,不追加历史日期或历史槽位;每组sample_times与data等长,每个有效值保留真实带时区采样时间与原日期,缺测值及其时间均填null。当前曲线使用同一窗口,历史每组来自此前某一天的对应窗口,历史点填到同一时刻槽;跨午夜时按窗口时间顺序排列。超限按对象分图或时间分段,保留目标设备覆盖;聚合时说明方法,各单位和各类业务含义分别展示。同主题同分组更新同一chart_id,保留图表供查看;报告保留关键结论和来源。
|
||||||
|
|
||||||
|
SCADA监测过程图填写chart.scada={role:current或comparison,metric:pressure或flow,unit,window_start,window_end},窗口为本轮完整三小时,截止非整刻仍按真实采样网格;超过100槽时连续分段并用不同稳定chart_id,分段scada窗口保持完整三小时,Server合并采样槽校验覆盖;历史散点仅保留完整窗口末三个采样槽;低频设备无计划采样槽时完整查询对应当天及历史窗口,以截止时刻单个null保留入口;每条series显式填写device_id和day_offset(当天0、此前七天1至7)。同期图逐设备保留当天和七天共8组,证据引用覆盖全部当前与历史采样;value_field默认monitored_value。缺测需完整查询证明,再保留null,Server生成scada.gaps;仅供参考的设备仍要展示。报告scada_device_ids列出监测明细表和展示目标的完整设备集合。Server按本轮累计目标校验当前总览及同期图,SCADA_PRESENTATION_INCOMPLETE表示必须按返回设备补图,不能删除目标、关闭窗口或声明不参与结论来绕过;报告和成功结束均校验。浏览器连接及显示回执不参与这项产物校验。
|
||||||
|
|
||||||
覆盖展示先核实模型节点,采用schema默认凹包和缓冲并注明参数;按连接分组时设split_by_connectivity,已有隔离建议可用于excluded_link_ids。只有result.map_payload.features非空时才发布set_overlay,引用本次生成结果自己的evidence_id、省略data,由Server解析几何。返回的data.presentation.command可直接作为command,另生成稳定request_id。缺少部分几何时展示有效部分并说明覆盖;全部缺失时展示可用点位或记录限制。
|
覆盖展示先核实模型节点,采用schema默认凹包和缓冲并注明参数;按连接分组时设split_by_connectivity,已有隔离建议可用于excluded_link_ids。只有result.map_payload.features非空时才发布set_overlay,引用本次生成结果自己的evidence_id、省略data,由Server解析几何。返回的data.presentation.command可直接作为command,另生成稳定request_id。缺少部分几何时展示有效部分并说明覆盖;全部缺失时展示可用点位或记录限制。
|
||||||
|
|
||||||
对象ID、数量、图层与样式遵循schema,坐标使用受控来源。展示失败时修正调用,仍失败则记录具体限制并继续独立分析。recorded表示请求已保存,浏览器跟随、连接和暂停由前端处理,收尾依据分析结果与发布回执。
|
对象ID、数量、图层与样式遵循schema,坐标使用受控来源。展示失败时修正调用,仍失败则记录具体限制并继续独立分析。recorded表示请求已保存,浏览器跟随、连接和暂停由前端处理,收尾依据分析结果与发布回执。
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -42,6 +42,8 @@ const reportFields = {
|
|||||||
conclusion_boundary: text,
|
conclusion_boundary: text,
|
||||||
evidence_refs: tool.schema.array(text).default([]),
|
evidence_refs: tool.schema.array(text).default([]),
|
||||||
ui_blocks: taskReportUiBlocksSchema,
|
ui_blocks: taskReportUiBlocksSchema,
|
||||||
|
scada_device_ids: tool.schema.array(identifier).max(1000).optional()
|
||||||
|
.describe("SCADA devices listed in monitoring tables or used as display targets, including reference-only devices. Server checks their current and seven-day chart coverage."),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const taskPublicationReportSchema = tool.schema.discriminatedUnion("outcome", [
|
export const taskPublicationReportSchema = tool.schema.discriminatedUnion("outcome", [
|
||||||
|
|||||||
@@ -4032,6 +4032,23 @@
|
|||||||
"title": "Recommendations",
|
"title": "Recommendations",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"scada_device_ids": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"maxLength": 128,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"maxItems": 1000,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Scada Device Ids"
|
||||||
|
},
|
||||||
"stage": {
|
"stage": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -4144,6 +4161,23 @@
|
|||||||
"title": "Recommendations",
|
"title": "Recommendations",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"scada_device_ids": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"maxLength": 128,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"maxItems": 1000,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Scada Device Ids"
|
||||||
|
},
|
||||||
"stage": {
|
"stage": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"interim",
|
"interim",
|
||||||
@@ -7383,6 +7417,110 @@
|
|||||||
"title": "RunSimulationManuallyByDateRest",
|
"title": "RunSimulationManuallyByDateRest",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ScadaChartContext": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"gaps": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScadaChartGap"
|
||||||
|
},
|
||||||
|
"maxItems": 8,
|
||||||
|
"title": "Gaps",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
"enum": [
|
||||||
|
"pressure",
|
||||||
|
"flow"
|
||||||
|
],
|
||||||
|
"title": "Metric",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"enum": [
|
||||||
|
"current",
|
||||||
|
"comparison"
|
||||||
|
],
|
||||||
|
"title": "Role",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"maxLength": 128,
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Unit",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value_field": {
|
||||||
|
"default": "monitored_value",
|
||||||
|
"enum": [
|
||||||
|
"monitored_value",
|
||||||
|
"cleaned_value"
|
||||||
|
],
|
||||||
|
"title": "Value Field",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"window_end": {
|
||||||
|
"format": "date-time",
|
||||||
|
"title": "Window End",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"window_start": {
|
||||||
|
"format": "date-time",
|
||||||
|
"title": "Window Start",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"role",
|
||||||
|
"metric",
|
||||||
|
"unit",
|
||||||
|
"window_start",
|
||||||
|
"window_end"
|
||||||
|
],
|
||||||
|
"title": "ScadaChartContext",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ScadaChartGap": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"day_offset": {
|
||||||
|
"maximum": 7.0,
|
||||||
|
"minimum": 0.0,
|
||||||
|
"title": "Day Offset",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"maxLength": 128,
|
||||||
|
"minLength": 1,
|
||||||
|
"title": "Device Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"missing_count": {
|
||||||
|
"maximum": 100.0,
|
||||||
|
"minimum": 1.0,
|
||||||
|
"title": "Missing Count",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"enum": [
|
||||||
|
"no_readings",
|
||||||
|
"outside_frozen_scope",
|
||||||
|
"outside_frozen_window",
|
||||||
|
"no_scheduled_samples"
|
||||||
|
],
|
||||||
|
"title": "Reason",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"device_id",
|
||||||
|
"day_offset",
|
||||||
|
"missing_count",
|
||||||
|
"reason"
|
||||||
|
],
|
||||||
|
"title": "ScadaChartGap",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ScadaDeviceResponse": {
|
"ScadaDeviceResponse": {
|
||||||
"description": "Project SCADA metadata keyed by the canonical device identifier.",
|
"description": "Project SCADA metadata keyed by the canonical device identifier.",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -10450,6 +10588,16 @@
|
|||||||
"title": "Chart Type",
|
"title": "Chart Type",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"scada": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/ScadaChartContext"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"series": {
|
"series": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/TaskSafeChartSeries"
|
"$ref": "#/components/schemas/TaskSafeChartSeries"
|
||||||
@@ -10527,6 +10675,32 @@
|
|||||||
"title": "Data",
|
"title": "Data",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"day_offset": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maximum": 7.0,
|
||||||
|
"minimum": 0.0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Day Offset"
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"maxLength": 128,
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Device Id"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"maxLength": 128,
|
"maxLength": 128,
|
||||||
"title": "Name",
|
"title": "Name",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"contracts": {
|
"contracts": {
|
||||||
"server": {
|
"server": {
|
||||||
"file": "server-v1.openapi.json",
|
"file": "server-v1.openapi.json",
|
||||||
"sha256": "7ed3f6485909c3ef7fac8d2083d6931765e4d197e690c3b9914b905a8aa743b2"
|
"sha256": "d96df154b86b797d0db913d1e69cf335d92d34dd119f800c9346b90c6119c9bb"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export function createChartSampleTimeSchema(zod: typeof z = z) {
|
|||||||
|
|
||||||
type PublicationChart = {
|
type PublicationChart = {
|
||||||
chart_type: string;
|
chart_type: string;
|
||||||
|
scada?: { role: "current" | "comparison"; window_start: string };
|
||||||
x_data: string[];
|
x_data: string[];
|
||||||
series: Array<{ type?: string; data: Array<number | null>; sample_times?: Array<string | null> }>;
|
series: Array<{ type?: string; data: Array<number | null>; sample_times?: Array<string | null> }>;
|
||||||
};
|
};
|
||||||
@@ -57,8 +58,12 @@ export function chartPublicationError(chart: PublicationChart): string | null {
|
|||||||
if ((series.type ?? "line") === "line") for (const base of bases) current.add(base);
|
if ((series.type ?? "line") === "line") for (const base of bases) current.add(base);
|
||||||
else historical.push(...bases);
|
else historical.push(...bases);
|
||||||
}
|
}
|
||||||
if (current.size !== 1) return "当前 line 必须包含真实采样时间,且所有当前曲线使用同一窗口";
|
const emptyScada = current.size === 0 && chart.scada?.role === "comparison";
|
||||||
const start = [...current][0];
|
if (current.size !== 1 && !emptyScada) return "当前 line 必须包含真实采样时间,且所有当前曲线使用同一窗口";
|
||||||
|
const windowStart = emptyScada ? Date.parse(chart.scada!.window_start) + 8 * 3600_000 : 0;
|
||||||
|
const midnight = Math.floor(windowStart / 86400_000) * 86400_000;
|
||||||
|
const firstSlot = midnight + seconds[0] * 1000;
|
||||||
|
const start = emptyScada ? firstSlot + (firstSlot < windowStart ? 86400_000 : 0) : [...current][0];
|
||||||
return historical.some((base) => start <= base || (start - base) % 86400_000 !== 0)
|
return historical.some((base) => start <= base || (start - base) % 86400_000 !== 0)
|
||||||
? "历史 scatter 必须来自当前窗口之前的对应日期" : null;
|
? "历史 scatter 必须来自当前窗口之前的对应日期" : null;
|
||||||
}
|
}
|
||||||
@@ -68,9 +73,25 @@ export function createViewSchemas(zod: typeof z = z, publication = false, valida
|
|||||||
const z = zod;
|
const z = zod;
|
||||||
|
|
||||||
const presentationIdentifierSchema = z.string().trim().min(1).max(128);
|
const presentationIdentifierSchema = z.string().trim().min(1).max(128);
|
||||||
|
const scadaChartSchema = z.object({
|
||||||
|
role: z.enum(["current", "comparison"]),
|
||||||
|
metric: z.enum(["pressure", "flow"]),
|
||||||
|
unit: presentationIdentifierSchema,
|
||||||
|
window_start: createChartSampleTimeSchema(z),
|
||||||
|
window_end: createChartSampleTimeSchema(z),
|
||||||
|
value_field: z.enum(["monitored_value", "cleaned_value"]).optional(),
|
||||||
|
// Populated by Server after checking observations, never a display exemption.
|
||||||
|
gaps: z.array(z.object({
|
||||||
|
device_id: presentationIdentifierSchema,
|
||||||
|
day_offset: z.number().int().min(0).max(7),
|
||||||
|
missing_count: z.number().int().min(1).max(100),
|
||||||
|
reason: z.enum(["no_readings", "outside_frozen_scope", "outside_frozen_window", "no_scheduled_samples"])
|
||||||
|
}).strict()).max(8).optional()
|
||||||
|
}).strict();
|
||||||
const chartSchema = z
|
const chartSchema = z
|
||||||
.object({
|
.object({
|
||||||
title: z.string().max(200),
|
title: z.string().max(200),
|
||||||
|
scada: scadaChartSchema.optional(),
|
||||||
chart_type: z.enum(["line", "bar", "time_comparison"]),
|
chart_type: z.enum(["line", "bar", "time_comparison"]),
|
||||||
x_axis_name: z.string().max(128).optional(),
|
x_axis_name: z.string().max(128).optional(),
|
||||||
y_axis_name: z.string().max(128).optional(),
|
y_axis_name: z.string().max(128).optional(),
|
||||||
@@ -80,6 +101,8 @@ export function createViewSchemas(zod: typeof z = z, publication = false, valida
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().max(128),
|
name: z.string().max(128),
|
||||||
|
device_id: presentationIdentifierSchema.optional(),
|
||||||
|
day_offset: z.number().int().min(0).max(7).optional(),
|
||||||
type: z.enum(["line", "bar", "scatter"]).optional(),
|
type: z.enum(["line", "bar", "scatter"]).optional(),
|
||||||
data: z.array(z.number().finite().nullable()).min(1).max(100),
|
data: z.array(z.number().finite().nullable()).min(1).max(100),
|
||||||
sample_times: z.array(createChartSampleTimeSchema(z).nullable()).min(1).max(100).optional()
|
sample_times: z.array(createChartSampleTimeSchema(z).nullable()).min(1).max(100).optional()
|
||||||
|
|||||||
@@ -207,6 +207,97 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 200
|
"maxLength": 200
|
||||||
},
|
},
|
||||||
|
"scada": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"current",
|
||||||
|
"comparison"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pressure",
|
||||||
|
"flow"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"window_start": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:[1-9][0-9]{3}|0[1-9][0-9]{2}|00[1-9][0-9]|000[1-9])-[0-9]{2}-[0-9]{2}T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$"
|
||||||
|
},
|
||||||
|
"window_end": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64,
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:[1-9][0-9]{3}|0[1-9][0-9]{2}|00[1-9][0-9]|000[1-9])-[0-9]{2}-[0-9]{2}T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$"
|
||||||
|
},
|
||||||
|
"value_field": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"monitored_value",
|
||||||
|
"cleaned_value"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gaps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"device_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"day_offset": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 7
|
||||||
|
},
|
||||||
|
"missing_count": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"no_readings",
|
||||||
|
"outside_frozen_scope",
|
||||||
|
"outside_frozen_window",
|
||||||
|
"no_scheduled_samples"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"device_id",
|
||||||
|
"day_offset",
|
||||||
|
"missing_count",
|
||||||
|
"reason"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"maxItems": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"role",
|
||||||
|
"metric",
|
||||||
|
"unit",
|
||||||
|
"window_start",
|
||||||
|
"window_end"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"chart_type": {
|
"chart_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -241,6 +332,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 128
|
"maxLength": 128
|
||||||
},
|
},
|
||||||
|
"device_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"day_offset": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 7
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
Vendored
+56
@@ -7219,6 +7219,8 @@ export interface components {
|
|||||||
pending_items?: string[];
|
pending_items?: string[];
|
||||||
/** Recommendations */
|
/** Recommendations */
|
||||||
recommendations?: string[];
|
recommendations?: string[];
|
||||||
|
/** Scada Device Ids */
|
||||||
|
scada_device_ids?: string[] | null;
|
||||||
/** Stage */
|
/** Stage */
|
||||||
stage?: ("interim" | "final") | null;
|
stage?: ("interim" | "final") | null;
|
||||||
/** Stop Reason */
|
/** Stop Reason */
|
||||||
@@ -7251,6 +7253,8 @@ export interface components {
|
|||||||
pending_items?: string[];
|
pending_items?: string[];
|
||||||
/** Recommendations */
|
/** Recommendations */
|
||||||
recommendations?: string[];
|
recommendations?: string[];
|
||||||
|
/** Scada Device Ids */
|
||||||
|
scada_device_ids?: string[] | null;
|
||||||
/**
|
/**
|
||||||
* Stage
|
* Stage
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
@@ -8505,6 +8509,53 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
start_time: string;
|
start_time: string;
|
||||||
};
|
};
|
||||||
|
/** ScadaChartContext */
|
||||||
|
ScadaChartContext: {
|
||||||
|
/** Gaps */
|
||||||
|
gaps?: components["schemas"]["ScadaChartGap"][];
|
||||||
|
/**
|
||||||
|
* Metric
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
metric: "pressure" | "flow";
|
||||||
|
/**
|
||||||
|
* Role
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
role: "current" | "comparison";
|
||||||
|
/** Unit */
|
||||||
|
unit: string;
|
||||||
|
/**
|
||||||
|
* Value Field
|
||||||
|
* @default monitored_value
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
value_field: "monitored_value" | "cleaned_value";
|
||||||
|
/**
|
||||||
|
* Window End
|
||||||
|
* Format: date-time
|
||||||
|
*/
|
||||||
|
window_end: string;
|
||||||
|
/**
|
||||||
|
* Window Start
|
||||||
|
* Format: date-time
|
||||||
|
*/
|
||||||
|
window_start: string;
|
||||||
|
};
|
||||||
|
/** ScadaChartGap */
|
||||||
|
ScadaChartGap: {
|
||||||
|
/** Day Offset */
|
||||||
|
day_offset: number;
|
||||||
|
/** Device Id */
|
||||||
|
device_id: string;
|
||||||
|
/** Missing Count */
|
||||||
|
missing_count: number;
|
||||||
|
/**
|
||||||
|
* Reason
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
reason: "no_readings" | "outside_frozen_scope" | "outside_frozen_window" | "no_scheduled_samples";
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* ScadaDeviceResponse
|
* ScadaDeviceResponse
|
||||||
* @description Project SCADA metadata keyed by the canonical device identifier.
|
* @description Project SCADA metadata keyed by the canonical device identifier.
|
||||||
@@ -9656,6 +9707,7 @@ export interface components {
|
|||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
chart_type: "line" | "bar" | "time_comparison";
|
chart_type: "line" | "bar" | "time_comparison";
|
||||||
|
scada?: components["schemas"]["ScadaChartContext"] | null;
|
||||||
/** Series */
|
/** Series */
|
||||||
series: components["schemas"]["TaskSafeChartSeries"][];
|
series: components["schemas"]["TaskSafeChartSeries"][];
|
||||||
/** Title */
|
/** Title */
|
||||||
@@ -9671,6 +9723,10 @@ export interface components {
|
|||||||
TaskSafeChartSeries: {
|
TaskSafeChartSeries: {
|
||||||
/** Data */
|
/** Data */
|
||||||
data: (number | null)[];
|
data: (number | null)[];
|
||||||
|
/** Day Offset */
|
||||||
|
day_offset?: number | null;
|
||||||
|
/** Device Id */
|
||||||
|
device_id?: string | null;
|
||||||
/** Name */
|
/** Name */
|
||||||
name: string;
|
name: string;
|
||||||
/** Sample Times */
|
/** Sample Times */
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export const taskReportSchema = z.object({
|
|||||||
data_gaps: z.array(dataGapSchema),
|
data_gaps: z.array(dataGapSchema),
|
||||||
diagram: investigationDiagramSchema.nullish(),
|
diagram: investigationDiagramSchema.nullish(),
|
||||||
ui_blocks: z.array(reportUiBlockSchema).max(8).optional(),
|
ui_blocks: z.array(reportUiBlockSchema).max(8).optional(),
|
||||||
|
scada_device_ids: z.array(text.max(128)).max(1000).optional(),
|
||||||
}).strict().superRefine((report, ctx) => {
|
}).strict().superRefine((report, ctx) => {
|
||||||
const invalid = (message: string, path: (string | number)[] = []) =>
|
const invalid = (message: string, path: (string | number)[] = []) =>
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message, path });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message, path });
|
||||||
|
|||||||
@@ -404,6 +404,12 @@ export class OperationalTaskClientError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Feed a concrete Server coverage rejection back into the same active investigation. */
|
||||||
|
export function scadaPresentationContinuation(error: unknown): string | null {
|
||||||
|
if (!(error instanceof OperationalTaskClientError) || error.code !== "SCADA_PRESENTATION_INCOMPLETE") return null;
|
||||||
|
return `Server尚未接受本轮调查结束:${error.message}\n请按返回设备补充publish_task_presentation监测图,必要时更新publish_task_report。保留目标设备与真实缺测说明,完成后再结束本轮。`;
|
||||||
|
}
|
||||||
|
|
||||||
const parseJson = (text: string): unknown => {
|
const parseJson = (text: string): unknown => {
|
||||||
if (!text.trim()) return {};
|
if (!text.trim()) return {};
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
clearOperationalTaskLease,
|
clearOperationalTaskLease,
|
||||||
operationalTaskClient,
|
operationalTaskClient,
|
||||||
releaseOperationalTaskCredential,
|
releaseOperationalTaskCredential,
|
||||||
|
scadaPresentationContinuation,
|
||||||
} from "../product/operational-task-client.js";
|
} from "../product/operational-task-client.js";
|
||||||
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
||||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||||
@@ -798,7 +799,7 @@ export const registerChatStreamRoute = (
|
|||||||
if (messaging && (messaging.hasInputThisTurn(binding.sessionId) || !await messaging.closeInteractive(activeSessionRecord))) return false;
|
if (messaging && (messaging.hasInputThisTurn(binding.sessionId) || !await messaging.closeInteractive(activeSessionRecord))) return false;
|
||||||
return inputTracker.canClose();
|
return inputTracker.canClose();
|
||||||
},
|
},
|
||||||
beforeIdle: messaging || (sessionPurpose === "task_execution" && waitTaskCalculations) ? async () => {
|
beforeIdle: messaging || sessionPurpose === "task_execution" ? async () => {
|
||||||
if (sessionPurpose !== "task_execution") return messaging ? messaging.closeInteractive(activeSessionRecord) : true;
|
if (sessionPurpose !== "task_execution") return messaging ? messaging.closeInteractive(activeSessionRecord) : true;
|
||||||
if (await messaging?.flush(binding.sessionId)) return false;
|
if (await messaging?.flush(binding.sessionId)) return false;
|
||||||
const calculationResume = await waitTaskCalculations?.(binding.sessionId, abortController.signal);
|
const calculationResume = await waitTaskCalculations?.(binding.sessionId, abortController.signal);
|
||||||
@@ -818,6 +819,8 @@ export const registerChatStreamRoute = (
|
|||||||
if (task.lifecycle_status === "failed") throw new Error("Server rejected incomplete investigation artifacts");
|
if (task.lifecycle_status === "failed") throw new Error("Server rejected incomplete investigation artifacts");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const continuation = scadaPresentationContinuation(error);
|
||||||
|
if (continuation) return continuation;
|
||||||
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
||||||
await messaging?.flush(binding.sessionId);
|
await messaging?.flush(binding.sessionId);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { expect, it } from "bun:test";
|
||||||
|
import { taskPresentationCommandSchema, type TaskPresentationCommand } from "../../src/product/task-presentation.js";
|
||||||
|
import { taskPublicationCommandSchema, taskPublicationReportSchema } from "../../.opencode/tools/task-publication-schema.js";
|
||||||
|
import { taskReportSchema } from "../../src/product/investigation-report.js";
|
||||||
|
import { OperationalTaskClientError, scadaPresentationContinuation } from "../../src/product/operational-task-client.js";
|
||||||
|
|
||||||
|
it("preserves SCADA device identities through native and host publication boundaries", () => {
|
||||||
|
const command: TaskPresentationCommand = { action: "open_chart", chart_id: "flow-overview", evidence_refs: ["observations"], chart: {
|
||||||
|
title: "流量当前趋势", chart_type: "line", x_data: ["05:00", "05:15"],
|
||||||
|
scada: { role: "current", metric: "flow", unit: "m3/h", window_start: "2026-07-16T05:00:00+08:00", window_end: "2026-07-16T08:00:00+08:00" },
|
||||||
|
series: [{ name: "524529", device_id: "524529", day_offset: 0, type: "line", data: [-6.8, 303.3],
|
||||||
|
sample_times: ["2026-07-16T05:00:00+08:00", "2026-07-16T05:15:00+08:00"] }],
|
||||||
|
} };
|
||||||
|
expect(taskPresentationCommandSchema.parse(command)).toEqual(command);
|
||||||
|
expect(taskPublicationCommandSchema.parse(command)).toEqual(command);
|
||||||
|
const report = { title: "调查", current_conclusion: "524529仅作参考", conclusion_boundary: "待复核",
|
||||||
|
stage: "final", outcome: "concluded", evidence_refs: ["observations"], data_gaps: [], scada_device_ids: ["524529"] };
|
||||||
|
expect(taskReportSchema.parse(report).scada_device_ids).toEqual(["524529"]);
|
||||||
|
expect(taskPublicationReportSchema.parse(report).scada_device_ids).toEqual(["524529"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues an incomplete SCADA presentation with the exact missing device instead of retrying other failures", () => {
|
||||||
|
const rejection = new OperationalTaskClientError(409, "缺少七天同期对比设备:524529", "SCADA_PRESENTATION_INCOMPLETE");
|
||||||
|
expect(scadaPresentationContinuation(rejection)).toContain("524529");
|
||||||
|
expect(scadaPresentationContinuation(rejection)).toContain("publish_task_presentation");
|
||||||
|
expect(scadaPresentationContinuation(new OperationalTaskClientError(403, "拒绝", "PERMISSION_DENIED"))).toBeNull();
|
||||||
|
expect(scadaPresentationContinuation(new Error("transport failed"))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("keeps all-null current chunks aligned to their own first slot across midnight", () => {
|
||||||
|
for (const [start, end, labels, times] of [
|
||||||
|
["2026-07-16T05:08:00+08:00", "2026-07-16T08:08:00+08:00", ["07:45", "08:00"],
|
||||||
|
["2026-07-15T07:45:00+08:00", "2026-07-15T08:00:00+08:00"]],
|
||||||
|
["2026-07-15T22:08:00+08:00", "2026-07-16T01:08:00+08:00", ["00:45", "01:00"],
|
||||||
|
["2026-07-15T00:45:00+08:00", "2026-07-15T01:00:00+08:00"]],
|
||||||
|
] as const) {
|
||||||
|
const command = { action: "open_chart", chart_id: "chunk", evidence_refs: ["observations"], chart: {
|
||||||
|
title: "流量同期分段", chart_type: "time_comparison", x_data: labels,
|
||||||
|
scada: { role: "comparison", metric: "flow", unit: "m3/h", window_start: start, window_end: end },
|
||||||
|
series: [{ name: "current", type: "line", data: [null, null], sample_times: [null, null] },
|
||||||
|
{ name: "history", type: "scatter", data: [1, 2], sample_times: times }],
|
||||||
|
} };
|
||||||
|
expect(taskPresentationCommandSchema.safeParse(command).success).toBe(true);
|
||||||
|
expect(taskPublicationCommandSchema.safeParse(command).success).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user