|
|
@@ -0,0 +1,583 @@
|
|
|
+import { Annotation, END, START, StateGraph } from "@langchain/langgraph";
|
|
|
+import type { RuntimeConfig } from "./config.js";
|
|
|
+import { McpClient } from "./mcpClient.js";
|
|
|
+import type {
|
|
|
+ AgentResultTable,
|
|
|
+ AgentRunRequest,
|
|
|
+ AgentRunResponse,
|
|
|
+ AgentTraceEvent,
|
|
|
+ ChatMessage,
|
|
|
+ McpToolDescriptor,
|
|
|
+ PlannedToolCall,
|
|
|
+ ToolObservation
|
|
|
+} from "./types.js";
|
|
|
+
|
|
|
+const AgentState = Annotation.Root({
|
|
|
+ message: Annotation<string>({
|
|
|
+ reducer: (_current, next) => next ?? "",
|
|
|
+ default: () => ""
|
|
|
+ }),
|
|
|
+ history: Annotation<ChatMessage[]>({
|
|
|
+ reducer: (_current, next) => next ?? [],
|
|
|
+ default: () => []
|
|
|
+ }),
|
|
|
+ tools: Annotation<McpToolDescriptor[]>({
|
|
|
+ reducer: (_current, next) => next ?? [],
|
|
|
+ default: () => []
|
|
|
+ }),
|
|
|
+ plannedToolCalls: Annotation<PlannedToolCall[]>({
|
|
|
+ reducer: (_current, next) => next ?? [],
|
|
|
+ default: () => []
|
|
|
+ }),
|
|
|
+ observations: Annotation<ToolObservation[]>({
|
|
|
+ reducer: (current, next) => [...(current ?? []), ...(next ?? [])],
|
|
|
+ default: () => []
|
|
|
+ }),
|
|
|
+ visitedSignatures: Annotation<string[]>({
|
|
|
+ reducer: (_current, next) => next ?? [],
|
|
|
+ default: () => []
|
|
|
+ }),
|
|
|
+ trace: Annotation<AgentTraceEvent[]>({
|
|
|
+ reducer: (current, next) => [...(current ?? []), ...(next ?? [])],
|
|
|
+ default: () => []
|
|
|
+ }),
|
|
|
+ steps: Annotation<number>({
|
|
|
+ reducer: (_current, next) => next ?? 0,
|
|
|
+ default: () => 0
|
|
|
+ }),
|
|
|
+ final: Annotation<AgentRunResponse | undefined>({
|
|
|
+ reducer: (_current, next) => next,
|
|
|
+ default: () => undefined
|
|
|
+ })
|
|
|
+});
|
|
|
+
|
|
|
+type AgentStateType = typeof AgentState.State;
|
|
|
+
|
|
|
+export class SmqjhAgentGraph {
|
|
|
+ private readonly mcp: McpClient;
|
|
|
+ private readonly graph: {
|
|
|
+ invoke(input: Partial<AgentStateType>): Promise<AgentStateType>;
|
|
|
+ };
|
|
|
+
|
|
|
+ constructor(private readonly config: RuntimeConfig) {
|
|
|
+ this.mcp = new McpClient(config.mcp.url, config.mcp.token);
|
|
|
+ this.graph = this.buildGraph();
|
|
|
+ }
|
|
|
+
|
|
|
+ async health(): Promise<unknown> {
|
|
|
+ return this.mcp.status();
|
|
|
+ }
|
|
|
+
|
|
|
+ async tools(): Promise<McpToolDescriptor[]> {
|
|
|
+ return this.loadRunnableTools();
|
|
|
+ }
|
|
|
+
|
|
|
+ async run(input: AgentRunRequest): Promise<AgentRunResponse> {
|
|
|
+ const result = await this.graph.invoke({
|
|
|
+ message: input.message,
|
|
|
+ history: input.history ?? [],
|
|
|
+ visitedSignatures: [],
|
|
|
+ trace: [trace("system", "收到用户请求", input.message)]
|
|
|
+ });
|
|
|
+
|
|
|
+ const final = result.final as AgentRunResponse | undefined;
|
|
|
+ const observations = asArray<ToolObservation>(result.observations);
|
|
|
+ const traceEvents = asArray<AgentTraceEvent>(result.trace);
|
|
|
+ return (
|
|
|
+ final ?? {
|
|
|
+ content: "这次没有生成有效回答,请稍后重试。",
|
|
|
+ model: "none",
|
|
|
+ usedMcp: false,
|
|
|
+ steps: typeof result.steps === "number" ? result.steps : 0,
|
|
|
+ toolCalls: observations,
|
|
|
+ tables: extractTables(observations),
|
|
|
+ trace: traceEvents
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildGraph(): { invoke(input: Partial<AgentStateType>): Promise<AgentStateType> } {
|
|
|
+ const workflow = new StateGraph(AgentState)
|
|
|
+ .addNode("loadTools", this.loadToolsNode)
|
|
|
+ .addNode("plan", this.planNode)
|
|
|
+ .addNode("executeTools", this.executeToolsNode)
|
|
|
+ .addNode("summarize", this.summarizeNode)
|
|
|
+ .addNode("chat", this.chatNode)
|
|
|
+ .addEdge(START, "loadTools")
|
|
|
+ .addEdge("loadTools", "plan")
|
|
|
+ .addConditionalEdges("plan", this.routeAfterPlan, {
|
|
|
+ executeTools: "executeTools",
|
|
|
+ summarize: "summarize",
|
|
|
+ chat: "chat"
|
|
|
+ })
|
|
|
+ .addEdge("executeTools", "plan")
|
|
|
+ .addEdge("summarize", END)
|
|
|
+ .addEdge("chat", END);
|
|
|
+
|
|
|
+ return workflow.compile() as unknown as { invoke(input: Partial<AgentStateType>): Promise<AgentStateType> };
|
|
|
+ }
|
|
|
+
|
|
|
+ private loadToolsNode = async (): Promise<Partial<AgentStateType>> => {
|
|
|
+ try {
|
|
|
+ const tools = await this.loadRunnableTools();
|
|
|
+ return {
|
|
|
+ tools,
|
|
|
+ trace: [trace("system", "MCP 工具加载完成", `可用工具 ${tools.length} 个`)]
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ tools: [],
|
|
|
+ trace: [trace("error", "MCP 工具加载失败", errorMessage(error))]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private planNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
|
|
|
+ if (state.steps >= this.config.maxSteps) {
|
|
|
+ return {
|
|
|
+ plannedToolCalls: [],
|
|
|
+ trace: [trace("plan", "达到最大工具步数", `maxSteps=${this.config.maxSteps}`)]
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await this.mcp.callTool("smqjh.ai.tool.plan", {
|
|
|
+ request: this.buildAssistantRequest(state),
|
|
|
+ tools: state.tools,
|
|
|
+ observations: state.observations
|
|
|
+ });
|
|
|
+ const record = asRecord(result.structuredContent);
|
|
|
+ const rawCalls = Array.isArray(record.toolCalls) ? record.toolCalls : [];
|
|
|
+ const visited = new Set(state.visitedSignatures);
|
|
|
+ const plannedToolCalls = rawCalls
|
|
|
+ .map((item) => normalizeToolCall(item, state.message))
|
|
|
+ .filter((item): item is PlannedToolCall => Boolean(item))
|
|
|
+ .filter((item) => !visited.has(toolSignature(item.name, item.arguments)))
|
|
|
+ .slice(0, 3);
|
|
|
+
|
|
|
+ return {
|
|
|
+ plannedToolCalls,
|
|
|
+ trace: [
|
|
|
+ trace(
|
|
|
+ "plan",
|
|
|
+ plannedToolCalls.length ? "DeepSeek 已规划工具调用" : "DeepSeek 判断无需继续调用工具",
|
|
|
+ reasonText(record),
|
|
|
+ { toolCalls: plannedToolCalls }
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ plannedToolCalls: [],
|
|
|
+ trace: [trace("error", "DeepSeek 工具规划失败", errorMessage(error))]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private executeToolsNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
|
|
|
+ const byName = new Map(state.tools.map((tool) => [tool.name, tool]));
|
|
|
+ const observations: ToolObservation[] = [];
|
|
|
+ const visited = new Set(state.visitedSignatures);
|
|
|
+
|
|
|
+ for (const call of state.plannedToolCalls) {
|
|
|
+ const resolvedName = resolveToolName(call.name, byName);
|
|
|
+ const signature = toolSignature(resolvedName || call.name, call.arguments);
|
|
|
+ visited.add(signature);
|
|
|
+
|
|
|
+ if (!resolvedName) {
|
|
|
+ observations.push({
|
|
|
+ name: call.name,
|
|
|
+ source: "mcp",
|
|
|
+ arguments: call.arguments,
|
|
|
+ ok: false,
|
|
|
+ error: "DeepSeek 选择了未注册 MCP 工具"
|
|
|
+ });
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const startedAt = Date.now();
|
|
|
+ try {
|
|
|
+ const result = await this.mcp.callTool(resolvedName, call.arguments);
|
|
|
+ observations.push({
|
|
|
+ name: resolvedName,
|
|
|
+ source: "mcp",
|
|
|
+ arguments: call.arguments,
|
|
|
+ ok: true,
|
|
|
+ result: result.structuredContent ?? result,
|
|
|
+ durationMs: Date.now() - startedAt
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ observations.push({
|
|
|
+ name: resolvedName,
|
|
|
+ source: "mcp",
|
|
|
+ arguments: call.arguments,
|
|
|
+ ok: false,
|
|
|
+ error: errorMessage(error),
|
|
|
+ durationMs: Date.now() - startedAt
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ observations,
|
|
|
+ plannedToolCalls: [],
|
|
|
+ visitedSignatures: Array.from(visited),
|
|
|
+ steps: state.steps + 1,
|
|
|
+ trace: observations.map((item) =>
|
|
|
+ trace(
|
|
|
+ item.ok ? "tool" : "error",
|
|
|
+ item.ok ? `工具执行完成:${item.name}` : `工具执行失败:${item.name}`,
|
|
|
+ item.ok ? `${item.durationMs ?? 0}ms` : item.error,
|
|
|
+ { arguments: item.arguments, result: summarizeResult(item.result) }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ private summarizeNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
|
|
|
+ const tables = extractTables(state.observations);
|
|
|
+ try {
|
|
|
+ const result = await this.mcp.callTool("smqjh.ai.tool.summarize", {
|
|
|
+ request: this.buildAssistantRequest(state),
|
|
|
+ observations: state.observations
|
|
|
+ });
|
|
|
+ const record = asRecord(result.structuredContent);
|
|
|
+ const content = typeof record.content === "string" && record.content.trim()
|
|
|
+ ? record.content.trim()
|
|
|
+ : fallbackSummary(state.observations, tables);
|
|
|
+ const model = typeof record.model === "string" && record.model ? record.model : "mcp-deepseek";
|
|
|
+
|
|
|
+ return {
|
|
|
+ final: {
|
|
|
+ content,
|
|
|
+ model,
|
|
|
+ usedMcp: true,
|
|
|
+ steps: state.steps,
|
|
|
+ toolCalls: state.observations,
|
|
|
+ tables,
|
|
|
+ trace: state.trace
|
|
|
+ },
|
|
|
+ trace: [trace("summary", "结果总结完成", `${tables.length} 个表格`)]
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ const content = fallbackSummary(state.observations, tables);
|
|
|
+ return {
|
|
|
+ final: {
|
|
|
+ content,
|
|
|
+ model: "tool-only",
|
|
|
+ usedMcp: true,
|
|
|
+ steps: state.steps,
|
|
|
+ toolCalls: state.observations,
|
|
|
+ tables,
|
|
|
+ trace: state.trace
|
|
|
+ },
|
|
|
+ trace: [trace("error", "结果总结失败,已使用工具结果兜底", errorMessage(error))]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private chatNode = async (state: AgentStateType): Promise<Partial<AgentStateType>> => {
|
|
|
+ try {
|
|
|
+ const result = await this.mcp.callTool("smqjh.ai.chat", {
|
|
|
+ request: this.buildAssistantRequest(state)
|
|
|
+ });
|
|
|
+ const record = asRecord(result.structuredContent);
|
|
|
+ const content = typeof record.content === "string" && record.content.trim()
|
|
|
+ ? record.content.trim()
|
|
|
+ : "DeepSeek 没有返回有效内容。";
|
|
|
+ const model = typeof record.model === "string" && record.model ? record.model : "mcp-deepseek";
|
|
|
+
|
|
|
+ return {
|
|
|
+ final: {
|
|
|
+ content,
|
|
|
+ model,
|
|
|
+ usedMcp: true,
|
|
|
+ steps: state.steps,
|
|
|
+ toolCalls: [],
|
|
|
+ tables: [],
|
|
|
+ trace: state.trace
|
|
|
+ },
|
|
|
+ trace: [trace("chat", "普通对话完成", model)]
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ final: {
|
|
|
+ content: `暂时无法完成回答:${errorMessage(error)}`,
|
|
|
+ model: "none",
|
|
|
+ usedMcp: false,
|
|
|
+ steps: state.steps,
|
|
|
+ toolCalls: [],
|
|
|
+ tables: [],
|
|
|
+ trace: state.trace
|
|
|
+ },
|
|
|
+ trace: [trace("error", "普通对话失败", errorMessage(error))]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ private routeAfterPlan = (state: AgentStateType): "executeTools" | "summarize" | "chat" => {
|
|
|
+ if (state.plannedToolCalls.length > 0 && state.steps < this.config.maxSteps) {
|
|
|
+ return "executeTools";
|
|
|
+ }
|
|
|
+ if (state.observations.length > 0) {
|
|
|
+ return "summarize";
|
|
|
+ }
|
|
|
+ return "chat";
|
|
|
+ };
|
|
|
+
|
|
|
+ private buildAssistantRequest(state: AgentStateType): Record<string, unknown> {
|
|
|
+ return {
|
|
|
+ message: state.message,
|
|
|
+ history: state.history,
|
|
|
+ environmentName: this.config.environmentName,
|
|
|
+ baseUrl: this.config.baseUrl,
|
|
|
+ authenticated: true,
|
|
|
+ username: "web-agent"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async loadRunnableTools(): Promise<McpToolDescriptor[]> {
|
|
|
+ const tools = await this.mcp.listTools();
|
|
|
+ return tools.filter((tool) => !tool.name.startsWith("smqjh.ai."));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeToolCall(value: unknown, message: string): PlannedToolCall | undefined {
|
|
|
+ const record = asRecord(value);
|
|
|
+ const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
|
+ if (!name) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ name,
|
|
|
+ arguments: repairToolArguments(name, asRecord(record.arguments), message)
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function repairToolArguments(name: string, args: Record<string, unknown>, message: string): Record<string, unknown> {
|
|
|
+ if (name === "smqjh.database.smart.query" && typeof args.question !== "string") {
|
|
|
+ return { ...args, question: message };
|
|
|
+ }
|
|
|
+ if (name === "smqjh.product.lookup.summary") {
|
|
|
+ const current = typeof args.productKeyword === "string" ? args.productKeyword.trim() : "";
|
|
|
+ return { ...args, productKeyword: current || extractProductKeyword(message) };
|
|
|
+ }
|
|
|
+ return args;
|
|
|
+}
|
|
|
+
|
|
|
+function extractProductKeyword(message: string): string {
|
|
|
+ return message
|
|
|
+ .replace(/[,。!??;;::]/g, " ")
|
|
|
+ .replace(/帮我|麻烦|查询一下|查一下|查询|查看|当前|业务系统|系统里面|系统里|后台|我方|我们的|商品库|商品表/g, " ")
|
|
|
+ .replace(/商品描述是什么|商品描述|描述是什么|描述|价格是多少|价格|定价|是多少|是什么|呢/g, " ")
|
|
|
+ .replace(/\s+/g, " ")
|
|
|
+ .trim();
|
|
|
+}
|
|
|
+
|
|
|
+function resolveToolName(name: string, byName: Map<string, McpToolDescriptor>): string | undefined {
|
|
|
+ const candidates = [
|
|
|
+ name,
|
|
|
+ name.startsWith("smqjh.") ? name.slice("smqjh.".length) : `smqjh.${name}`,
|
|
|
+ name === "product.lookup.summary" ? "smqjh.product.lookup.summary" : "",
|
|
|
+ name === "order.count.query" ? "smqjh.order.count.query" : "",
|
|
|
+ name === "database.smart.query" ? "smqjh.database.smart.query" : "",
|
|
|
+ name === "database.readonly.query" ? "smqjh.database.readonly.query" : "",
|
|
|
+ name === "cloud.health" ? "smqjh.cloud.health" : ""
|
|
|
+ ].filter(Boolean);
|
|
|
+ return candidates.find((candidate) => byName.has(candidate));
|
|
|
+}
|
|
|
+
|
|
|
+function extractTables(observations: ToolObservation[]): AgentResultTable[] {
|
|
|
+ const tables: AgentResultTable[] = [];
|
|
|
+ for (const observation of observations) {
|
|
|
+ if (!observation.ok) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ tables.push(...extractTablesFromValue(toolTitle(observation.name), observation.result));
|
|
|
+ }
|
|
|
+ return tables.slice(0, 6);
|
|
|
+}
|
|
|
+
|
|
|
+function extractTablesFromValue(title: string, value: unknown): AgentResultTable[] {
|
|
|
+ const tables: AgentResultTable[] = [];
|
|
|
+ const seen = new WeakSet<object>();
|
|
|
+
|
|
|
+ function visit(node: unknown, currentTitle: string, depth: number): void {
|
|
|
+ if (!node || typeof node !== "object" || depth > 5) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (seen.has(node)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ seen.add(node);
|
|
|
+
|
|
|
+ const record = node as Record<string, unknown>;
|
|
|
+ addRowsTable(currentTitle, record, "rows");
|
|
|
+ addRowsTable(currentTitle, record, "comparisonRows");
|
|
|
+
|
|
|
+ for (const key of ["data", "structuredContent", "result", "record"]) {
|
|
|
+ if (record[key]) {
|
|
|
+ visit(record[key], currentTitle, depth + 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function addRowsTable(currentTitle: string, record: Record<string, unknown>, rowsKey: "rows" | "comparisonRows"): void {
|
|
|
+ const rows = Array.isArray(record[rowsKey]) ? record[rowsKey] : undefined;
|
|
|
+ if (!rows || !rows.every(isRecord)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const rowRecords = rows.slice(0, 200).map((row) => normalizeRow(row as Record<string, unknown>));
|
|
|
+ const providedColumns = Array.isArray(record.columns) ? record.columns.map((item) => String(item)) : [];
|
|
|
+ const columns = providedColumns.length
|
|
|
+ ? providedColumns
|
|
|
+ : Array.from(new Set(rowRecords.flatMap((row) => Object.keys(row))));
|
|
|
+ if (!columns.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ tables.push({
|
|
|
+ title: buildTableTitle(currentTitle, record, rowsKey),
|
|
|
+ columns,
|
|
|
+ rows: rowRecords
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ visit(value, title, 0);
|
|
|
+ return tables;
|
|
|
+}
|
|
|
+
|
|
|
+function fallbackSummary(observations: ToolObservation[], tables: AgentResultTable[]): string {
|
|
|
+ const okCount = observations.filter((item) => item.ok).length;
|
|
|
+ const failed = observations.filter((item) => !item.ok);
|
|
|
+ const lines = [`已执行 ${observations.length} 个 MCP 工具,成功 ${okCount} 个。`];
|
|
|
+ if (tables.length) {
|
|
|
+ lines.push("结果已整理为下方表格。");
|
|
|
+ }
|
|
|
+ if (failed.length) {
|
|
|
+ lines.push(`失败工具:${failed.map((item) => `${item.name}:${item.error || "调用失败"}`).join(";")}`);
|
|
|
+ }
|
|
|
+ const evidence = findEvidence(observations);
|
|
|
+ if (evidence) {
|
|
|
+ lines.push(`依据:${evidence}`);
|
|
|
+ }
|
|
|
+ return lines.join("\n\n");
|
|
|
+}
|
|
|
+
|
|
|
+function findEvidence(observations: ToolObservation[]): string {
|
|
|
+ for (const observation of observations) {
|
|
|
+ const sql = findKeyValue(observation.result, "executedSql", 0);
|
|
|
+ if (typeof sql === "string" && sql.trim()) {
|
|
|
+ return sql.trim();
|
|
|
+ }
|
|
|
+ const evidence = findKeyValue(observation.result, "evidence", 0);
|
|
|
+ if (typeof evidence === "string" && evidence.trim()) {
|
|
|
+ return evidence.trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
+function findKeyValue(value: unknown, key: string, depth: number): unknown {
|
|
|
+ if (!value || typeof value !== "object" || depth > 4) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ const record = value as Record<string, unknown>;
|
|
|
+ if (record[key] !== undefined) {
|
|
|
+ return record[key];
|
|
|
+ }
|
|
|
+ for (const child of Object.values(record)) {
|
|
|
+ const result = findKeyValue(child, key, depth + 1);
|
|
|
+ if (result !== undefined) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
+function trace(phase: AgentTraceEvent["phase"], title: string, detail?: string, data?: unknown): AgentTraceEvent {
|
|
|
+ return {
|
|
|
+ at: new Date().toISOString(),
|
|
|
+ phase,
|
|
|
+ title,
|
|
|
+ detail,
|
|
|
+ data
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function reasonText(record: Record<string, unknown>): string {
|
|
|
+ return typeof record.reason === "string" && record.reason.trim() ? record.reason.trim() : "";
|
|
|
+}
|
|
|
+
|
|
|
+function summarizeResult(value: unknown): unknown {
|
|
|
+ const record = asRecord(value);
|
|
|
+ if (typeof record.rowCount === "number") {
|
|
|
+ return { rowCount: record.rowCount, title: record.title, summary: record.summary };
|
|
|
+ }
|
|
|
+ if (Array.isArray(record.rows)) {
|
|
|
+ return { rows: record.rows.length };
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+}
|
|
|
+
|
|
|
+function buildTableTitle(baseTitle: string, record: Record<string, unknown>, rowsKey: string): string {
|
|
|
+ const title = typeof record.title === "string" && record.title.trim() ? record.title.trim() : baseTitle;
|
|
|
+ const count = record.rowCount === undefined ? "" : `(${record.rowCount} 行)`;
|
|
|
+ return rowsKey === "comparisonRows" ? `${title}:价格对比` : `${title}${count}`;
|
|
|
+}
|
|
|
+
|
|
|
+function toolTitle(name: string): string {
|
|
|
+ const titles: Record<string, string> = {
|
|
|
+ "smqjh.config.get": "运行配置",
|
|
|
+ "smqjh.cloud.health": "网关连通检查",
|
|
|
+ "smqjh.schema.search": "业务表搜索结果",
|
|
|
+ "smqjh.schema.getTable": "业务表说明",
|
|
|
+ "smqjh.schema.businessRules": "业务规则",
|
|
|
+ "smqjh.database.readonly.query": "数据库只读查询结果",
|
|
|
+ "smqjh.database.smart.query": "智能数据库查询结果",
|
|
|
+ "smqjh.order.count.query": "订单统计结果",
|
|
|
+ "smqjh.product.lookup.summary": "商品资料查询结果",
|
|
|
+ "smqjh.settlement.enterprise.list": "月结企业清单",
|
|
|
+ "smqjh.settlement.monthly.plan": "企业月结计划"
|
|
|
+ };
|
|
|
+ return titles[name] ?? name;
|
|
|
+}
|
|
|
+
|
|
|
+function toolSignature(name: string, args: Record<string, unknown>): string {
|
|
|
+ return `${name}:${stableStringify(args)}`;
|
|
|
+}
|
|
|
+
|
|
|
+function stableStringify(value: unknown): string {
|
|
|
+ if (!value || typeof value !== "object") {
|
|
|
+ return JSON.stringify(value);
|
|
|
+ }
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ return `[${value.map(stableStringify).join(",")}]`;
|
|
|
+ }
|
|
|
+ const record = value as Record<string, unknown>;
|
|
|
+ return `{${Object.keys(record)
|
|
|
+ .sort()
|
|
|
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
|
+ .join(",")}}`;
|
|
|
+}
|
|
|
+
|
|
|
+function asRecord(value: unknown): Record<string, unknown> {
|
|
|
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
|
+}
|
|
|
+
|
|
|
+function asArray<T>(value: unknown): T[] {
|
|
|
+ return Array.isArray(value) ? (value as T[]) : [];
|
|
|
+}
|
|
|
+
|
|
|
+function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeRow(row: Record<string, unknown>): Record<string, string> {
|
|
|
+ return Object.fromEntries(
|
|
|
+ Object.entries(row).map(([key, value]) => [key, value === undefined || value === null ? "" : String(value)])
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function errorMessage(error: unknown): string {
|
|
|
+ return error instanceof Error ? error.message : String(error);
|
|
|
+}
|