Files
mnote/wolai-frontend/src/app/api/ai-agent/run/route.ts
T

611 lines
26 KiB
TypeScript
Raw Normal View History

2026-01-10 23:08:56 +08:00
import { NextResponse } from "next/server";
import { loadOnlineAiConfig } from "@/lib/ai/onlineAiConfig";
import { loadLocalAiConfig } from "@/lib/ai/localAiConfig";
import { createToolRegistry, resolveAllowedToolIds } from "@/lib/ai-agent/tools/registry";
import { builtinTools, builtinToolSets } from "@/lib/ai-agent/tools/builtins/registryBuiltins";
import { runAiAgent } from "@/lib/ai-agent/runtime/runAgent";
2026-01-17 10:12:53 +08:00
import { isConvexEnabled } from "@/lib/convex/enabled";
2026-01-10 23:08:56 +08:00
import { searchSearxng } from "@/lib/ai-agent/tools/builtins/searchWeb";
import { createMindmapServerTools, type MindmapSupabaseClient } from "@/lib/ai-agent/tools/builtins/mindmap/mindmapServerTools";
import { createDocServerTools, type DocSupabaseClient } from "@/lib/ai-agent/tools/builtins/doc/docServerTools";
import { createRagServerTools } from "@/lib/ai-agent/tools/builtins/rag/lightragServerTools";
import { createDocsServerTools, type DocsSupabaseClient } from "@/lib/ai-agent/tools/builtins/docs/docsServerTools";
import { createMediaServerTools, type MediaSupabaseClient } from "@/lib/ai-agent/tools/builtins/media/mediaServerTools";
import { createSlashServerTools, type SlashSupabaseClient } from "@/lib/ai-agent/tools/builtins/slash/slashServerTools";
2026-01-11 12:35:53 +08:00
import { createOnlyOfficeServerTools, type OnlyOfficeSupabaseClient } from "@/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools";
import { buildClientToolKey, registerClientToolCall } from "@/lib/ai-agent/runtime/clientToolBridge";
2026-01-17 10:12:53 +08:00
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
2026-01-10 23:08:56 +08:00
export const dynamic = "force-dynamic";
2026-01-11 12:35:53 +08:00
type AgentMessage = { role: "user" | "assistant"; content: string };
type AgentScope = "global" | "mindmap" | "document" | "onlyoffice";
2026-01-10 23:08:56 +08:00
type RequestPayload = {
stream?: boolean;
maxSteps?: number;
scope?: AgentScope;
messages: AgentMessage[];
attachments?: Array<{ id: string; title: string; fileUrl: string; mimeType?: string | null }>;
toolChoice?: { mode: "auto" | "manual"; toolSets?: string[]; tools?: string[] };
context?: {
documentId?: string;
mindmapId?: string;
selectedUids?: string[];
// v1BlockNote 文档快照(前端可选传入,避免覆盖未落盘编辑)
documentBlocks?: unknown;
};
options?: { searxng?: boolean; ai?: { provider?: "online" | "local"; model?: string } };
};
const DEFAULT_MAX_STEPS = 10;
2026-01-11 12:35:53 +08:00
const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 60_000;
const makeRunId = () => {
try {
const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto;
if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
} catch {
// ignore
}
return `run_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
};
const isOnlyOfficeClientTool = (toolId: string) => toolId.startsWith("oo_");
2026-01-10 23:08:56 +08:00
const sseHeaders = {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
} as const;
const toSseFrame = (event: string, data: unknown) => {
const json = JSON.stringify(data ?? null);
return `event: ${event}\ndata: ${json}\n\n`;
};
export async function POST(request: Request) {
const payload = (await request.json().catch(() => null)) as RequestPayload | null;
if (!payload || !Array.isArray(payload.messages) || payload.messages.length === 0) {
return NextResponse.json({ error: "缺少 messages" }, { status: 400 });
}
2026-01-17 10:12:53 +08:00
// v1:鉴权(Convex 迁移阶段使用固定开发用户;非 Convex 模式仍走 Supabase session
const convexOn = isConvexEnabled();
const { userId, supabase, convexClient } = await (async () => {
if (convexOn) {
2026-01-18 05:13:53 +08:00
const { auth, client } = await getAuthedConvexClient();
2026-01-17 10:12:53 +08:00
return { userId: auth.userId, supabase: null as any, convexClient: client };
}
2026-01-18 19:01:31 +08:00
return { userId: "", supabase: null as any, convexClient: null as any };
2026-01-17 10:12:53 +08:00
})();
if (!userId) {
2026-01-10 23:08:56 +08:00
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const provider = payload.options?.ai?.provider === "local" ? "local" : "online";
const modelOverride = String(payload.options?.ai?.model ?? "").trim() || null;
const cfg =
provider === "local"
? await loadLocalAiConfig().catch(() => null)
: await loadOnlineAiConfig().catch(() => null);
if (!cfg) {
const tip =
provider === "local"
? "未找到本地 AI 配置(LOCAL_AI_BASE_URL/LOCAL_AI_MODEL 或 ai.local.md / ai-local.md"
: "未找到在线 AI 配置(ai.md 或 ONLINE_AI_* 环境变量)";
return NextResponse.json({ error: tip }, { status: 500 });
}
const registry = createToolRegistry({ tools: builtinTools, toolSets: builtinToolSets });
const allowedToolIds = resolveAllowedToolIds({
registry,
mode: payload.toolChoice?.mode === "manual" ? "manual" : "auto",
toolSetIds: payload.toolChoice?.toolSets,
toolIds: payload.toolChoice?.tools,
});
const documentId = String(payload.context?.documentId ?? "").trim();
const mindmapId = String(payload.context?.mindmapId ?? "").trim();
const scope: AgentScope = (() => {
const raw = String(payload.scope ?? "").trim();
2026-01-11 12:35:53 +08:00
if (raw === "global" || raw === "mindmap" || raw === "document" || raw === "onlyoffice") return raw;
2026-01-10 23:08:56 +08:00
// 兜底:有 mindmapId 则认为在 mindmap 场景,否则视为全局场景
return mindmapId ? "mindmap" : "global";
})();
// v1:按“使用位置”隔离工具,避免工具混淆/误调用(即使用户手动传入,也会被过滤)
const allowToolSetIds: string[] =
scope === "mindmap"
? ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.mindmap_read", "toolset.mindmap_write"]
: scope === "document"
? ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.doc_read", "toolset.doc_write", "toolset.slash_write"]
2026-01-11 12:35:53 +08:00
: scope === "onlyoffice"
? ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.onlyoffice_read", "toolset.onlyoffice_write", "toolset.onlyoffice_editor"]
: ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.slash_write"];
2026-01-10 23:08:56 +08:00
const allowlist = new Set<string>();
for (const sid of allowToolSetIds) {
const s = registry.toolSetsById.get(sid);
(s?.toolIds ?? []).forEach((id) => allowlist.add(id));
}
for (const id of [...allowedToolIds]) {
if (!allowlist.has(id)) allowedToolIds.delete(id);
}
// v1:允许通过 options.searxng 关闭联网检索(比如离线模型/内网)
if (payload.options?.searxng === false) allowedToolIds.delete("search_web");
// v1:mindmap 工具必须在提供上下文时才允许,避免模型盲调导致误操作
const selectedUids = Array.isArray(payload.context?.selectedUids)
? payload.context!.selectedUids!.map((x) => String(x)).filter(Boolean).slice(0, 6)
: [];
const hasMindmapContext = Boolean(documentId && mindmapId);
const hasDocumentContext = Boolean(documentId);
const documentBlocks = payload.context?.documentBlocks ?? null;
const attachments = Array.isArray(payload.attachments) ? payload.attachments.slice(0, 12) : [];
const attachmentLines = attachments
.map((a, idx) => `${idx + 1}. id=${String(a.id)} title=${String(a.title)} mime=${String(a.mimeType ?? "")} url=${String(a.fileUrl)}`)
.join("\n");
if (!hasMindmapContext) {
allowedToolIds.delete("mindmap_get");
allowedToolIds.delete("mindmap_get_subtree");
allowedToolIds.delete("mindmap_apply_ops");
allowedToolIds.delete("mindmap_expand_node");
allowedToolIds.delete("mindmap_add_child");
allowedToolIds.delete("mindmap_add_sibling_after");
allowedToolIds.delete("mindmap_update_node_text");
allowedToolIds.delete("mindmap_set_hyperlink");
allowedToolIds.delete("mindmap_append_note");
allowedToolIds.delete("mindmap_set_refs");
allowedToolIds.delete("mindmap_delete_node");
allowedToolIds.delete("mindmap_add_attachment_ref");
allowedToolIds.delete("mindmap_add_attachment_child");
allowedToolIds.delete("mindmap_add_image_child");
allowedToolIds.delete("mindmap_append_image_note");
}
if (!hasDocumentContext) {
allowedToolIds.delete("doc_get");
allowedToolIds.delete("doc_find");
allowedToolIds.delete("doc_insert_blocks");
allowedToolIds.delete("doc_replace_range");
}
2026-01-17 10:12:53 +08:00
// 说明:Convex 迁移阶段(M4)先确保“不会再触发 Supabase 依赖”。
// 未迁移的能力(OnlyOffice 等)在 Convex 模式下直接禁用对应工具。
if (convexOn) {
for (const id of [...allowedToolIds]) {
if (
id === "search_web" ||
id === "image_read" ||
id === "slash_run" ||
id.startsWith("rag_") ||
id.startsWith("mindmap_") ||
id.startsWith("doc_") ||
id.startsWith("docs_")
) {
continue;
}
allowedToolIds.delete(id);
}
}
2026-01-10 23:08:56 +08:00
const systemContextText = (() => {
const lines: string[] = [];
if (documentId) lines.push(`documentId=${documentId}`);
if (scope === "mindmap") {
if (mindmapId) lines.push(`mindmapId=${mindmapId}`);
if (selectedUids.length) lines.push(`selectedUids=${selectedUids.join(",")}`);
}
if (scope === "document") {
if (documentBlocks) lines.push("documentBlocks=provided");
}
if (attachments.length) lines.push(`attachments:\n${attachmentLines}`);
return lines.join("\n").trim();
})();
2026-01-17 10:12:53 +08:00
const normalizeBlocksForTools = (content: unknown): unknown[] => {
if (Array.isArray(content)) return content;
if (content && typeof content === "object" && "blocks" in (content as any)) {
const blocks = (content as any).blocks;
if (Array.isArray(blocks)) return blocks;
}
return [];
};
const extractPlainTextFromBlocks = (blocks: unknown[], maxChars: number) => {
const pieces: string[] = [];
const walk = (list: unknown[]) => {
for (const b of list) {
if (!b || typeof b !== "object") continue;
const content = (b as any).content;
if (Array.isArray(content)) {
for (const n of content) {
const t = n && typeof n === "object" ? String((n as any).text ?? "") : "";
if (t) pieces.push(t);
if (pieces.join("").length >= maxChars) return;
}
}
const children = (b as any).children;
if (Array.isArray(children)) {
walk(children);
if (pieces.join("").length >= maxChars) return;
}
}
};
walk(blocks);
const raw = pieces.join("").replace(/\s+/g, " ").trim();
return raw.length > maxChars ? `${raw.slice(0, maxChars)}…` : raw;
};
2026-01-10 23:08:56 +08:00
const mindmapTools = hasMindmapContext
? createMindmapServerTools({
supabase: supabase as unknown as MindmapSupabaseClient,
2026-01-17 10:12:53 +08:00
ctx: { documentId, mindmapId, userId, selectedUids, attachments },
2026-01-10 23:08:56 +08:00
cfg: { ...cfg, model: modelOverride ?? cfg.model },
allowedToolIds,
2026-01-17 10:12:53 +08:00
...(convexOn
? {
loadMindmap: async () => {
if (!convexClient) throw new Error("Convex 未初始化");
const [mm, meta] = await Promise.all([
convexClient.query(api.mindmaps.get, { userId, docId: documentId, mindmapId }),
convexClient.query(api.documents.getMeta, { userId, id: documentId }),
]);
const title = meta?.title ?? null;
const workspaceId = meta?.workspace_id ?? (mm as any)?.meta?.workspace_id ?? null;
return {
doc: { id: documentId, title, workspace_id: workspaceId },
base: (mm as any)?.data ?? { data: { text: "中心主题" }, children: [] },
};
},
saveMindmap: async ({ data }) => {
if (!convexClient) throw new Error("Convex 未初始化");
await convexClient.mutation(api.mindmaps.put, { userId, docId: documentId, mindmapId, data });
},
}
: {}),
2026-01-10 23:08:56 +08:00
})
: null;
const docTools =
hasDocumentContext &&
(allowedToolIds.has("doc_get") ||
allowedToolIds.has("doc_find") ||
allowedToolIds.has("doc_insert_blocks") ||
allowedToolIds.has("doc_replace_range"))
? createDocServerTools({
supabase: supabase as unknown as DocSupabaseClient,
2026-01-17 10:12:53 +08:00
ctx: { documentId, userId, baseBlocks: documentBlocks },
2026-01-10 23:08:56 +08:00
allowedToolIds,
2026-01-17 10:12:53 +08:00
...(convexOn
? {
loadBlocks: async () => {
const base = normalizeBlocksForTools(documentBlocks);
if (base.length > 0) return { blocks: base, source: "client" };
if (!convexClient) throw new Error("Convex 未初始化");
const res = await convexClient.query(api.documents.getContent, { userId, id: documentId });
const blocks = normalizeBlocksForTools(res?.content ?? null);
return { blocks, source: "convex" };
},
saveBlocks: async (blocks: unknown[]) => {
if (!convexClient) throw new Error("Convex 未初始化");
await convexClient.mutation(api.documents.updateContent, { userId, id: documentId, content: blocks });
},
}
: {}),
2026-01-10 23:08:56 +08:00
})
: null;
const ragTools = allowedToolIds.has("rag_lightrag_query")
? createRagServerTools({
2026-01-17 10:12:53 +08:00
ctx: { userId },
2026-01-10 23:08:56 +08:00
allowedToolIds,
})
: null;
const docsTools =
allowedToolIds.has("docs_search") || allowedToolIds.has("docs_read")
? createDocsServerTools({
supabase: supabase as unknown as DocsSupabaseClient,
2026-01-17 10:12:53 +08:00
ctx: { userId },
2026-01-10 23:08:56 +08:00
allowedToolIds,
2026-01-17 10:12:53 +08:00
...(convexOn
? {
searchDocs: async ({ query, limit, workspaceId, includeDeleted }) => {
if (!convexClient) throw new Error("Convex 未初始化");
const wsIds = workspaceId
? [workspaceId]
: ((await convexClient.query(api.workspaces.fetchWorkspaceSummaries, { userId }))?.workspaces ?? []).map((w: any) =>
String(w?.id ?? ""),
);
const q = query.toLowerCase();
const results: any[] = [];
for (const wid of wsIds.filter(Boolean)) {
const docs = await convexClient.query(api.documents.listByWorkspace, { userId, workspaceId: wid });
const extra = includeDeleted
? await convexClient.query(api.documents.listTrashedByWorkspace, { userId, workspaceId: wid }).catch(() => [])
: [];
const all = [...(Array.isArray(docs) ? docs : []), ...(Array.isArray(extra) ? extra : [])];
for (const d of all) {
const title = String((d as any)?.title ?? "");
if (!title.toLowerCase().includes(q)) continue;
results.push({
id: String((d as any)?.id ?? ""),
title,
workspaceId: String((d as any)?.workspace_id ?? wid),
parentId: (d as any)?.parent_id ? String((d as any).parent_id) : null,
updatedAt: (d as any)?.updated_at ?? null,
snippet: title.slice(0, 120),
});
if (results.length >= limit) break;
}
if (results.length >= limit) break;
}
return results.slice(0, limit);
},
readDoc: async ({ documentId: rid, maxChars, includeContent }) => {
if (!convexClient) throw new Error("Convex 未初始化");
const meta = await convexClient.query(api.documents.getMeta, { userId, id: rid });
if (!meta) throw new Error("页面不存在");
const contentRes = await convexClient.query(api.documents.getContent, { userId, id: rid });
const blocks = normalizeBlocksForTools(contentRes?.content ?? null);
const rawText = extractPlainTextFromBlocks(blocks, maxChars);
return {
ok: true,
documentId: rid,
title: String(meta.title ?? ""),
workspaceId: String((meta as any).workspace_id ?? ""),
parentId: (meta as any).parent_id ? String((meta as any).parent_id) : null,
updatedAt: (meta as any).updated_at ?? null,
rawTextLength: rawText.length,
rawText,
...(includeContent ? { content: contentRes?.content ?? null } : {}),
};
},
}
: {}),
2026-01-10 23:08:56 +08:00
})
: null;
const mediaTools = allowedToolIds.has("image_read")
? createMediaServerTools({
supabase: supabase as unknown as MediaSupabaseClient,
2026-01-17 10:12:53 +08:00
ctx: { userId, attachments },
2026-01-10 23:08:56 +08:00
allowedToolIds,
2026-01-17 10:12:53 +08:00
...(convexOn
? {
loadById: async (id: string) => {
if (!convexClient) throw new Error("Convex 未初始化");
return await convexClient.query(api.mediaAssets.getById, { userId, id });
},
loadByFileUrl: async (_fileUrl: string) => null,
}
: {}),
2026-01-10 23:08:56 +08:00
})
: null;
2026-01-11 12:35:53 +08:00
const onlyofficeTools =
2026-01-17 10:12:53 +08:00
!convexOn && (allowedToolIds.has("asset_extract_outline") || allowedToolIds.has("asset_to_mindmap"))
2026-01-11 12:35:53 +08:00
? createOnlyOfficeServerTools({
supabase: supabase as unknown as OnlyOfficeSupabaseClient,
2026-01-17 10:12:53 +08:00
ctx: { userId, documentId: documentId || undefined, attachments },
2026-01-11 12:35:53 +08:00
allowedToolIds,
})
: null;
2026-01-10 23:08:56 +08:00
const slashTools = allowedToolIds.has("slash_run")
? createSlashServerTools({
supabase: supabase as unknown as SlashSupabaseClient,
2026-01-17 10:12:53 +08:00
ctx: { userId, currentDocumentId: documentId || undefined },
2026-01-10 23:08:56 +08:00
allowedToolIds,
2026-01-17 10:12:53 +08:00
...(convexOn
? {
loadWorkspaceIds: async (uid: string) => {
if (!convexClient) throw new Error("Convex 未初始化");
const res = await convexClient.query(api.workspaces.fetchWorkspaceSummaries, { userId: uid });
return (res?.workspaces ?? []).map((w: any) => String(w?.id ?? "")).filter(Boolean);
},
inferWorkspaceIdFromDoc: async (docId: string) => {
if (!convexClient) throw new Error("Convex 未初始化");
const meta = await convexClient.query(api.documents.getMeta, { userId, id: docId });
return meta ? String((meta as any).workspace_id ?? "") || null : null;
},
createDoc: async ({ workspaceId, parentId, title }) => {
if (!convexClient) throw new Error("Convex 未初始化");
const id = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `doc_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
const created = await convexClient.mutation(api.documents.create, {
userId,
id,
workspaceId,
parentId,
title,
accessScope: "private",
content: [],
});
return {
id: String((created as any).id ?? id),
title: String((created as any).title ?? title),
workspaceId: String((created as any).workspace_id ?? workspaceId),
parentId: (created as any).parent_id ? String((created as any).parent_id) : parentId,
createdAt: (created as any).created_at ?? null,
updatedAt: (created as any).updated_at ?? null,
};
},
renameDoc: async ({ documentId: did, title }) => {
if (!convexClient) throw new Error("Convex 未初始化");
await convexClient.mutation(api.documents.updateTitle, { userId, id: did, title });
const meta = await convexClient.query(api.documents.getMeta, { userId, id: did });
if (!meta) throw new Error("页面不存在");
return {
id: String((meta as any).id ?? did),
title: String((meta as any).title ?? title),
workspaceId: String((meta as any).workspace_id ?? ""),
parentId: (meta as any).parent_id ? String((meta as any).parent_id) : null,
updatedAt: (meta as any).updated_at ?? null,
};
},
}
: {}),
2026-01-10 23:08:56 +08:00
})
: null;
const runTool = async (toolId: string, toolArgs: Record<string, unknown>) => {
2026-01-11 12:35:53 +08:00
if (isOnlyOfficeClientTool(toolId)) {
throw new Error("oo_* 属于客户端工具:必须使用 stream 模式并在 OnlyOffice 页面内执行");
}
2026-01-10 23:08:56 +08:00
if (toolId === "search_web") {
const query = String(toolArgs.query ?? "").trim();
const count = Number(toolArgs.count ?? 6);
return await searchSearxng(query, Number.isFinite(count) ? count : 6);
}
if (toolId.startsWith("rag_")) {
if (!ragTools) throw new Error(`工具未初始化:${toolId}`);
return await ragTools.run(toolId, toolArgs);
}
if (toolId.startsWith("docs_")) {
if (!docsTools) throw new Error(`工具未初始化:${toolId}`);
return await docsTools.run(toolId, toolArgs);
}
if (toolId === "image_read") {
2026-01-11 12:35:53 +08:00
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
2026-01-10 23:08:56 +08:00
return await mediaTools.run(toolId, toolArgs);
}
2026-01-11 12:35:53 +08:00
if (toolId.startsWith("asset_")) {
if (!onlyofficeTools) throw new Error(`工具未初始化:${toolId}`);
return await onlyofficeTools.run(toolId, toolArgs);
}
2026-01-10 23:08:56 +08:00
if (toolId === "slash_run") {
2026-01-11 12:35:53 +08:00
if (!slashTools) throw new Error(`工具未初始化:${toolId}`);
2026-01-10 23:08:56 +08:00
return await slashTools.run(toolId, toolArgs);
}
if (toolId.startsWith("doc_")) {
if (!hasDocumentContext) throw new Error(`工具需要 document 上下文:${toolId}`);
if (!docTools) throw new Error(`工具需要 document 上下文:${toolId}`);
return await docTools.run(toolId, toolArgs);
}
if (!hasMindmapContext) throw new Error(`工具需要 mindmap 上下文:${toolId}`);
if (!mindmapTools) throw new Error(`工具需要 mindmap 上下文:${toolId}`);
return await mindmapTools.run(toolId, toolArgs);
};
const maxSteps = (() => {
const raw = Number(payload.maxSteps ?? DEFAULT_MAX_STEPS);
if (!Number.isFinite(raw)) return DEFAULT_MAX_STEPS;
return Math.max(1, Math.min(24, Math.floor(raw)));
})();
const stream = payload.stream !== false;
if (!stream) {
const events: Array<{ type: string; data: unknown }> = [];
const result = await runAiAgent({
userMessages: payload.messages.slice(0, 50),
cfg: { ...cfg, model: modelOverride ?? cfg.model },
allowedToolIds,
runTool,
maxSteps,
systemContextText,
defaultMindmapTargetUid: selectedUids[0] ?? "",
onEvent: (ev) => events.push(ev),
}).catch((e) => ({ ok: false as const, error: e instanceof Error ? e.message : String(e) }));
if (!result.ok) return NextResponse.json({ error: result.error }, { status: 500 });
return NextResponse.json({ text: result.text, steps: result.steps, events });
}
const encoder = new TextEncoder();
const body = new ReadableStream<Uint8Array>({
start(controller) {
const send = (event: string, data: unknown) => {
controller.enqueue(encoder.encode(toSseFrame(event, data)));
};
// 先发一个 ready,方便前端快速进入“流式模式”
2026-01-11 12:35:53 +08:00
const requestId = makeRunId();
send("ready", { ok: true, requestId });
let lastToolCall: { id: string; tool: string; args: Record<string, unknown> } | null = null;
const runToolStream = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (isOnlyOfficeClientTool(toolId)) {
const callId = lastToolCall?.tool === toolId ? lastToolCall.id : `call_${Date.now()}`;
const key = buildClientToolKey(requestId, callId);
2026-01-17 10:12:53 +08:00
const wait = registerClientToolCall({
key,
userId,
timeoutMs: DEFAULT_CLIENT_TOOL_TIMEOUT_MS,
});
2026-01-11 12:35:53 +08:00
// 说明:客户端收到该事件后,需要执行插件 API 并回调 /api/ai-agent/client-tool-result
send("client_tool_call", { requestId, callId, tool: toolId, args: toolArgs });
const result = await wait;
if (!result.ok) throw new Error(result.error);
return result.result;
}
return await runTool(toolId, toolArgs);
};
2026-01-10 23:08:56 +08:00
const ping = setInterval(() => {
// 避免某些代理/浏览器长连接超时
try {
controller.enqueue(encoder.encode(`: ping ${Date.now()}\n\n`));
} catch {
// ignore
}
}, 15_000);
(async () => {
const result = await runAiAgent({
userMessages: payload.messages.slice(0, 50),
cfg: { ...cfg, model: modelOverride ?? cfg.model },
allowedToolIds,
2026-01-11 12:35:53 +08:00
runTool: runToolStream,
2026-01-10 23:08:56 +08:00
maxSteps,
systemContextText,
defaultMindmapTargetUid: selectedUids[0] ?? "",
onEvent: (ev) => {
if (!ev?.type) return;
2026-01-11 12:35:53 +08:00
if (ev.type === "tool_call") {
try {
const d = ev.data as unknown;
const obj =
typeof d === "object" && d
? (d as Record<string, unknown>)
: ({} as Record<string, unknown>);
const id = String(obj.id ?? "").trim();
const tool = String(obj.tool ?? "").trim();
const args =
typeof obj.args === "object" && obj.args
? (obj.args as Record<string, unknown>)
: ({} as Record<string, unknown>);
if (id && tool) lastToolCall = { id, tool, args };
} catch {
// ignore
}
}
2026-01-10 23:08:56 +08:00
send(ev.type, ev.data ?? null);
},
});
if (!result.ok) {
send("error", { ok: false, message: result.error });
return;
}
send("completion", { ok: true, text: result.text, steps: result.steps });
})()
.catch((e) => {
const msg = e instanceof Error ? e.message : String(e);
send("error", { ok: false, message: msg });
})
.finally(() => {
clearInterval(ping);
controller.close();
});
},
});
return new Response(body, { headers: sseHeaders });
}