feat: add SCADA frontend action bridge
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -60,7 +60,9 @@ export type TodoUpdatePayload = {
|
||||
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
||||
|
||||
const toolLabels: Record<string, string> = {
|
||||
clear_scada_analysis: "清除 SCADA 地图分析",
|
||||
memory_manager: "记忆写入",
|
||||
render_scada_analysis: "渲染 SCADA 地图分析",
|
||||
session_search: "历史会话检索",
|
||||
skill_manager: "流程沉淀",
|
||||
};
|
||||
|
||||
@@ -207,6 +207,9 @@ export const updateLastAssistantQuestion = (
|
||||
});
|
||||
|
||||
const getToolArtifactKind = (tool: string): ToolArtifactKind => {
|
||||
if (tool === "render_scada_analysis" || tool === "clear_scada_analysis") {
|
||||
return "map";
|
||||
}
|
||||
return "tool";
|
||||
};
|
||||
|
||||
|
||||
+1
-7
@@ -52,13 +52,7 @@ app.post("/internal/frontend-actions/request", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
if (!context.capabilities?.includes("frontend-action@1")) {
|
||||
// Legacy clients still execute the existing UIEnvelope emitted from the tool call.
|
||||
res.json({
|
||||
version: "frontend-action-result@1",
|
||||
status: "succeeded",
|
||||
output: { legacy: true },
|
||||
detail: "legacy client executes the action from its UIEnvelope",
|
||||
});
|
||||
res.status(409).json({ message: "frontend-action@1 browser capability is required" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user