Files
TJWaterFrontend_Refine/src/components/chat/hooks/useAgentToolActions.ts
T
jiang 9761ade8d8
Build Push and Deploy / docker-image (push) Successful in 1m30s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped
新增应用样式 agent 工具
2026-05-29 10:27:27 +08:00

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],
);
};