feat: add SCADA frontend action bridge

This commit is contained in:
2026-07-20 19:57:25 +08:00
parent 72019ac1f2
commit 432edaaf2f
16 changed files with 445 additions and 20 deletions
+5 -1
View File
@@ -48,7 +48,11 @@ export class FrontendActionCoordinator {
this.sinks.set(sessionId, sink);
const now = Date.now();
for (const pending of this.pending.values()) {
if (pending.request.sessionId === sessionId && pending.request.expiresAt > now) sink(pending.request);
if (
pending.request.sessionId === sessionId &&
pending.request.replayPolicy === "explicit" &&
pending.request.expiresAt > now
) sink(pending.request);
}
return () => {
if (this.sinks.get(sessionId) === sink) this.sinks.delete(sessionId);
+52 -1
View File
@@ -2,7 +2,58 @@ import { z } from "zod";
import type { FrontendActionManifest } from "./types.js";
type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny };
const definitions: Definition[] = [];
const sensorId = z.string().trim().min(1).max(128);
const scadaLevel = z.enum(["high", "medium", "low", "unrated"]);
const levelCounts = z.object({
high: z.number().int().nonnegative(),
medium: z.number().int().nonnegative(),
low: z.number().int().nonnegative(),
unrated: z.number().int().nonnegative(),
}).strict();
const renderScadaAnalysisInput = z.object({
items: z.array(z.object({ sensor_id: sensorId, level: scadaLevel }).strict()).min(1).max(100),
}).strict().superRefine((value, context) => {
const seen = new Set<string>();
value.items.forEach((item, index) => {
if (seen.has(item.sensor_id)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "sensor_id values must be unique",
path: ["items", index, "sensor_id"],
});
}
seen.add(item.sensor_id);
});
});
const definitions: Definition[] = [
{
manifest: {
id: "render_scada_analysis",
version: "frontend-action@1",
risk: "view",
timeoutMs: 15_000,
supportedSurfaces: ["map_overlay"],
},
inputSchema: renderScadaAnalysisInput,
outputSchema: z.object({
rendered_ids: z.array(sensorId),
missing_ids: z.array(sensorId),
level_counts: levelCounts,
fitted: z.literal(true),
}).strict(),
},
{
manifest: {
id: "clear_scada_analysis",
version: "frontend-action@1",
risk: "view",
timeoutMs: 15_000,
supportedSurfaces: ["map_overlay"],
},
inputSchema: z.object({}).strict(),
outputSchema: z.object({ cleared: z.literal(true) }).strict(),
},
];
export const frontendActionRegistry = definitions.map((item) => item.manifest);
export const getFrontendActionDefinition = (name: string) => definitions.find((item) => item.manifest.id === name);