抽象统一定位方法,支持多种地理要素
This commit is contained in:
@@ -34,8 +34,26 @@ type ToolMeta = {
|
||||
color: string;
|
||||
};
|
||||
|
||||
const LOCATE_TOOL_TO_LAYER: Record<string, string> = {
|
||||
locate_features: "",
|
||||
locate_junctions: "geo_junctions_mat",
|
||||
locate_pipes: "geo_pipes_mat",
|
||||
locate_valves: "geo_valves",
|
||||
locate_reservoirs: "geo_reservoirs",
|
||||
locate_pumps: "geo_pumps",
|
||||
locate_tanks: "geo_tanks",
|
||||
};
|
||||
|
||||
const LOCATE_LINE_TOOLS = new Set<string>(["locate_pipes"]);
|
||||
|
||||
const TOOL_META: Record<string, ToolMeta> = {
|
||||
locate_nodes: {
|
||||
locate_features: {
|
||||
label: "定位要素",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#5470c6",
|
||||
},
|
||||
locate_junctions: {
|
||||
label: "定位节点",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
@@ -47,6 +65,30 @@ const TOOL_META: Record<string, ToolMeta> = {
|
||||
actionLabel: "定位到地图",
|
||||
color: "#91cc75",
|
||||
},
|
||||
locate_valves: {
|
||||
label: "定位阀门",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#9a60b4",
|
||||
},
|
||||
locate_reservoirs: {
|
||||
label: "定位水源",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#ea7ccc",
|
||||
},
|
||||
locate_pumps: {
|
||||
label: "定位泵站",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#fc8452",
|
||||
},
|
||||
locate_tanks: {
|
||||
label: "定位水池",
|
||||
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||
actionLabel: "定位到地图",
|
||||
color: "#3ba272",
|
||||
},
|
||||
view_history: {
|
||||
label: "查看计算结果",
|
||||
icon: <TimelineRounded sx={{ fontSize: 18 }} />,
|
||||
@@ -71,6 +113,19 @@ const TOOL_META: Record<string, ToolMeta> = {
|
||||
|
||||
function getToolDescription(toolCall: ToolCall): string {
|
||||
const { params } = toolCall;
|
||||
const normalizeIds = (): string[] => {
|
||||
const rawIds = params.ids;
|
||||
if (Array.isArray(rawIds)) {
|
||||
return rawIds.map((id) => String(id)).filter((id) => id.trim().length > 0);
|
||||
}
|
||||
if (typeof rawIds === "string") {
|
||||
return rawIds
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||
const rawFeatureInfos = params.feature_infos;
|
||||
if (Array.isArray(rawFeatureInfos)) {
|
||||
@@ -119,13 +174,36 @@ function getToolDescription(toolCall: ToolCall): string {
|
||||
(params.to as string | undefined) ??
|
||||
(params.end as string | undefined),
|
||||
});
|
||||
const resolveLocateFeatureType = (): string => {
|
||||
const rawType = params.feature_type;
|
||||
if (typeof rawType === "string" && rawType.trim()) {
|
||||
return rawType.trim().toLowerCase();
|
||||
}
|
||||
return "";
|
||||
};
|
||||
switch (toolCall.tool) {
|
||||
case "locate_nodes":
|
||||
case "locate_pipes": {
|
||||
const ids = (params.ids as string[] | undefined) ?? [];
|
||||
return ids.length > 3
|
||||
case "locate_features":
|
||||
case "locate_junctions":
|
||||
case "locate_pipes":
|
||||
case "locate_valves":
|
||||
case "locate_reservoirs":
|
||||
case "locate_pumps":
|
||||
case "locate_tanks": {
|
||||
const ids = normalizeIds();
|
||||
const idsText =
|
||||
ids.length > 3
|
||||
? `${ids.slice(0, 3).join(", ")} 等 ${ids.length} 个`
|
||||
: ids.join(", ");
|
||||
if (toolCall.tool !== "locate_features") {
|
||||
return idsText;
|
||||
}
|
||||
const featureType = resolveLocateFeatureType();
|
||||
if (!featureType) {
|
||||
return idsText;
|
||||
}
|
||||
return idsText
|
||||
? `${featureType} · ${idsText}`
|
||||
: featureType;
|
||||
}
|
||||
case "view_history":
|
||||
case "view_scada": {
|
||||
@@ -155,6 +233,19 @@ function getToolDescription(toolCall: ToolCall): string {
|
||||
|
||||
function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
||||
const { params } = toolCall;
|
||||
const normalizeIds = (): string[] => {
|
||||
const rawIds = params.ids;
|
||||
if (Array.isArray(rawIds)) {
|
||||
return rawIds.map((id) => String(id)).filter((id) => id.trim().length > 0);
|
||||
}
|
||||
if (typeof rawIds === "string") {
|
||||
return rawIds
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const resolveScadaFeatureInfos = (): [string, string][] => {
|
||||
const rawFeatureInfos = params.feature_infos;
|
||||
if (Array.isArray(rawFeatureInfos)) {
|
||||
@@ -204,16 +295,36 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
||||
(params.end as string | undefined),
|
||||
});
|
||||
switch (toolCall.tool) {
|
||||
case "locate_nodes":
|
||||
case "locate_features": {
|
||||
const featureTypeRaw = params.feature_type;
|
||||
const featureType =
|
||||
typeof featureTypeRaw === "string"
|
||||
? featureTypeRaw.trim().toLowerCase()
|
||||
: "";
|
||||
const config = locateFeatureTypeToConfig(featureType);
|
||||
if (!config) return null;
|
||||
return {
|
||||
type: "locate_nodes",
|
||||
ids: (params.ids as string[] | undefined) ?? [],
|
||||
type: "locate_features",
|
||||
ids: normalizeIds(),
|
||||
layer: config.layer,
|
||||
geometryKind: config.geometryKind,
|
||||
};
|
||||
}
|
||||
case "locate_junctions":
|
||||
case "locate_pipes":
|
||||
case "locate_valves":
|
||||
case "locate_reservoirs":
|
||||
case "locate_pumps":
|
||||
case "locate_tanks": {
|
||||
const layer = LOCATE_TOOL_TO_LAYER[toolCall.tool];
|
||||
if (!layer) return null;
|
||||
return {
|
||||
type: "locate_pipes",
|
||||
ids: (params.ids as string[] | undefined) ?? [],
|
||||
type: "locate_features",
|
||||
ids: normalizeIds(),
|
||||
layer,
|
||||
geometryKind: LOCATE_LINE_TOOLS.has(toolCall.tool) ? "line" : "point",
|
||||
};
|
||||
}
|
||||
case "view_history": {
|
||||
const historyRange = resolveTimeRange();
|
||||
return {
|
||||
@@ -383,3 +494,29 @@ export const ChatToolCallBlock: React.FC<ChatToolCallBlockProps> = ({
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
const locateFeatureTypeToConfig = (
|
||||
featureType: string,
|
||||
): { layer: string; geometryKind: "point" | "line" } | null => {
|
||||
switch (featureType) {
|
||||
case "junction":
|
||||
case "junctions":
|
||||
return { layer: "geo_junctions_mat", geometryKind: "point" };
|
||||
case "pipe":
|
||||
case "pipes":
|
||||
return { layer: "geo_pipes_mat", geometryKind: "line" };
|
||||
case "valve":
|
||||
case "valves":
|
||||
return { layer: "geo_valves", geometryKind: "point" };
|
||||
case "reservoir":
|
||||
case "reservoirs":
|
||||
return { layer: "geo_reservoirs", geometryKind: "point" };
|
||||
case "pump":
|
||||
case "pumps":
|
||||
return { layer: "geo_pumps", geometryKind: "point" };
|
||||
case "tank":
|
||||
case "tanks":
|
||||
return { layer: "geo_tanks", geometryKind: "point" };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -789,15 +789,50 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
}
|
||||
|
||||
// Other frontend tools → dispatch to chatToolStore immediately
|
||||
const buildLocateFeaturesAction = (
|
||||
layer: string,
|
||||
geometryKind: "point" | "line",
|
||||
): ChatToolAction => ({
|
||||
type: "locate_features" as const,
|
||||
ids: (params.ids as string[]) ?? [],
|
||||
layer,
|
||||
geometryKind,
|
||||
});
|
||||
const buildLocateByFeatureType = (): ChatToolAction | null => {
|
||||
const rawType = params.feature_type;
|
||||
const featureType =
|
||||
typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
|
||||
const featureTypeMap: Record<
|
||||
string,
|
||||
{ layer: string; geometryKind: "point" | "line" }
|
||||
> = {
|
||||
junction: { layer: "geo_junctions_mat", geometryKind: "point" },
|
||||
junctions: { layer: "geo_junctions_mat", geometryKind: "point" },
|
||||
pipe: { layer: "geo_pipes_mat", geometryKind: "line" },
|
||||
pipes: { layer: "geo_pipes_mat", geometryKind: "line" },
|
||||
valve: { layer: "geo_valves", geometryKind: "point" },
|
||||
valves: { layer: "geo_valves", geometryKind: "point" },
|
||||
reservoir: { layer: "geo_reservoirs", geometryKind: "point" },
|
||||
reservoirs: { layer: "geo_reservoirs", geometryKind: "point" },
|
||||
pump: { layer: "geo_pumps", geometryKind: "point" },
|
||||
pumps: { layer: "geo_pumps", geometryKind: "point" },
|
||||
tank: { layer: "geo_tanks", geometryKind: "point" },
|
||||
tanks: { layer: "geo_tanks", geometryKind: "point" },
|
||||
};
|
||||
const config = featureTypeMap[featureType];
|
||||
if (!config) return null;
|
||||
return buildLocateFeaturesAction(config.layer, config.geometryKind);
|
||||
};
|
||||
const actionMap: Record<string, () => ChatToolAction | null> = {
|
||||
locate_nodes: () => ({
|
||||
type: "locate_nodes" as const,
|
||||
ids: (params.ids as string[]) ?? [],
|
||||
}),
|
||||
locate_pipes: () => ({
|
||||
type: "locate_pipes" as const,
|
||||
ids: (params.ids as string[]) ?? [],
|
||||
}),
|
||||
locate_features: buildLocateByFeatureType,
|
||||
locate_pipes: () => buildLocateFeaturesAction("geo_pipes_mat", "line"),
|
||||
locate_junctions: () =>
|
||||
buildLocateFeaturesAction("geo_junctions_mat", "point"),
|
||||
locate_valves: () => buildLocateFeaturesAction("geo_valves", "point"),
|
||||
locate_reservoirs: () =>
|
||||
buildLocateFeaturesAction("geo_reservoirs", "point"),
|
||||
locate_pumps: () => buildLocateFeaturesAction("geo_pumps", "point"),
|
||||
locate_tanks: () => buildLocateFeaturesAction("geo_tanks", "point"),
|
||||
view_history: () => ({
|
||||
type: "view_history" as const,
|
||||
featureInfos: (params.feature_infos as [string, string][]) ?? [],
|
||||
@@ -837,6 +872,17 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
||||
);
|
||||
} else if (event.type === "done") {
|
||||
if (!conversationId && event.conversationId) setConversationId(event.conversationId);
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId && m.content.trim().length === 0
|
||||
? {
|
||||
...m,
|
||||
content: "⚠️ **错误:** Copilot 未返回内容,请稍后重试。",
|
||||
isError: true,
|
||||
}
|
||||
: m
|
||||
)
|
||||
);
|
||||
setIsStreaming(false);
|
||||
} else if (event.type === "error") {
|
||||
setMessages((prev) =>
|
||||
|
||||
@@ -56,11 +56,11 @@ describe("parseContentWithToolCalls", () => {
|
||||
|
||||
it("parses a complete tool_call block", () => {
|
||||
const content =
|
||||
'分析完成。\n<tool_call>{"tool":"locate_nodes","params":{"ids":["J1","J2"]}}</tool_call>\n以上是结果。';
|
||||
'分析完成。\n<tool_call>{"tool":"locate_junctions","params":{"ids":["J1","J2"]}}</tool_call>\n以上是结果。';
|
||||
const result = parseContentWithToolCalls(content);
|
||||
|
||||
expect(result.toolCalls).toHaveLength(1);
|
||||
expect(result.toolCalls[0].tool).toBe("locate_nodes");
|
||||
expect(result.toolCalls[0].tool).toBe("locate_junctions");
|
||||
expect(result.toolCalls[0].params).toEqual({ ids: ["J1", "J2"] });
|
||||
|
||||
expect(result.segments).toHaveLength(3);
|
||||
@@ -70,7 +70,7 @@ describe("parseContentWithToolCalls", () => {
|
||||
});
|
||||
expect(result.segments[1]).toMatchObject({
|
||||
type: "tool_call",
|
||||
toolCall: { tool: "locate_nodes" },
|
||||
toolCall: { tool: "locate_junctions" },
|
||||
});
|
||||
expect(result.segments[2]).toEqual({
|
||||
type: "text",
|
||||
|
||||
@@ -15,6 +15,7 @@ import VectorLayer from "ol/layer/Vector";
|
||||
import { Style, Stroke, Fill, Circle } from "ol/style";
|
||||
import Feature from "ol/Feature";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import Point from "ol/geom/Point";
|
||||
import { bbox, featureCollection } from "@turf/turf";
|
||||
import StyleEditorPanel from "./StyleEditorPanel";
|
||||
import { LayerStyleState } from "./StyleEditorPanel";
|
||||
@@ -72,38 +73,52 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
useCallback(
|
||||
(action) => {
|
||||
const geojsonFormat = new GeoJSON();
|
||||
const zoomToFeatures = (features: Feature[]) => {
|
||||
const zoomToFeatures = (
|
||||
features: Feature[],
|
||||
geometryKind: "point" | "line",
|
||||
) => {
|
||||
if (features.length === 0) return;
|
||||
|
||||
if (geometryKind === "point" && features.length === 1) {
|
||||
const geometry = features[0].getGeometry();
|
||||
if (geometry instanceof Point) {
|
||||
map?.getView().animate({
|
||||
center: geometry.getCoordinates(),
|
||||
zoom: 18,
|
||||
duration: 1000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const geojsonFeatures = features.map((f) =>
|
||||
geojsonFormat.writeFeatureObject(f),
|
||||
);
|
||||
const extent = bbox(featureCollection(geojsonFeatures as any));
|
||||
if (extent) {
|
||||
map?.getView().fit(extent, { maxZoom: 18, duration: 1000 });
|
||||
map?.getView().fit(extent, {
|
||||
maxZoom: 18,
|
||||
duration: 1000,
|
||||
padding: geometryKind === "line" ? [60, 60, 60, 60] : [40, 40, 40, 40],
|
||||
});
|
||||
}
|
||||
};
|
||||
const locateFeatures = (
|
||||
ids: string[],
|
||||
layer: string,
|
||||
geometryKind: "point" | "line",
|
||||
) => {
|
||||
queryFeaturesByIds(ids, layer).then((features) => {
|
||||
if (features.length > 0) {
|
||||
setHighlightFeatures(features);
|
||||
zoomToFeatures(features, geometryKind);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
switch (action.type) {
|
||||
case "locate_nodes": {
|
||||
queryFeaturesByIds(action.ids, "geo_junctions_mat").then(
|
||||
(features) => {
|
||||
if (features.length > 0) {
|
||||
setHighlightFeatures(features);
|
||||
zoomToFeatures(features);
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "locate_pipes": {
|
||||
queryFeaturesByIds(action.ids, "geo_pipes_mat").then(
|
||||
(features) => {
|
||||
if (features.length > 0) {
|
||||
setHighlightFeatures(features);
|
||||
zoomToFeatures(features);
|
||||
}
|
||||
},
|
||||
);
|
||||
case "locate_features": {
|
||||
locateFeatures(action.ids, action.layer, action.geometryKind);
|
||||
break;
|
||||
}
|
||||
case "view_history": {
|
||||
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
* ```ts
|
||||
* useChatToolActionHandler((action) => {
|
||||
* switch (action.type) {
|
||||
* case "locate_nodes": handleLocateNodes(action.ids); break;
|
||||
* case "locate_pipes": handleLocatePipes(action.ids); break;
|
||||
* case "locate_features": handleLocateFeatures(action.ids, action.layer, action.geometryKind); break;
|
||||
* case "view_history": openHistoryPanel(action.featureInfos, action.dataType); break;
|
||||
* case "view_scada": openScadaPanel(action.featureInfos); break;
|
||||
* }
|
||||
@@ -23,7 +22,10 @@ export function useChatToolActionHandler(
|
||||
handler: (action: ChatToolAction) => void,
|
||||
) {
|
||||
const handlerRef = useRef(handler);
|
||||
handlerRef.current = handler;
|
||||
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = useChatToolStore.subscribe(
|
||||
|
||||
@@ -7,8 +7,12 @@ import { create } from "zustand";
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export type ChatToolAction =
|
||||
| { type: "locate_nodes"; ids: string[] }
|
||||
| { type: "locate_pipes"; ids: string[] }
|
||||
| {
|
||||
type: "locate_features";
|
||||
ids: string[];
|
||||
layer: string;
|
||||
geometryKind: "point" | "line";
|
||||
}
|
||||
| {
|
||||
type: "view_history";
|
||||
featureInfos: [string, string][];
|
||||
|
||||
Reference in New Issue
Block a user