fix: resume incomplete SCADA presentations
This commit is contained in:
@@ -10,6 +10,7 @@ export function createChartSampleTimeSchema(zod: typeof z = z) {
|
||||
|
||||
type PublicationChart = {
|
||||
chart_type: string;
|
||||
scada?: { role: "current" | "comparison"; window_start: string };
|
||||
x_data: string[];
|
||||
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);
|
||||
else historical.push(...bases);
|
||||
}
|
||||
if (current.size !== 1) return "当前 line 必须包含真实采样时间,且所有当前曲线使用同一窗口";
|
||||
const start = [...current][0];
|
||||
const emptyScada = current.size === 0 && chart.scada?.role === "comparison";
|
||||
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)
|
||||
? "历史 scatter 必须来自当前窗口之前的对应日期" : null;
|
||||
}
|
||||
@@ -68,9 +73,25 @@ export function createViewSchemas(zod: typeof z = z, publication = false, valida
|
||||
const z = zod;
|
||||
|
||||
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
|
||||
.object({
|
||||
title: z.string().max(200),
|
||||
scada: scadaChartSchema.optional(),
|
||||
chart_type: z.enum(["line", "bar", "time_comparison"]),
|
||||
x_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
|
||||
.object({
|
||||
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(),
|
||||
data: z.array(z.number().finite().nullable()).min(1).max(100),
|
||||
sample_times: z.array(createChartSampleTimeSchema(z).nullable()).min(1).max(100).optional()
|
||||
|
||||
@@ -207,6 +207,97 @@
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -241,6 +332,16 @@
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"device_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128
|
||||
},
|
||||
"day_offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 7
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Vendored
+56
@@ -7219,6 +7219,8 @@ export interface components {
|
||||
pending_items?: string[];
|
||||
/** Recommendations */
|
||||
recommendations?: string[];
|
||||
/** Scada Device Ids */
|
||||
scada_device_ids?: string[] | null;
|
||||
/** Stage */
|
||||
stage?: ("interim" | "final") | null;
|
||||
/** Stop Reason */
|
||||
@@ -7251,6 +7253,8 @@ export interface components {
|
||||
pending_items?: string[];
|
||||
/** Recommendations */
|
||||
recommendations?: string[];
|
||||
/** Scada Device Ids */
|
||||
scada_device_ids?: string[] | null;
|
||||
/**
|
||||
* Stage
|
||||
* @enum {string}
|
||||
@@ -8505,6 +8509,53 @@ export interface components {
|
||||
*/
|
||||
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
|
||||
* @description Project SCADA metadata keyed by the canonical device identifier.
|
||||
@@ -9656,6 +9707,7 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
chart_type: "line" | "bar" | "time_comparison";
|
||||
scada?: components["schemas"]["ScadaChartContext"] | null;
|
||||
/** Series */
|
||||
series: components["schemas"]["TaskSafeChartSeries"][];
|
||||
/** Title */
|
||||
@@ -9671,6 +9723,10 @@ export interface components {
|
||||
TaskSafeChartSeries: {
|
||||
/** Data */
|
||||
data: (number | null)[];
|
||||
/** Day Offset */
|
||||
day_offset?: number | null;
|
||||
/** Device Id */
|
||||
device_id?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Sample Times */
|
||||
|
||||
@@ -107,6 +107,7 @@ export const taskReportSchema = z.object({
|
||||
data_gaps: z.array(dataGapSchema),
|
||||
diagram: investigationDiagramSchema.nullish(),
|
||||
ui_blocks: z.array(reportUiBlockSchema).max(8).optional(),
|
||||
scada_device_ids: z.array(text.max(128)).max(1000).optional(),
|
||||
}).strict().superRefine((report, ctx) => {
|
||||
const invalid = (message: string, path: (string | number)[] = []) =>
|
||||
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 => {
|
||||
if (!text.trim()) return {};
|
||||
try {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
clearOperationalTaskLease,
|
||||
operationalTaskClient,
|
||||
releaseOperationalTaskCredential,
|
||||
scadaPresentationContinuation,
|
||||
} from "../product/operational-task-client.js";
|
||||
import { type SessionTranscriptStore } from "../sessions/transcriptStore.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;
|
||||
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 (await messaging?.flush(binding.sessionId)) return false;
|
||||
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");
|
||||
return true;
|
||||
} catch (error) {
|
||||
const continuation = scadaPresentationContinuation(error);
|
||||
if (continuation) return continuation;
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
||||
await messaging?.flush(binding.sessionId);
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user