feat: initialize drainage network frontend

This commit is contained in:
2026-07-10 16:16:17 +08:00
commit 66de96d9e4
133 changed files with 33057 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
export type { UIEnvelope, UIEnvelopePayload, UIRegistry, UISurface } from "./types";
export { toTrustedMapAction, type TrustedMapAction } from "./map-actions";
export {
isUiEnvelopeAllowed,
parseUiEnvelope,
parseUiEnvelopePayload,
parseUiRegistry
} from "./validator";
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import { toTrustedMapAction } from "./map-actions";
describe("toTrustedMapAction", () => {
it("rejects zoom_to_map actions without a valid center", () => {
expect(toTrustedMapAction("zoom_to_map", { zoom: 14 })).toBeNull();
expect(toTrustedMapAction("zoom_to_map", { center: ["east", 39.1] })).toBeNull();
});
it("rejects unsupported map actions", () => {
expect(toTrustedMapAction("run_javascript", { code: "alert(1)" })).toBeNull();
});
it("downgrades malformed apply_layer_style params to safe undefined fields", () => {
expect(
toTrustedMapAction("apply_layer_style", {
layer_group_id: 123,
layer_id: [],
visible: "true"
})
).toEqual({
type: "apply_layer_style",
layerGroupId: undefined,
layerId: undefined,
visible: undefined,
fallbackText: undefined
});
});
it("accepts valid zoom_to_map coordinates", () => {
expect(toTrustedMapAction("zoom_to_map", { lng: 117.2, lat: 39.1, zoom: 13 })).toEqual({
type: "zoom_to_map",
center: [117.2, 39.1],
zoom: 13,
fallbackText: undefined
});
});
it("accepts legacy zoom coordinate aliases and numeric strings", () => {
expect(toTrustedMapAction("zoom_to_map", { coordinate: ["117.2", "39.1"], zoom: "13" })).toEqual({
type: "zoom_to_map",
center: [117.2, 39.1],
zoom: 13,
fallbackText: undefined
});
expect(toTrustedMapAction("zoom_to_map", { x: "117.2", y: "39.1" })).toMatchObject({
type: "zoom_to_map",
center: [117.2, 39.1]
});
});
it("normalizes legacy locate tool actions", () => {
expect(toTrustedMapAction("locate_pipes", { pipe_ids: "P1,P2" })).toEqual({
type: "locate_features",
featureIds: ["P1", "P2"],
layer: "geo_pipes_mat",
fallbackText: undefined
});
});
it("normalizes numeric locate ids", () => {
expect(toTrustedMapAction("locate_junctions", { junction_id: 42 })).toMatchObject({
type: "locate_features",
featureIds: ["42"],
layer: "geo_junctions_mat"
});
});
});
+185
View File
@@ -0,0 +1,185 @@
export type TrustedMapAction =
| {
type: "locate_features";
featureIds: string[];
layer?: string;
fallbackText?: string;
}
| {
type: "zoom_to_map";
center: [number, number];
zoom?: number;
fallbackText?: string;
}
| {
type: "apply_layer_style";
layerGroupId?: string;
layerId?: string;
visible?: boolean;
fallbackText?: string;
}
| {
type: "render_junctions";
renderRef: string;
fallbackText?: string;
};
export function toTrustedMapAction(action: string, params: unknown, fallbackText?: string): TrustedMapAction | null {
if (!isRecord(params)) {
return null;
}
if (action === "locate_features" || action in LEGACY_LOCATE_ACTION_LAYERS) {
const featureIds = readLocateIds(params);
return {
type: "locate_features",
featureIds,
layer:
stringValue(params.layer ?? params.target_layer ?? params.targetLayer) ??
LEGACY_LOCATE_ACTION_LAYERS[action],
fallbackText
};
}
if (action === "zoom_to_map") {
const center = parseCenter(params);
if (!center) {
return null;
}
return {
type: "zoom_to_map",
center,
zoom: numberValue(params.zoom),
fallbackText
};
}
if (action === "apply_layer_style") {
return {
type: "apply_layer_style",
layerGroupId: stringValue(params.layer_group_id ?? params.layerGroupId),
layerId: stringValue(params.layer_id ?? params.layerId),
visible: booleanValue(params.visible),
fallbackText
};
}
if (action === "render_junctions") {
const renderRef = stringValue(params.render_ref ?? params.renderRef);
if (!renderRef) {
return null;
}
return {
type: "render_junctions",
renderRef,
fallbackText
};
}
return null;
}
function parseCenter(params: Record<string, unknown>): [number, number] | null {
if (Array.isArray(params.center) && params.center.length >= 2) {
const lng = numberValue(params.center[0]);
const lat = numberValue(params.center[1]);
return lng === undefined || lat === undefined ? null : [lng, lat];
}
const rawCoordinate = params.coordinate ?? params.coordinates;
if (Array.isArray(rawCoordinate) && rawCoordinate.length >= 2) {
const lng = numberValue(rawCoordinate[0]);
const lat = numberValue(rawCoordinate[1]);
return lng === undefined || lat === undefined ? null : [lng, lat];
}
const lng = numberValue(params.lng ?? params.lon ?? params.longitude ?? params.x);
const lat = numberValue(params.lat ?? params.latitude ?? params.y);
return lng === undefined || lat === undefined ? null : [lng, lat];
}
function stringValue(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function numberValue(value: unknown) {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}
function booleanValue(value: unknown) {
return typeof value === "boolean" ? value : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
const LEGACY_LOCATE_ACTION_LAYERS: Record<string, string> = {
locate_junctions: "geo_junctions_mat",
locate_pipes: "geo_pipes_mat",
locate_valves: "geo_valves",
locate_reservoirs: "geo_reservoirs",
locate_pumps: "geo_pumps",
locate_tanks: "geo_tanks"
};
const LOCATE_ID_PARAM_KEYS = [
"feature_ids",
"feature_id",
"featureIds",
"featureId",
"ids",
"id",
"node_ids",
"node_id",
"junction_ids",
"junction_id",
"pipe_ids",
"pipe_id",
"valve_ids",
"valve_id",
"reservoir_ids",
"reservoir_id",
"pump_ids",
"pump_id",
"tank_ids",
"tank_id"
] as const;
function readLocateIds(params: Record<string, unknown>) {
for (const key of LOCATE_ID_PARAM_KEYS) {
const value = params[key];
const ids = normalizeIds(value);
if (ids.length > 0) {
return ids;
}
}
return [];
}
function normalizeIds(value: unknown) {
if (Array.isArray(value)) {
return value.map((item) => String(item).trim()).filter(Boolean);
}
if (typeof value === "string" || typeof value === "number") {
return String(value)
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
return [];
}
+43
View File
@@ -0,0 +1,43 @@
export type UISurface = "chat_inline" | "side_panel" | "canvas" | "map_overlay";
export type UIEnvelope =
| {
kind: "registered_component";
schemaVersion: "agent-ui@1";
component: string;
surface: UISurface;
props: unknown;
data?: unknown;
fallbackText?: string;
}
| {
kind: "chart";
schemaVersion: "agent-ui@1";
grammar: "echarts-safe-subset";
surface: "chat_inline" | "side_panel" | "canvas";
spec: unknown;
data: unknown;
fallbackText?: string;
}
| {
kind: "map_action";
schemaVersion: "agent-ui@1";
action: string;
surface: "map_overlay";
params: unknown;
fallbackText?: string;
};
export type UIEnvelopePayload = {
session_id: string;
envelope_id: string;
created_at: number;
envelope: UIEnvelope;
};
export type UIRegistry = {
schema_version: "agent-ui-registry@1";
chart_grammars: string[];
components: Array<{ id: string; supportedSurfaces: UISurface[] }>;
actions: Array<{ id: string; supportedSurfaces: UISurface[] }>;
};
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { isUiEnvelopeAllowed, parseUiEnvelope, parseUiRegistry } from "./validator";
import type { UIRegistry } from "./types";
const registry: UIRegistry = {
schema_version: "agent-ui-registry@1",
chart_grammars: ["echarts-safe-subset"],
components: [
{ id: "HistoryPanel", supportedSurfaces: ["side_panel", "canvas"] },
{ id: "ScadaPanel", supportedSurfaces: ["side_panel", "canvas"] }
],
actions: [{ id: "zoom_to_map", supportedSurfaces: ["map_overlay"] }]
};
describe("UIEnvelope validation", () => {
it("rejects registered components outside the frontend allowlist", () => {
const envelope = parseUiEnvelope({
kind: "registered_component",
schemaVersion: "agent-ui@1",
component: "UnsafePanel",
surface: "side_panel",
props: {}
});
expect(envelope).not.toBeNull();
expect(envelope ? isUiEnvelopeAllowed(envelope, registry) : false).toBe(false);
});
it("rejects registered components on unsupported surfaces", () => {
const envelope = parseUiEnvelope({
kind: "registered_component",
schemaVersion: "agent-ui@1",
component: "HistoryPanel",
surface: "chat_inline",
props: {}
});
expect(envelope).not.toBeNull();
expect(envelope ? isUiEnvelopeAllowed(envelope, registry) : false).toBe(false);
});
it("accepts only the echarts-safe-subset chart grammar", () => {
expect(
parseUiEnvelope({
kind: "chart",
schemaVersion: "agent-ui@1",
grammar: "vega-lite",
surface: "chat_inline",
spec: {},
data: {}
})
).toBeNull();
const envelope = parseUiEnvelope({
kind: "chart",
schemaVersion: "agent-ui@1",
grammar: "echarts-safe-subset",
surface: "chat_inline",
spec: {},
data: {}
});
expect(envelope).not.toBeNull();
expect(envelope ? isUiEnvelopeAllowed(envelope, registry) : false).toBe(true);
});
it("filters invalid registry surfaces before allowlist checks", () => {
const parsed = parseUiRegistry({
schema_version: "agent-ui-registry@1",
chart_grammars: ["echarts-safe-subset"],
components: [{ id: "HistoryPanel", supportedSurfaces: ["side_panel", "unsafe_surface"] }],
actions: []
});
expect(parsed?.components[0]?.supportedSurfaces).toEqual(["side_panel"]);
});
});
+141
View File
@@ -0,0 +1,141 @@
import type { UIEnvelope, UIEnvelopePayload, UIRegistry, UISurface } from "./types";
const surfaces = new Set<UISurface>(["chat_inline", "side_panel", "canvas", "map_overlay"]);
export function parseUiEnvelopePayload(value: unknown): UIEnvelopePayload | null {
if (!isRecord(value)) {
return null;
}
const envelope = parseUiEnvelope(value.envelope);
if (!envelope) {
return null;
}
return {
session_id: typeof value.session_id === "string" ? value.session_id : "",
envelope_id: typeof value.envelope_id === "string" ? value.envelope_id : "",
created_at: typeof value.created_at === "number" ? value.created_at : Date.now(),
envelope
};
}
export function parseUiEnvelope(value: unknown): UIEnvelope | null {
if (!isRecord(value) || value.schemaVersion !== "agent-ui@1") {
return null;
}
if (value.kind === "registered_component") {
if (typeof value.component !== "string" || !isSurface(value.surface)) {
return null;
}
return {
kind: "registered_component",
schemaVersion: "agent-ui@1",
component: value.component,
surface: value.surface,
props: value.props,
data: value.data,
fallbackText: optionalString(value.fallbackText)
};
}
if (value.kind === "chart") {
if (value.grammar !== "echarts-safe-subset" || !isChartSurface(value.surface)) {
return null;
}
return {
kind: "chart",
schemaVersion: "agent-ui@1",
grammar: "echarts-safe-subset",
surface: value.surface,
spec: value.spec,
data: value.data,
fallbackText: optionalString(value.fallbackText)
};
}
if (value.kind === "map_action") {
if (typeof value.action !== "string" || value.surface !== "map_overlay") {
return null;
}
return {
kind: "map_action",
schemaVersion: "agent-ui@1",
action: value.action,
surface: "map_overlay",
params: value.params,
fallbackText: optionalString(value.fallbackText)
};
}
return null;
}
export function isUiEnvelopeAllowed(envelope: UIEnvelope, registry: UIRegistry | null) {
if (!registry) {
return true;
}
if (envelope.kind === "registered_component") {
const manifest = registry.components.find((item) => item.id === envelope.component);
return Boolean(manifest?.supportedSurfaces.includes(envelope.surface));
}
if (envelope.kind === "chart") {
return registry.chart_grammars.includes(envelope.grammar);
}
const manifest = registry.actions.find((item) => item.id === envelope.action);
return Boolean(manifest?.supportedSurfaces.includes(envelope.surface));
}
export function parseUiRegistry(value: unknown): UIRegistry | null {
if (!isRecord(value) || value.schema_version !== "agent-ui-registry@1") {
return null;
}
return {
schema_version: "agent-ui-registry@1",
chart_grammars: stringArray(value.chart_grammars),
components: manifestArray(value.components),
actions: manifestArray(value.actions)
};
}
function manifestArray(value: unknown) {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((item) => {
if (!isRecord(item) || typeof item.id !== "string") {
return [];
}
return [{ id: item.id, supportedSurfaces: stringArray(item.supportedSurfaces).filter(isSurface) }];
});
}
function stringArray(value: unknown) {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
}
function isChartSurface(value: unknown): value is "chat_inline" | "side_panel" | "canvas" {
return value === "chat_inline" || value === "side_panel" || value === "canvas";
}
function isSurface(value: unknown): value is UISurface {
return typeof value === "string" && surfaces.has(value as UISurface);
}
function optionalString(value: unknown) {
return typeof value === "string" ? value : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}