312 lines
9.1 KiB
TypeScript
312 lines
9.1 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback } from "react";
|
|
|
|
import { useChatToolStore, type ChatToolAction } from "@/store/chatToolStore";
|
|
import type { StreamEvent } from "@/lib/chatStream";
|
|
import type { AgentArtifact, AgentArtifactKind } from "../GlobalChatbox.types";
|
|
import {
|
|
APPLY_LAYER_STYLE_TOOL,
|
|
describeApplyLayerStyle,
|
|
parseApplyLayerStylePayload,
|
|
} from "../toolCallStyleHelpers";
|
|
|
|
type ToolCallEvent = StreamEvent & { type: "tool_call" };
|
|
|
|
type HandleToolCallOptions = {
|
|
assistantMessageId: string;
|
|
appendArtifact: (messageId: string, artifact: AgentArtifact) => void;
|
|
};
|
|
|
|
const FEATURE_TYPE_MAP: Record<
|
|
string,
|
|
{ layer: string; geometryKind: "point" | "line"; label: string }
|
|
> = {
|
|
junction: { layer: "geo_junctions_mat", geometryKind: "point", label: "节点" },
|
|
junctions: { layer: "geo_junctions_mat", geometryKind: "point", label: "节点" },
|
|
pipe: { layer: "geo_pipes_mat", geometryKind: "line", label: "管道" },
|
|
pipes: { layer: "geo_pipes_mat", geometryKind: "line", label: "管道" },
|
|
valve: { layer: "geo_valves", geometryKind: "point", label: "阀门" },
|
|
valves: { layer: "geo_valves", geometryKind: "point", label: "阀门" },
|
|
reservoir: { layer: "geo_reservoirs", geometryKind: "point", label: "水源" },
|
|
reservoirs: { layer: "geo_reservoirs", geometryKind: "point", label: "水源" },
|
|
pump: { layer: "geo_pumps", geometryKind: "point", label: "泵站" },
|
|
pumps: { layer: "geo_pumps", geometryKind: "point", label: "泵站" },
|
|
tank: { layer: "geo_tanks", geometryKind: "point", label: "水池" },
|
|
tanks: { layer: "geo_tanks", geometryKind: "point", label: "水池" },
|
|
};
|
|
|
|
const LOCATE_TOOL_CONFIG: Record<
|
|
string,
|
|
{ layer: string; geometryKind: "point" | "line"; label: string }
|
|
> = {
|
|
locate_pipes: { layer: "geo_pipes_mat", geometryKind: "line", label: "管道" },
|
|
locate_junctions: { layer: "geo_junctions_mat", geometryKind: "point", label: "节点" },
|
|
locate_valves: { layer: "geo_valves", geometryKind: "point", label: "阀门" },
|
|
locate_reservoirs: { layer: "geo_reservoirs", geometryKind: "point", label: "水源" },
|
|
locate_pumps: { layer: "geo_pumps", geometryKind: "point", label: "泵站" },
|
|
locate_tanks: { layer: "geo_tanks", geometryKind: "point", label: "水池" },
|
|
};
|
|
|
|
const LOCATE_ID_PARAM_KEYS = [
|
|
"ids",
|
|
"id",
|
|
"feature_ids",
|
|
"feature_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;
|
|
|
|
const normalizeIds = (params: Record<string, unknown>): string[] => {
|
|
for (const key of LOCATE_ID_PARAM_KEYS) {
|
|
const rawValue = params[key];
|
|
if (Array.isArray(rawValue)) {
|
|
const normalized = rawValue.map((id) => String(id).trim()).filter(Boolean);
|
|
if (normalized.length > 0) {
|
|
return normalized;
|
|
}
|
|
}
|
|
if (typeof rawValue === "string" || typeof rawValue === "number") {
|
|
const normalized = String(rawValue)
|
|
.split(",")
|
|
.map((id) => id.trim())
|
|
.filter(Boolean);
|
|
if (normalized.length > 0) {
|
|
return normalized;
|
|
}
|
|
}
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const resolveScadaFeatureInfos = (params: Record<string, unknown>): [string, string][] => {
|
|
const rawFeatureInfos = params.feature_infos;
|
|
if (Array.isArray(rawFeatureInfos)) {
|
|
const normalizedFeatureInfos = rawFeatureInfos
|
|
.map((item) => (Array.isArray(item) ? item : null))
|
|
.filter((item): item is [unknown, unknown] => Boolean(item))
|
|
.map(
|
|
(item) =>
|
|
[String(item[0] ?? ""), String(item[1] ?? "scada")] as [
|
|
string,
|
|
string,
|
|
],
|
|
)
|
|
.filter(([id]) => id.trim().length > 0);
|
|
if (normalizedFeatureInfos.length > 0) {
|
|
return normalizedFeatureInfos;
|
|
}
|
|
}
|
|
|
|
const rawDeviceIds =
|
|
params.device_ids ??
|
|
params.deviceId ??
|
|
params.device_id ??
|
|
params.id ??
|
|
params.ids;
|
|
const deviceIds = Array.isArray(rawDeviceIds)
|
|
? rawDeviceIds.map((id) => String(id))
|
|
: typeof rawDeviceIds === "string"
|
|
? rawDeviceIds
|
|
.split(",")
|
|
.map((id) => id.trim())
|
|
.filter(Boolean)
|
|
: [];
|
|
|
|
return deviceIds.map((id) => [id, "scada"]);
|
|
};
|
|
|
|
const resolveTimeRange = (params: Record<string, unknown>) => ({
|
|
startTime:
|
|
(params.start_time as string | undefined) ??
|
|
(params.startTime as string | undefined) ??
|
|
(params.from as string | undefined) ??
|
|
(params.start as string | undefined),
|
|
endTime:
|
|
(params.end_time as string | undefined) ??
|
|
(params.endTime as string | undefined) ??
|
|
(params.to as string | undefined) ??
|
|
(params.end as string | undefined),
|
|
});
|
|
|
|
const compactNames = (names: string[]) => {
|
|
if (!names.length) return "";
|
|
return names.length > 3
|
|
? `${names.slice(0, 3).join(", ")} 等 ${names.length} 个`
|
|
: names.join(", ");
|
|
};
|
|
|
|
const buildLocateArtifact = (
|
|
tool: string,
|
|
params: Record<string, unknown>,
|
|
): { artifact: Omit<AgentArtifact, "id" | "params" | "tool">; action: ChatToolAction | null } => {
|
|
const ids = normalizeIds(params);
|
|
const rawType = params.feature_type;
|
|
const featureType =
|
|
typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
|
|
const config = tool === "locate_features"
|
|
? FEATURE_TYPE_MAP[featureType]
|
|
: LOCATE_TOOL_CONFIG[tool];
|
|
|
|
return {
|
|
artifact: {
|
|
kind: "map",
|
|
title: config ? `地图定位${config.label}` : "地图定位",
|
|
description: compactNames(ids),
|
|
},
|
|
action: config
|
|
? {
|
|
type: "locate_features",
|
|
ids,
|
|
layer: config.layer,
|
|
geometryKind: config.geometryKind,
|
|
}
|
|
: null,
|
|
};
|
|
};
|
|
|
|
const buildToolAction = (
|
|
tool: string,
|
|
params: Record<string, unknown>,
|
|
): { action: ChatToolAction | null; kind: AgentArtifactKind; title: string; description?: string } => {
|
|
if (tool === "show_chart") {
|
|
return {
|
|
action: null,
|
|
kind: "chart",
|
|
title: (params.title as string | undefined) ?? "生成图表",
|
|
description: "已生成可视化图表",
|
|
};
|
|
}
|
|
|
|
if (tool === "locate_features" || LOCATE_TOOL_CONFIG[tool]) {
|
|
const locate = buildLocateArtifact(tool, params);
|
|
return {
|
|
action: locate.action,
|
|
kind: locate.artifact.kind,
|
|
title: locate.artifact.title,
|
|
description: locate.artifact.description,
|
|
};
|
|
}
|
|
|
|
if (tool === "view_history") {
|
|
const featureInfos = (params.feature_infos as [string, string][] | undefined) ?? [];
|
|
const { startTime, endTime } = resolveTimeRange(params);
|
|
return {
|
|
action: {
|
|
type: "view_history",
|
|
featureInfos,
|
|
dataType:
|
|
(params.data_type as "realtime" | "scheme" | "none" | undefined) ??
|
|
"realtime",
|
|
startTime,
|
|
endTime,
|
|
},
|
|
kind: "panel",
|
|
title: "打开计算结果曲线",
|
|
description: compactNames(featureInfos.map(([id]) => id)),
|
|
};
|
|
}
|
|
|
|
if (tool === "view_scada") {
|
|
const featureInfos = resolveScadaFeatureInfos(params);
|
|
const { startTime, endTime } = resolveTimeRange(params);
|
|
return {
|
|
action: {
|
|
type: "view_scada",
|
|
featureInfos,
|
|
startTime,
|
|
endTime,
|
|
},
|
|
kind: "panel",
|
|
title: "打开 SCADA 数据面板",
|
|
description: compactNames(featureInfos.map(([id]) => id)),
|
|
};
|
|
}
|
|
|
|
if (tool === "render_junctions") {
|
|
const renderRef =
|
|
typeof params.render_ref === "string" ? params.render_ref.trim() : "";
|
|
|
|
return {
|
|
action: renderRef
|
|
? {
|
|
type: "render_junctions",
|
|
renderRef,
|
|
sessionId: undefined,
|
|
}
|
|
: null,
|
|
kind: "map",
|
|
title: "渲染节点分区",
|
|
description: renderRef || "渲染引用",
|
|
};
|
|
}
|
|
|
|
if (tool === APPLY_LAYER_STYLE_TOOL) {
|
|
const payload = parseApplyLayerStylePayload(params);
|
|
return {
|
|
action: payload
|
|
? {
|
|
type: "apply_layer_style",
|
|
layerId: payload.layerId,
|
|
resetToDefault: payload.resetToDefault,
|
|
styleConfig: payload.styleConfig,
|
|
}
|
|
: null,
|
|
kind: "map",
|
|
title: payload?.resetToDefault ? "重置图层样式" : "应用图层样式",
|
|
description: payload ? describeApplyLayerStyle(payload) : "图层样式",
|
|
};
|
|
}
|
|
|
|
return {
|
|
action: null,
|
|
kind: "tool",
|
|
title: tool || "工具调用",
|
|
description: "Agent 已执行工具动作",
|
|
};
|
|
};
|
|
|
|
export const useAgentToolActions = () => {
|
|
const dispatchToolAction = useChatToolStore((s) => s.dispatch);
|
|
|
|
return useCallback(
|
|
(event: ToolCallEvent, options: HandleToolCallOptions) => {
|
|
const { action, kind, title, description } = buildToolAction(
|
|
event.tool,
|
|
event.params,
|
|
);
|
|
|
|
const normalizedAction =
|
|
action?.type === "render_junctions"
|
|
? { ...action, sessionId: event.sessionId }
|
|
: action;
|
|
|
|
options.appendArtifact(options.assistantMessageId, {
|
|
id: `${event.tool}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
tool: event.tool,
|
|
kind,
|
|
title,
|
|
description,
|
|
params: event.params,
|
|
});
|
|
|
|
if (normalizedAction) {
|
|
dispatchToolAction(normalizedAction);
|
|
}
|
|
},
|
|
[dispatchToolAction],
|
|
);
|
|
};
|