refactor(scada): use canonical device identifiers
This commit is contained in:
@@ -32,8 +32,8 @@ Agent 负责:
|
|||||||
1. `render_scada_analysis` 只能在分析结论已经完成后调用,不得让前端计算、猜测或修改高中低等级。
|
1. `render_scada_analysis` 只能在分析结论已经完成后调用,不得让前端计算、猜测或修改高中低等级。
|
||||||
2. 每次普通工具调用必须填写具体 `reason`。
|
2. 每次普通工具调用必须填写具体 `reason`。
|
||||||
3. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。
|
3. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。
|
||||||
4. SCADA 地图结果只允许使用可信 `sensor_id` 和 `high | medium | low | unrated` 等级,不得传入自定义标签、颜色或 HTML。
|
4. SCADA 地图结果只允许使用后端 API 返回的可信 `device_id` 和 `high | medium | low | unrated` 等级,不得传入自定义标签、颜色或 HTML。
|
||||||
5. 同一批结果不得包含重复 `sensor_id`,每次最多 100 个点位。
|
5. 同一批结果不得包含重复 `device_id`,每次最多 100 个点位。
|
||||||
6. 只有 SCADA 地图工具返回浏览器的成功结果后,才能声称已渲染或已清除;工具报错、超时或无活跃浏览器连接时,必须明确告知用户动作失败,不得伪造已渲染的点位、编号或等级。
|
6. 只有 SCADA 地图工具返回浏览器的成功结果后,才能声称已渲染或已清除;工具报错、超时或无活跃浏览器连接时,必须明确告知用户动作失败,不得伪造已渲染的点位、编号或等级。
|
||||||
|
|
||||||
## 执行约束
|
## 执行约束
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { tool } from "@opencode-ai/plugin";
|
|||||||
|
|
||||||
import { executeFrontendAction } from "./frontend_action.js";
|
import { executeFrontendAction } from "./frontend_action.js";
|
||||||
|
|
||||||
const sensorId = tool.schema.string().trim().min(1).max(128);
|
const deviceId = tool.schema.string().trim().min(1).max(128);
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description:
|
description:
|
||||||
@@ -16,7 +16,7 @@ export default tool({
|
|||||||
items: tool.schema
|
items: tool.schema
|
||||||
.array(
|
.array(
|
||||||
tool.schema.object({
|
tool.schema.object({
|
||||||
sensor_id: sensorId.describe("Trusted SCADA sensor identifier."),
|
device_id: deviceId.describe("Trusted SCADA device identifier from the backend API."),
|
||||||
level: tool.schema
|
level: tool.schema
|
||||||
.enum(["high", "medium", "low", "unrated"])
|
.enum(["high", "medium", "low", "unrated"])
|
||||||
.describe("Completed analysis level; use unrated only when no level was concluded."),
|
.describe("Completed analysis level; use unrated only when no level was concluded."),
|
||||||
@@ -25,8 +25,8 @@ export default tool({
|
|||||||
.min(1)
|
.min(1)
|
||||||
.max(100)
|
.max(100)
|
||||||
.refine(
|
.refine(
|
||||||
(items) => new Set(items.map((item) => item.sensor_id)).size === items.length,
|
(items) => new Set(items.map((item) => item.device_id)).size === items.length,
|
||||||
"sensor_id values must be unique",
|
"device_id values must be unique",
|
||||||
)
|
)
|
||||||
.describe("One to one hundred unique SCADA analysis results."),
|
.describe("One to one hundred unique SCADA analysis results."),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
import type { FrontendActionManifest } from "./types.js";
|
import type { FrontendActionManifest } from "./types.js";
|
||||||
|
|
||||||
type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny };
|
type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny };
|
||||||
const sensorId = z.string().trim().min(1).max(128);
|
const deviceId = z.string().trim().min(1).max(128);
|
||||||
const scadaLevel = z.enum(["high", "medium", "low", "unrated"]);
|
const scadaLevel = z.enum(["high", "medium", "low", "unrated"]);
|
||||||
const levelCounts = z.object({
|
const levelCounts = z.object({
|
||||||
high: z.number().int().nonnegative(),
|
high: z.number().int().nonnegative(),
|
||||||
@@ -11,18 +11,18 @@ const levelCounts = z.object({
|
|||||||
unrated: z.number().int().nonnegative(),
|
unrated: z.number().int().nonnegative(),
|
||||||
}).strict();
|
}).strict();
|
||||||
const renderScadaAnalysisInput = z.object({
|
const renderScadaAnalysisInput = z.object({
|
||||||
items: z.array(z.object({ sensor_id: sensorId, level: scadaLevel }).strict()).min(1).max(100),
|
items: z.array(z.object({ device_id: deviceId, level: scadaLevel }).strict()).min(1).max(100),
|
||||||
}).strict().superRefine((value, context) => {
|
}).strict().superRefine((value, context) => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
value.items.forEach((item, index) => {
|
value.items.forEach((item, index) => {
|
||||||
if (seen.has(item.sensor_id)) {
|
if (seen.has(item.device_id)) {
|
||||||
context.addIssue({
|
context.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "sensor_id values must be unique",
|
message: "device_id values must be unique",
|
||||||
path: ["items", index, "sensor_id"],
|
path: ["items", index, "device_id"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
seen.add(item.sensor_id);
|
seen.add(item.device_id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const definitions: Definition[] = [
|
const definitions: Definition[] = [
|
||||||
@@ -36,8 +36,8 @@ const definitions: Definition[] = [
|
|||||||
},
|
},
|
||||||
inputSchema: renderScadaAnalysisInput,
|
inputSchema: renderScadaAnalysisInput,
|
||||||
outputSchema: z.object({
|
outputSchema: z.object({
|
||||||
rendered_ids: z.array(sensorId),
|
rendered_ids: z.array(deviceId),
|
||||||
missing_ids: z.array(sensorId),
|
missing_ids: z.array(deviceId),
|
||||||
level_counts: levelCounts,
|
level_counts: levelCounts,
|
||||||
fitted: z.literal(true),
|
fitted: z.literal(true),
|
||||||
}).strict(),
|
}).strict(),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { frontendActionRegistry } from "../../src/frontendAction/registry.js";
|
|||||||
import type { FrontendActionRequest } from "../../src/frontendAction/types.js";
|
import type { FrontendActionRequest } from "../../src/frontendAction/types.js";
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ sensor_id: "MP01", level: "high" },
|
{ device_id: "MP01", level: "high" },
|
||||||
{ sensor_id: "MP02", level: "medium" },
|
{ device_id: "MP02", level: "medium" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const start = (
|
const start = (
|
||||||
@@ -119,10 +119,10 @@ describe("FrontendActionCoordinator", () => {
|
|||||||
params: value,
|
params: value,
|
||||||
});
|
});
|
||||||
expect(() => request({ items: [] })).toThrow(FrontendActionError);
|
expect(() => request({ items: [] })).toThrow(FrontendActionError);
|
||||||
expect(() => request({ items: Array.from({ length: 101 }, (_, index) => ({ sensor_id: `MP${index}`, level: "low" })) })).toThrow("invalid frontend action params");
|
expect(() => request({ items: Array.from({ length: 101 }, (_, index) => ({ device_id: `MP${index}`, level: "low" })) })).toThrow("invalid frontend action params");
|
||||||
expect(() => request({ items: [{ sensor_id: "MP01", level: "high" }, { sensor_id: "MP01", level: "low" }] })).toThrow("invalid frontend action params");
|
expect(() => request({ items: [{ device_id: "MP01", level: "high" }, { device_id: "MP01", level: "low" }] })).toThrow("invalid frontend action params");
|
||||||
expect(() => request({ items: [{ sensor_id: "x".repeat(129), level: "low" }] })).toThrow("invalid frontend action params");
|
expect(() => request({ items: [{ device_id: "x".repeat(129), level: "low" }] })).toThrow("invalid frontend action params");
|
||||||
expect(() => request({ items: [{ sensor_id: "MP01", level: "critical" }] })).toThrow("invalid frontend action params");
|
expect(() => request({ items: [{ device_id: "MP01", level: "critical" }] })).toThrow("invalid frontend action params");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps terminal results idempotent and rejects conflicts", async () => {
|
it("keeps terminal results idempotent and rejects conflicts", async () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("appendBackendToolArtifact", () => {
|
|||||||
const artifacts = appendBackendToolArtifact([], {
|
const artifacts = appendBackendToolArtifact([], {
|
||||||
tool: "render_scada_analysis",
|
tool: "render_scada_analysis",
|
||||||
reason: "显示已完成的分级结论",
|
reason: "显示已完成的分级结论",
|
||||||
params: { items: [{ sensor_id: "MP01", level: "high" }] },
|
params: { items: [{ device_id: "MP01", level: "high" }] },
|
||||||
}) as Array<Record<string, unknown>>;
|
}) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
expect(artifacts[0]).toMatchObject({
|
expect(artifacts[0]).toMatchObject({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("toUiEnvelopeFromToolCall", () => {
|
|||||||
it("does not generate duplicate UIEnvelopes for SCADA frontend actions", () => {
|
it("does not generate duplicate UIEnvelopes for SCADA frontend actions", () => {
|
||||||
expect(toUiEnvelopeFromToolCall({
|
expect(toUiEnvelopeFromToolCall({
|
||||||
tool: "render_scada_analysis",
|
tool: "render_scada_analysis",
|
||||||
params: { items: [{ sensor_id: "MP01", level: "high" }] },
|
params: { items: [{ device_id: "MP01", level: "high" }] },
|
||||||
})).toBeNull();
|
})).toBeNull();
|
||||||
expect(toUiEnvelopeFromToolCall({
|
expect(toUiEnvelopeFromToolCall({
|
||||||
tool: "clear_scada_analysis",
|
tool: "clear_scada_analysis",
|
||||||
|
|||||||
Reference in New Issue
Block a user