feat(agent): add frontend action bridge

This commit is contained in:
2026-07-13 17:26:03 +08:00
parent 4418e99a5c
commit 6b45e7c40c
16 changed files with 447 additions and 14 deletions
+36
View File
@@ -0,0 +1,36 @@
import { z } from "zod";
import type { FrontendActionManifest } from "./types.js";
type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny };
const zoomInput = z.object({ x: z.number().finite(), y: z.number().finite(), source_crs: z.enum(["EPSG:3857", "EPSG:4326"]).optional(), zoom: z.number().finite().optional(), duration_ms: z.number().finite().nonnegative().optional() }).strict();
const id = z.string().trim().min(1).max(256);
const isoTime = z.string().datetime({ offset: true });
const timeRange = {
start_time: isoTime.optional(),
end_time: isoTime.optional(),
};
const locateInput = z.object({
ids: z.array(id).min(1).max(100),
feature_type: z.enum(["junction", "pipe", "valve", "reservoir", "pump", "tank", "conduit", "orifice", "outfall"]),
}).strict();
const historyInput = z.object({
feature_infos: z.array(z.tuple([id, id])).min(1).max(100),
data_type: z.enum(["realtime", "scheme", "none"]),
...timeRange,
}).strict().refine((value) => !value.start_time || !value.end_time || Date.parse(value.start_time) <= Date.parse(value.end_time), "start_time must not be after end_time");
const scadaInput = z.object({
device_id: id.optional(),
device_ids: z.array(id).min(1).max(100).optional(),
...timeRange,
}).strict().refine((value) => Boolean(value.device_id) !== Boolean(value.device_ids), "provide exactly one of device_id or device_ids")
.refine((value) => !value.start_time || !value.end_time || Date.parse(value.start_time) <= Date.parse(value.end_time), "start_time must not be after end_time");
const definitions: Definition[] = [
{ manifest: { id: "zoom_to_map", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["map_overlay"] }, inputSchema: zoomInput, outputSchema: z.object({}).passthrough() },
{ manifest: { id: "locate_features", version: "frontend-action@1", risk: "view", timeoutMs: 15_000, supportedSurfaces: ["map_overlay"] }, inputSchema: locateInput, outputSchema: z.object({ locatedIds: z.array(id), missingIds: z.array(id), featureType: id, bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]) }).strict() },
{ manifest: { id: "view_history", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["side_panel", "canvas"] }, inputSchema: historyInput, outputSchema: z.object({ component: z.literal("HistoryPanel"), rendered: z.literal(true), itemCount: z.number().int().nonnegative() }).strict() },
{ manifest: { id: "view_scada", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["side_panel", "canvas"] }, inputSchema: scadaInput, outputSchema: z.object({ component: z.literal("ScadaPanel"), rendered: z.literal(true), itemCount: z.number().int().nonnegative() }).strict() },
];
export const frontendActionRegistry = definitions.map((item) => item.manifest);
export const getFrontendActionDefinition = (name: string) => definitions.find((item) => item.manifest.id === name);
export const frontendActionRegistryResponse = { schema_version: "frontend-action-registry@1" as const, actions: frontendActionRegistry };