refactor(agent)!: focus tools on SCADA

This commit is contained in:
2026-07-15 18:30:58 +08:00
parent 8a21908785
commit 155513fdbc
47 changed files with 503 additions and 2910 deletions
-28
View File
@@ -3,36 +3,8 @@ 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 requiredTimeRange = {
start_time: isoTime,
end_time: isoTime,
};
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"]),
...requiredTimeRange,
}).strict().refine((value) => Date.parse(value.start_time) < Date.parse(value.end_time), "start_time must precede 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);
+75 -682
View File
@@ -4,7 +4,7 @@
*/
export interface paths {
"/api/v1/meta/projects": {
"/api/v1/projects": {
parameters: {
query?: never;
header?: never;
@@ -12,7 +12,7 @@ export interface paths {
cookie?: never;
};
/** Projects */
get: operations["projects_api_v1_meta_projects_get"];
get: operations["projects_api_v1_projects_get"];
put?: never;
post?: never;
delete?: never;
@@ -21,15 +21,15 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/meta/project": {
"/api/v1/scada/assets": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Project */
get: operations["project_api_v1_meta_project_get"];
/** Assets */
get: operations["assets_api_v1_scada_assets_get"];
put?: never;
post?: never;
delete?: never;
@@ -38,58 +38,7 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/meta/db/health": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Health */
get: operations["health_api_v1_meta_db_health_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/monitoring/devices": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Devices */
get: operations["devices_api_v1_monitoring_devices_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/monitoring/points": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Points */
get: operations["points_api_v1_monitoring_points_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/monitoring/readings/query": {
"/api/v1/scada/readings/query": {
parameters: {
query?: never;
header?: never;
@@ -99,133 +48,14 @@ export interface paths {
get?: never;
put?: never;
/** Readings */
post: operations["readings_api_v1_monitoring_readings_query_post"];
post: operations["readings_api_v1_scada_readings_query_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/analyses": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Create */
post: operations["create_api_v1_analyses_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/analyses/{job_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get */
get: operations["get_api_v1_analyses__job_id__get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/analyses/{job_id}/cancel": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Cancel */
post: operations["cancel_api_v1_analyses__job_id__cancel_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/simulation/models": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Upload */
post: operations["upload_api_v1_simulation_models_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/simulations": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Create */
post: operations["create_api_v1_simulations_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/simulations/{job_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get */
get: operations["get_api_v1_simulations__job_id__get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/jobs/{job_id}/events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Events */
get: operations["events_api_v1_jobs__job_id__events_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/tianditu/geocode": {
"/api/v1/geocode": {
parameters: {
query?: never;
header?: never;
@@ -235,7 +65,7 @@ export interface paths {
get?: never;
put?: never;
/** Geocode */
post: operations["geocode_api_v1_tianditu_geocode_post"];
post: operations["geocode_api_v1_geocode_post"];
delete?: never;
options?: never;
head?: never;
@@ -280,80 +110,39 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** AnalysisCreate */
AnalysisCreate: {
/**
* Algorithm
* @constant
*/
algorithm: "data-quality";
/** Device Ids */
device_ids: string[];
/**
* Observed From
* Format: date-time
*/
observed_from: string;
/**
* Observed To
* Format: date-time
*/
observed_to: string;
/** GeocodeLocation */
GeocodeLocation: {
/** Lon */
lon: number;
/** Lat */
lat: number;
/** Level */
level?: string | null;
/** Score */
score?: number | null;
/** Keyword */
keyWord?: string | null;
};
/** Body_upload_api_v1_simulation_models_post */
Body_upload_api_v1_simulation_models_post: {
/** Name */
name: string;
/** File */
file: string;
/** GeocodeRequest */
GeocodeRequest: {
/** Keyword */
keyword: string;
};
/** DatabaseHealthResponse */
DatabaseHealthResponse: {
/** Postgres */
postgres: string;
/** Timescale */
timescale: string;
};
/** DeviceOut */
DeviceOut: {
/**
* Id
* Format: uuid
*/
id: string;
/**
* Monitoring Point Id
* Format: uuid
*/
monitoring_point_id: string;
/** External Id */
external_id: string;
/** Active */
active: boolean;
/** GeocodeResponse */
GeocodeResponse: {
/** Status */
status: string | number;
/** Msg */
msg?: string | null;
location?: components["schemas"]["GeocodeLocation"] | null;
/** Searchversion */
searchVersion?: string | null;
};
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
detail?: components["schemas"]["ValidationError"][];
};
/** JobOut */
JobOut: {
/**
* Id
* Format: uuid
*/
id: string;
/** Status */
status: string;
/** Progress */
progress?: number | null;
/** Result */
result?: {
[key: string]: unknown;
} | null;
/** Error */
error?: string | null;
};
/** MapExtent */
MapExtent: {
/** Xmin */
@@ -365,37 +154,8 @@ export interface components {
/** Ymax */
ymax: number;
};
/** ModelUploadOut */
ModelUploadOut: {
/**
* Model Id
* Format: uuid
*/
model_id: string;
/**
* Model Version Id
* Format: uuid
*/
model_version_id: string;
/** Version */
version: number;
/** Sha256 */
sha256: string;
};
/** MonitoringPointOut */
MonitoringPointOut: {
/**
* Id
* Format: uuid
*/
id: string;
/** External Id */
external_id: string;
/** Name */
name: string;
};
/** ProjectMetaResponse */
ProjectMetaResponse: {
/** ProjectResponse */
ProjectResponse: {
/**
* Project Id
* Format: uuid
@@ -418,31 +178,36 @@ export interface components {
gs_workspace: string;
map_extent: components["schemas"]["MapExtent"] | null;
};
/** ProjectSummaryResponse */
ProjectSummaryResponse: {
/** ScadaAssetOut */
ScadaAssetOut: {
/**
* Project Id
* Scada Id
* Format: uuid
*/
project_id: string;
/** Code */
code: string;
scada_id: string;
/** External Id */
external_id: string;
/** Site External Id */
site_external_id: string;
/** Name */
name: string;
/** Description */
description: string | null;
/**
* Status
* Kind
* @enum {string}
*/
status: "active" | "inactive";
/** Project Role */
project_role: string;
kind: "water_quality" | "radar_level" | "ultrasonic_flow";
/** Active */
active: boolean;
};
/** ReadingQuery */
ReadingQuery: {
/** Device Ids */
device_ids: string[];
/** ScadaAssetsResponse */
ScadaAssetsResponse: {
/** Assets */
assets: components["schemas"]["ScadaAssetOut"][];
};
/** ScadaReadingQuery */
ScadaReadingQuery: {
/** Asset Ids */
asset_ids: string[];
/**
* Observed From
* Format: date-time
@@ -460,24 +225,24 @@ export interface components {
*/
granularity: "5m" | "1h" | "1d";
/** Metrics */
metrics: string[];
metrics: ("conductivity_mean" | "level_mean" | "flow_mean" | "temperature_mean")[];
};
/** ReadingQueryResponse */
ReadingQueryResponse: {
/** ScadaReadingQueryResponse */
ScadaReadingQueryResponse: {
/** Data */
data: components["schemas"]["ReadingRow"][];
data: components["schemas"]["ScadaReadingRow"][];
/** Meta */
meta: {
[key: string]: unknown;
};
};
/** ReadingRow */
ReadingRow: {
/** ScadaReadingRow */
ScadaReadingRow: {
/**
* Device Id
* Asset Id
* Format: uuid
*/
device_id: string;
asset_id: string;
/**
* Observed At
* Format: date-time
@@ -488,46 +253,6 @@ export interface components {
[key: string]: number | null;
};
};
/** SimulationCreate */
SimulationCreate: {
/**
* Model Version Id
* Format: uuid
*/
model_version_id: string;
/** Parameters */
parameters?: {
[key: string]: unknown;
};
};
/** TiandituGeocodeLocation */
TiandituGeocodeLocation: {
/** Lon */
lon: number;
/** Lat */
lat: number;
/** Level */
level?: string | null;
/** Score */
score?: number | null;
/** Keyword */
keyWord?: string | null;
};
/** TiandituGeocodeRequest */
TiandituGeocodeRequest: {
/** Keyword */
keyword: string;
};
/** TiandituGeocodeResponse */
TiandituGeocodeResponse: {
/** Status */
status: string | number;
/** Msg */
msg?: string | null;
location?: components["schemas"]["TiandituGeocodeLocation"] | null;
/** Searchversion */
searchVersion?: string | null;
};
/** ValidationError */
ValidationError: {
/** Location */
@@ -550,7 +275,7 @@ export interface components {
}
export type $defs = Record<string, never>;
export interface operations {
projects_api_v1_meta_projects_get: {
projects_api_v1_projects_get: {
parameters: {
query?: never;
header?: never;
@@ -565,12 +290,12 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProjectSummaryResponse"][];
"application/json": components["schemas"]["ProjectResponse"][];
};
};
};
};
project_api_v1_meta_project_get: {
assets_api_v1_scada_assets_get: {
parameters: {
query?: never;
header?: {
@@ -587,7 +312,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ProjectMetaResponse"];
"application/json": components["schemas"]["ScadaAssetsResponse"];
};
};
/** @description Validation Error */
@@ -601,100 +326,7 @@ export interface operations {
};
};
};
health_api_v1_meta_db_health_get: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DatabaseHealthResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
devices_api_v1_monitoring_devices_get: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DeviceOut"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
points_api_v1_monitoring_points_get: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MonitoringPointOut"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
readings_api_v1_monitoring_readings_query_post: {
readings_api_v1_scada_readings_query_post: {
parameters: {
query?: never;
header?: {
@@ -705,7 +337,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["ReadingQuery"];
"application/json": components["schemas"]["ScadaReadingQuery"];
};
};
responses: {
@@ -715,7 +347,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReadingQueryResponse"];
"application/json": components["schemas"]["ScadaReadingQueryResponse"];
};
};
/** @description Validation Error */
@@ -729,246 +361,7 @@ export interface operations {
};
};
};
create_api_v1_analyses_post: {
parameters: {
query?: never;
header?: {
"Idempotency-Key"?: string | null;
"X-Project-Id"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AnalysisCreate"];
};
};
responses: {
/** @description Successful Response */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["JobOut"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_api_v1_analyses__job_id__get: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path: {
job_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["JobOut"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
cancel_api_v1_analyses__job_id__cancel_post: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path: {
job_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["JobOut"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
upload_api_v1_simulation_models_post: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["Body_upload_api_v1_simulation_models_post"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ModelUploadOut"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
create_api_v1_simulations_post: {
parameters: {
query?: never;
header?: {
"Idempotency-Key"?: string | null;
"X-Project-Id"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SimulationCreate"];
};
};
responses: {
/** @description Successful Response */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["JobOut"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_api_v1_simulations__job_id__get: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path: {
job_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["JobOut"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
events_api_v1_jobs__job_id__events_get: {
parameters: {
query?: never;
header?: {
"X-Project-Id"?: string | null;
};
path: {
job_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
geocode_api_v1_tianditu_geocode_post: {
geocode_api_v1_geocode_post: {
parameters: {
query?: never;
header?: never;
@@ -977,7 +370,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["TiandituGeocodeRequest"];
"application/json": components["schemas"]["GeocodeRequest"];
};
};
responses: {
@@ -987,7 +380,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TiandituGeocodeResponse"];
"application/json": components["schemas"]["GeocodeResponse"];
};
};
/** @description Validation Error */
+1 -108
View File
@@ -1,11 +1,8 @@
import { readJsonFile } from "../utils/fileStore.js";
import {
type ResultReferenceKind,
type ResultReferenceRecord,
type ResultReferenceSource,
type RetrievalContext,
RESULT_REFERENCE_KIND,
RESULT_REFERENCE_SOURCE,
type ResultReferenceStore,
} from "./store.js";
@@ -26,12 +23,6 @@ type RegisterResultReferenceInput = {
traceId: string;
};
export type RenderJunctionPayload = {
node_area_map: Record<string, string>;
area_ids?: string[];
area_colors?: Record<string, string>;
};
export class ResultReferenceResolver {
constructor(private readonly store: ResultReferenceStore) {}
@@ -59,34 +50,6 @@ export class ResultReferenceResolver {
});
}
async registerRenderPayloadFile(
filePath: string,
input: Omit<RegisterResultReferenceInput, "data" | "kind" | "schemaVersion">,
) {
const raw = await readJsonFile<unknown>(filePath);
if (raw === null) {
throw new Error(`render payload file not found: ${filePath}`);
}
const wrapper = normalizeRenderPayloadFile(raw, filePath);
if (!wrapper) {
throw new Error("render payload file must use the wrapped { metadata, location, data } format");
}
const payload = extractRenderJunctionPayload(wrapper.data);
if (!payload) {
throw new Error("render payload file does not contain a valid junction render payload");
}
return this.register({
...input,
data: payload,
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
schemaVersion: 1,
source: RESULT_REFERENCE_SOURCE.agentGenerated,
});
}
async getFullAuthorized(
resultRef: string,
context: RetrievalContext,
@@ -136,83 +99,13 @@ export class ResultReferenceResolver {
}
}
export const extractRenderJunctionPayload = (
value: unknown,
): RenderJunctionPayload | null => {
const candidate = unwrapReferencePayload(value);
if (!candidate || !isRecord(candidate.node_area_map)) {
return null;
}
// 节点渲染结果只保留前端真正需要的映射字段,剔除空值并统一转为字符串。
const nodeAreaMap = normalizeStringRecord(candidate.node_area_map);
if (Object.keys(nodeAreaMap).length === 0) {
return null;
}
const areaIds = Array.isArray(candidate.area_ids)
? candidate.area_ids.map((entry) => String(entry).trim()).filter(Boolean)
: undefined;
const areaColors = isRecord(candidate.area_colors)
? normalizeStringRecord(candidate.area_colors)
: undefined;
return {
node_area_map: nodeAreaMap,
...(areaIds && areaIds.length > 0 ? { area_ids: areaIds } : {}),
...(areaColors && Object.keys(areaColors).length > 0
? { area_colors: areaColors }
: {}),
};
};
const normalizeDataForKind = (
kind: ResultReferenceKind,
_kind: ResultReferenceKind,
data: unknown,
schemaVersion: number,
): unknown | null => {
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
return null;
}
if (kind === RESULT_REFERENCE_KIND.renderJunctionsPayload) {
return extractRenderJunctionPayload(data);
}
return data;
};
const normalizeRenderPayloadFile = (
value: unknown,
filePath: string,
): { data: unknown } | null => {
if (!isRecord(value) || !("data" in value)) {
return null;
}
if (!isRecord(value.metadata) || !isRecord(value.location)) {
return null;
}
if (value.location.file_path !== filePath) {
return null;
}
return { data: value.data };
};
const unwrapReferencePayload = (value: unknown): Record<string, unknown> | null => {
if (!isRecord(value)) {
return null;
}
if ("data" in value && value.data !== undefined && value.data !== null) {
return isRecord(value.data) ? value.data : null;
}
return value;
};
const normalizeStringRecord = (value: Record<string, unknown>) =>
Object.fromEntries(
Object.entries(value)
.map(([key, entry]) => [String(key), String(entry ?? "").trim()])
.filter(([, entry]) => entry.length > 0),
);
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
-1
View File
@@ -16,7 +16,6 @@ export const RESULT_REF_PATTERN = /^res-[a-f0-9-]{8,64}$/;
const RESULT_REF_FILE_PATTERN = /^(res-[a-f0-9-]{8,64})(?:\.json)?$/;
export const RESULT_REFERENCE_KIND = {
renderJunctionsPayload: "render-junctions-payload",
serverApiPayload: "server_api_payload",
} as const;
-38
View File
@@ -31,11 +31,6 @@ import { FrontendActionCoordinator } from "../frontendAction/coordinator.js";
import { frontendActionRegistryResponse } from "../frontendAction/registry.js";
import type { UIEnvelopePayload } from "../uiEnvelope/types.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
import {
createPressureTrendDemoEnvelopePayload,
createPressureTrendDemoToolCall,
isPressureTrendDemoPrompt,
} from "./pressureTrendDemo.js";
import {
buildForkedSessionUiState,
buildPromptWithLearningContext,
@@ -1059,39 +1054,6 @@ export const buildChatRouter = (
: undefined;
try {
if (isPressureTrendDemoPrompt(promptText)) {
const toolCall = createPressureTrendDemoToolCall();
publish("progress", {
session_id: clientSessionId,
id: "pressure-trend-demo",
phase: "tool",
status: "running",
title: "生成压力趋势图",
detail: "已命中受控演示用例,正在通过 show_chart 生成可信 UIEnvelope。",
started_at: Date.now(),
elapsed_ms: 0,
});
publish("tool_call", {
session_id: clientSessionId,
tool: toolCall.tool,
params: toolCall.params,
reason: toolCall.reason,
});
publish(
"ui_envelope",
createPressureTrendDemoEnvelopePayload(clientSessionId, toolCall),
);
publish("token", {
session_id: clientSessionId,
content: "已生成最近压力趋势图,并通过可信 UIEnvelope 发送到前端。",
});
publish("done", {
session_id: clientSessionId,
duration_ms: 0,
});
return;
}
const preparedMessage = await buildPromptWithLearningContext(
memoryStore,
requestContext.actorKey,
-37
View File
@@ -48,43 +48,6 @@ export const registerChatAuxiliaryRoutes = (
frontendActionCoordinator,
}: RegisterAuxiliaryRoutesOptions,
) => {
chatRouter.get("/render-ref/:render_ref", async (req, res) => {
const renderRef = req.params.render_ref?.trim();
const authContext = getLocalAgentContext(req);
const userId = authContext.userId;
const projectId = authContext.projectId;
const clientSessionId =
typeof req.query.session_id === "string"
? req.query.session_id.trim()
: undefined;
if (!renderRef) {
res.status(400).json({
message: "render_ref is required",
});
return;
}
const result = await resultReferenceResolver.getFullAuthorized(
renderRef,
{
actorKey: toActorKey(userId),
clientSessionId,
projectId,
},
{
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
},
);
if (!result) {
res.status(404).json({ message: "render_ref not found" });
return;
}
res.json(result);
});
chatRouter.get("/result-ref/:result_ref", async (req, res) => {
const resultRef = req.params.result_ref?.trim();
const context = getLocalAgentContext(req);
-4
View File
@@ -4,7 +4,6 @@ import { MemoryStore } from "../memory/store.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { collectTextContent } from "./chatStream.js";
import { isPressureTrendDemoPrompt } from "./pressureTrendDemo.js";
const TITLE_PROMPT_TIMEOUT_MS = 5000;
const TITLE_CONTEXT_MESSAGE_LIMIT = 40;
@@ -241,9 +240,6 @@ export const resolveRequestedStreamSessionId = (input: {
if (sessionId) {
return sessionId;
}
if (isPressureTrendDemoPrompt(input.promptText)) {
return input.createClientSessionId();
}
return undefined;
};
+1 -1
View File
@@ -830,7 +830,7 @@ export const streamPromptResponse = async ({
params: toolParams,
reason,
});
if (envelope && !(suppressLegacyFrontendActions && ["zoom_to_map", "locate_features", "view_history", "view_scada"].includes(part.tool))) {
if (envelope && !(suppressLegacyFrontendActions && part.tool === "zoom_to_map")) {
write("ui_envelope", {
session_id: clientSessionId,
envelope_id: createEnvelopeId(part.tool),
-5
View File
@@ -65,12 +65,7 @@ const toolLabels: Record<string, string> = {
session_search: "历史会话检索",
skill_manager: "流程沉淀",
web_search: "网页搜索",
locate_features: "地图定位",
zoom_to_map: "地图缩放",
view_history: "历史数据面板",
view_scada: "SCADA 面板",
show_chart: "图表渲染",
render_junctions: "节点渲染",
};
export const logDevelopmentDebug = (
+1 -15
View File
@@ -207,17 +207,9 @@ export const updateLastAssistantQuestion = (
});
const getToolArtifactKind = (tool: string): ToolArtifactKind => {
if (tool === "show_chart" || tool === "chart") return "chart";
if (
tool === "locate_features" ||
tool === "zoom_to_map" ||
tool === "render_junctions" ||
tool === "apply_layer_style" ||
tool.startsWith("locate_")
) {
if (tool === "zoom_to_map") {
return "map";
}
if (tool === "view_history" || tool === "view_scada") return "panel";
return "tool";
};
@@ -225,13 +217,7 @@ const getToolArtifactTitle = (tool: string, params: Record<string, unknown>) =>
if (typeof params.title === "string" && params.title.trim()) {
return params.title.trim();
}
if (tool === "show_chart" || tool === "chart") return "生成图表";
if (tool === "zoom_to_map") return "缩放到地图坐标";
if (tool === "render_junctions") return "渲染节点分区";
if (tool === "view_history") return "打开计算结果曲线";
if (tool === "view_scada") return "打开 SCADA 数据面板";
if (tool === "apply_layer_style") return "应用图层样式";
if (tool === "locate_features" || tool.startsWith("locate_")) return "地图定位";
return tool || "工具调用";
};
-66
View File
@@ -1,66 +0,0 @@
import { createEnvelopeId } from "../uiEnvelope/ids.js";
import { toUiEnvelopeFromToolCall } from "../uiEnvelope/fromToolCall.js";
import type { UIEnvelopePayload } from "../uiEnvelope/types.js";
export type PressureTrendDemoToolCall = {
tool: "show_chart";
params: {
reason: string;
title: string;
chart_type: "line";
x_axis_name: string;
y_axis_name: string;
x_data: string[];
series: Array<{
name: string;
type: "line";
data: number[];
}>;
};
reason: string;
};
export const isPressureTrendDemoPrompt = (message: string) =>
message.replace(/\s+/g, "").includes("展示最近压力趋势");
export const createPressureTrendDemoToolCall = (): PressureTrendDemoToolCall => ({
tool: "show_chart",
reason: "展示最近 6 小时压力趋势。",
params: {
reason: "展示最近 6 小时压力趋势。",
title: "最近压力趋势",
chart_type: "line",
x_axis_name: "时间",
y_axis_name: "压力 MPa",
x_data: ["10:00", "11:00", "12:00", "13:00", "14:00", "15:00"],
series: [
{
name: "东部主干压力",
type: "line",
data: [0.42, 0.41, 0.39, 0.36, 0.35, 0.37],
},
{
name: "正常基线",
type: "line",
data: [0.43, 0.43, 0.42, 0.42, 0.41, 0.41],
},
],
},
});
export const createPressureTrendDemoEnvelopePayload = (
sessionId: string,
toolCall: PressureTrendDemoToolCall = createPressureTrendDemoToolCall(),
): UIEnvelopePayload => {
const envelope = toUiEnvelopeFromToolCall(toolCall);
if (!envelope) {
throw new Error("pressure trend demo tool call did not produce a UIEnvelope");
}
return {
session_id: sessionId,
envelope_id: createEnvelopeId(toolCall.tool),
created_at: Date.now(),
envelope,
};
};
+2 -56
View File
@@ -11,10 +11,7 @@ import { logger } from "./logger.js";
import { LearningOrchestrator } from "./learning/orchestrator.js";
import { MemoryStore } from "./memory/store.js";
import { ResultReferenceResolver } from "./results/resolver.js";
import {
RESULT_REFERENCE_SOURCE,
ResultReferenceStore,
} from "./results/store.js";
import { ResultReferenceStore } from "./results/store.js";
import { buildChatRouter } from "./routes/chat.js";
import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js";
import { opencodeRuntime } from "./runtime/opencode.js";
@@ -44,7 +41,6 @@ const serverApiGateway = new ServerApiGateway(resultReferenceResolver);
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
const frontendActionCoordinator = new FrontendActionCoordinator();
// 这个 token 只用于仍需服务端上下文的工具桥(store_render_ref)。
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
app.use(cors());
@@ -124,56 +120,6 @@ app.get("/health", async (_req, res) => {
}
});
app.post("/internal/tools/store-render-ref", async (req, res) => {
if (req.header("x-agent-internal-token") !== internalToken) {
res.status(403).json({ message: "forbidden" });
return;
}
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : "";
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
detail: sessionId,
});
return;
}
if (!filePath) {
res.status(400).json({ message: "file_path is required" });
return;
}
try {
const record = await resultReferenceResolver.registerRenderPayloadFile(filePath, {
actorKey: context.actorKey,
clientSessionId: context.clientSessionId,
projectId: context.projectId,
projectKey: context.projectKey,
sessionId: context.clientSessionId,
source: RESULT_REFERENCE_SOURCE.agentGenerated,
traceId: context.traceId,
});
res.json({
ok: true,
render_ref: record.resultRef,
stored_at: record.createdAt,
preview: record.preview,
kind: record.kind,
schema_version: record.schemaVersion,
source: record.source,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
res.status(400).json({
message: "store render ref failed",
detail,
});
}
});
app.post("/internal/tools/session-search", async (req, res) => {
if (req.header("x-agent-internal-token") !== internalToken) {
res.status(403).json({ message: "forbidden" });
@@ -340,7 +286,7 @@ app.post("/internal/tools/geocode", async (req, res) => {
try {
const response = await callBackendJson(
"/api/v1/tianditu/geocode",
"/api/v1/geocode",
context,
{ keyword },
);
+19 -26
View File
@@ -1,4 +1,3 @@
import { createHash } from "node:crypto";
import { z } from "zod";
import { config } from "../config.js";
@@ -21,11 +20,8 @@ const responseSchemas: Record<DomainToolName, z.ZodTypeAny> = {
gs_workspace: z.string(),
map_extent: z.object({ xmin: z.number(), ymin: z.number(), xmax: z.number(), ymax: z.number() }).nullable(),
}),
list_monitoring_assets: z.object({ devices: z.array(z.unknown()).optional(), points: z.array(z.unknown()).optional() }),
query_monitoring_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }),
start_data_quality_analysis: z.object({ id: z.string().uuid(), status: z.string() }).passthrough(),
start_simulation: z.object({ id: z.string().uuid(), status: z.string() }).passthrough(),
get_job_status: z.object({ id: z.string().uuid(), status: z.string() }).passthrough(),
list_scada_assets: z.object({ assets: z.array(z.unknown()) }),
query_scada_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }),
};
export class ServerApiGateway {
@@ -62,33 +58,32 @@ export class ServerApiGateway {
}
private async dispatch(name: DomainToolName, args: any, context: RuntimeSessionContext): Promise<unknown> {
if (name === "list_monitoring_assets") {
const result: Record<string, unknown> = {};
if (args.include !== "points") result.devices = await this.request("GET", "/api/v1/monitoring/devices", undefined, context);
if (args.include !== "devices") result.points = await this.request("GET", "/api/v1/monitoring/points", undefined, context);
return result;
if (name === "get_project_context") {
const projects = await this.request("GET", "/api/v1/projects", undefined, context);
if (!Array.isArray(projects)) throw new GatewayHttpError(502);
const project = projects.find((item) => (
typeof item === "object"
&& item !== null
&& "project_id" in item
&& item.project_id === context.projectId
));
if (!project) throw new GatewayHttpError(404);
return project;
}
const routes = {
get_project_context: ["GET", "/api/v1/meta/project"],
query_monitoring_readings: ["POST", "/api/v1/monitoring/readings/query"],
start_data_quality_analysis: ["POST", "/api/v1/analyses"],
start_simulation: ["POST", "/api/v1/simulations"],
get_job_status: ["GET", args.job_type === "analysis" ? `/api/v1/analyses/${args.job_id}` : `/api/v1/simulations/${args.job_id}`],
} as const;
const [method, path] = routes[name];
const body = name === "start_data_quality_analysis" ? { ...args, algorithm: "data-quality" } : method === "POST" ? args : undefined;
const idempotencyKey = name.startsWith("start_") ? createIdempotencyKey(context, name, body) : undefined;
return this.request(method, path, body, context, idempotencyKey);
if (name === "list_scada_assets") {
return this.request("GET", "/api/v1/scada/assets", undefined, context);
}
return this.request("POST", "/api/v1/scada/readings/query", args, context);
}
private async request(method: string, path: string, body: unknown, context: RuntimeSessionContext, idempotencyKey?: string) {
private async request(method: string, path: string, body: unknown, context: RuntimeSessionContext) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
try {
const response = await this.fetchImpl(new URL(path, config.TJWATER_API_BASE_URL), {
method, signal: controller.signal, headers: {
Accept: "application/json", "Content-Type": "application/json", "X-Project-Id": context.projectId!,
"X-Request-Id": context.traceId, ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
"X-Request-Id": context.traceId,
}, ...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
if (!response.ok) throw new GatewayHttpError(response.status);
@@ -99,5 +94,3 @@ export class ServerApiGateway {
class GatewayHttpError extends Error { constructor(readonly status: number) { super("upstream request failed"); } }
const failure = (code: GatewayErrorCode, message: string, detail?: unknown) => ({ ok: false, error: { code, message, ...(detail ? { detail } : {}) } });
export const createIdempotencyKey = (context: RuntimeSessionContext, name: string, args: unknown) => createHash("sha256").update(JSON.stringify([context.clientSessionId, name, canonicalize(args)])).digest("hex");
const canonicalize = (value: unknown): unknown => Array.isArray(value) ? value.map(canonicalize) : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, canonicalize(entry)])) : value;
+5 -15
View File
@@ -3,31 +3,21 @@ import { z } from "zod";
const uuid = z.string().uuid();
const dateTime = z.string().datetime({ offset: true });
const metrics = z.enum([
"flow_mean", "flow_total", "conductivity_mean", "velocity_mean",
"temperature_mean", "level_mean",
"conductivity_mean", "level_mean", "flow_mean", "temperature_mean",
]);
export const domainToolSchemas = {
get_project_context: z.object({}),
list_monitoring_assets: z.object({ include: z.enum(["devices", "points", "all"]).default("all") }),
query_monitoring_readings: z.object({
device_ids: z.array(uuid).min(1).max(100),
list_scada_assets: z.object({}),
query_scada_readings: z.object({
asset_ids: z.array(uuid).min(1).max(100),
observed_from: dateTime,
observed_to: dateTime,
granularity: z.enum(["5m", "1h", "1d"]).default("5m"),
metrics: z.array(metrics).min(1).max(6),
metrics: z.array(metrics).min(1).max(4),
}).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), {
message: "observed_from must precede observed_to",
}),
start_data_quality_analysis: z.object({
device_ids: z.array(uuid).min(1).max(100),
observed_from: dateTime,
observed_to: dateTime,
}).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), {
message: "observed_from must precede observed_to",
}),
start_simulation: z.object({ model_version_id: uuid, parameters: z.record(z.unknown()).default({}) }),
get_job_status: z.object({ job_id: uuid, job_type: z.enum(["analysis", "simulation"]) }),
} as const;
export type DomainToolName = keyof typeof domainToolSchemas;
+1 -96
View File
@@ -1,5 +1,3 @@
import { RESULT_REF_PATTERN } from "../results/store.js";
import { chartGrammar } from "./registry.js";
import type { UIEnvelope } from "./types.js";
type ToolCallInput = {
@@ -8,67 +6,12 @@ type ToolCallInput = {
reason?: string;
};
const isStringArray = (value: unknown): value is string[] =>
Array.isArray(value) && value.every((item) => typeof item === "string");
const normalizeSeries = (value: unknown) =>
Array.isArray(value)
? value
.filter(
(item): item is { name: string; data: number[]; type?: "line" | "bar" } =>
typeof item === "object" &&
item !== null &&
"name" in item &&
typeof item.name === "string" &&
"data" in item &&
Array.isArray(item.data) &&
item.data.every((entry: unknown) => typeof entry === "number"),
)
.map((item) => ({
name: item.name,
data: item.data,
...(item.type === "line" || item.type === "bar" ? { type: item.type } : {}),
}))
: [];
export const toUiEnvelopeFromToolCall = ({
tool,
params,
reason,
}: ToolCallInput): UIEnvelope | null => {
if (tool === "show_chart" || tool === "chart") {
const xData = isStringArray(params.x_data) ? params.x_data : [];
const series = normalizeSeries(params.series);
if (xData.length === 0 || series.length === 0) {
return null;
}
const chartType = params.chart_type === "bar" ? "bar" : "line";
return {
kind: "chart",
schemaVersion: "agent-ui@1",
grammar: chartGrammar,
surface: "chat_inline",
spec: {
title: typeof params.title === "string" ? params.title : undefined,
chart_type: chartType,
x_axis_name:
typeof params.x_axis_name === "string" ? params.x_axis_name : undefined,
y_axis_name:
typeof params.y_axis_name === "string" ? params.y_axis_name : undefined,
},
data: {
x_data: xData,
series,
},
fallbackText: reason || "已生成图表。",
};
}
if (
tool === "locate_features" ||
tool === "zoom_to_map" ||
tool === "apply_layer_style"
) {
if (tool === "zoom_to_map") {
return {
kind: "map_action",
schemaVersion: "agent-ui@1",
@@ -79,43 +22,5 @@ export const toUiEnvelopeFromToolCall = ({
};
}
if (tool === "render_junctions") {
const renderRef =
typeof params.render_ref === "string" ? params.render_ref.trim() : "";
if (!RESULT_REF_PATTERN.test(renderRef)) {
return null;
}
return {
kind: "map_action",
schemaVersion: "agent-ui@1",
action: tool,
surface: "map_overlay",
params: { render_ref: renderRef },
fallbackText: reason || "已生成节点渲染操作。",
};
}
if (tool === "view_history") {
return {
kind: "registered_component",
schemaVersion: "agent-ui@1",
component: "HistoryPanel",
surface: "side_panel",
props: params,
fallbackText: reason || "已打开历史数据面板。",
};
}
if (tool === "view_scada") {
return {
kind: "registered_component",
schemaVersion: "agent-ui@1",
component: "ScadaPanel",
surface: "side_panel",
props: params,
fallbackText: reason || "已打开 SCADA 面板。",
};
}
return null;
};
+2 -33
View File
@@ -2,51 +2,20 @@ import type { ActionManifest, ComponentManifest } from "./types.js";
export const chartGrammar = "echarts-safe-subset" as const;
export const componentRegistry: ComponentManifest[] = [
{
id: "HistoryPanel",
version: "1.0.0",
description: "Open a historical result panel for selected network features.",
supportedSurfaces: ["side_panel", "canvas"],
},
{
id: "ScadaPanel",
version: "1.0.0",
description: "Open a SCADA history panel for selected devices.",
supportedSurfaces: ["side_panel", "canvas"],
},
];
export const componentRegistry: ComponentManifest[] = [];
export const actionRegistry: ActionManifest[] = [
{
id: "locate_features",
version: "1.0.0",
description: "Locate and highlight network features on the map.",
supportedSurfaces: ["map_overlay"],
},
{
id: "zoom_to_map",
version: "1.0.0",
description: "Zoom the map to a coordinate.",
supportedSurfaces: ["map_overlay"],
},
{
id: "render_junctions",
version: "1.0.0",
description: "Render junction categories from an authorized render_ref.",
supportedSurfaces: ["map_overlay"],
},
{
id: "apply_layer_style",
version: "1.0.0",
description: "Apply or reset a map layer style.",
supportedSurfaces: ["map_overlay"],
},
];
export const uiRegistryResponse = {
schema_version: "agent-ui-registry@1",
chart_grammars: [chartGrammar],
chart_grammars: [],
components: componentRegistry,
actions: actionRegistry,
};