0.1.14 上线前更改

This commit is contained in:
liaibo
2026-01-11 12:35:53 +08:00
parent be71849aa5
commit 725a60d3aa
44 changed files with 3427 additions and 310 deletions
@@ -12,11 +12,13 @@ import { createRagServerTools } from "@/lib/ai-agent/tools/builtins/rag/lightrag
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";
import { createOnlyOfficeServerTools, type OnlyOfficeSupabaseClient } from "@/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools";
import { buildClientToolKey, registerClientToolCall } from "@/lib/ai-agent/runtime/clientToolBridge";
export const dynamic = "force-dynamic";
type AgentMessage = { role: "user" | "assistant"; content: string };
type AgentScope = "global" | "mindmap" | "document";
type AgentMessage = { role: "user" | "assistant"; content: string };
type AgentScope = "global" | "mindmap" | "document" | "onlyoffice";
type RequestPayload = {
stream?: boolean;
@@ -36,6 +38,19 @@ type RequestPayload = {
};
const DEFAULT_MAX_STEPS = 10;
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_");
const sseHeaders = {
"Content-Type": "text/event-stream; charset=utf-8",
@@ -90,7 +105,7 @@ export async function POST(request: Request) {
const mindmapId = String(payload.context?.mindmapId ?? "").trim();
const scope: AgentScope = (() => {
const raw = String(payload.scope ?? "").trim();
if (raw === "global" || raw === "mindmap" || raw === "document") return raw;
if (raw === "global" || raw === "mindmap" || raw === "document" || raw === "onlyoffice") return raw;
// 兜底:有 mindmapId 则认为在 mindmap 场景,否则视为全局场景
return mindmapId ? "mindmap" : "global";
})();
@@ -101,7 +116,9 @@ export async function POST(request: Request) {
? ["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"]
: ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.slash_write"];
: 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"];
const allowlist = new Set<string>();
for (const sid of allowToolSetIds) {
const s = registry.toolSetsById.get(sid);
@@ -210,6 +227,15 @@ export async function POST(request: Request) {
})
: null;
const onlyofficeTools =
allowedToolIds.has("asset_extract_outline") || allowedToolIds.has("asset_to_mindmap")
? createOnlyOfficeServerTools({
supabase: supabase as unknown as OnlyOfficeSupabaseClient,
ctx: { userId: session.user.id, documentId: documentId || undefined, attachments },
allowedToolIds,
})
: null;
const slashTools = allowedToolIds.has("slash_run")
? createSlashServerTools({
supabase: supabase as unknown as SlashSupabaseClient,
@@ -219,6 +245,9 @@ export async function POST(request: Request) {
: null;
const runTool = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (isOnlyOfficeClientTool(toolId)) {
throw new Error("oo_* 属于客户端工具:必须使用 stream 模式并在 OnlyOffice 页面内执行");
}
if (toolId === "search_web") {
const query = String(toolArgs.query ?? "").trim();
const count = Number(toolArgs.count ?? 6);
@@ -233,11 +262,15 @@ export async function POST(request: Request) {
return await docsTools.run(toolId, toolArgs);
}
if (toolId === "image_read") {
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
return await mediaTools.run(toolId, toolArgs);
}
if (toolId.startsWith("asset_")) {
if (!onlyofficeTools) throw new Error(`工具未初始化:${toolId}`);
return await onlyofficeTools.run(toolId, toolArgs);
}
if (toolId === "slash_run") {
if (!slashTools) throw new Error(`工具未初始化:${toolId}`);
if (!slashTools) throw new Error(`工具未初始化:${toolId}`);
return await slashTools.run(toolId, toolArgs);
}
if (toolId.startsWith("doc_")) {
@@ -281,7 +314,30 @@ export async function POST(request: Request) {
};
// 先发一个 ready,方便前端快速进入“流式模式”
send("ready", { ok: true });
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);
const wait = registerClientToolCall({
key,
userId: session.user.id,
timeoutMs: DEFAULT_CLIENT_TOOL_TIMEOUT_MS,
});
// 说明:客户端收到该事件后,需要执行插件 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);
};
const ping = setInterval(() => {
// 避免某些代理/浏览器长连接超时
@@ -297,12 +353,30 @@ export async function POST(request: Request) {
userMessages: payload.messages.slice(0, 50),
cfg: { ...cfg, model: modelOverride ?? cfg.model },
allowedToolIds,
runTool,
runTool: runToolStream,
maxSteps,
systemContextText,
defaultMindmapTargetUid: selectedUids[0] ?? "",
onEvent: (ev) => {
if (!ev?.type) return;
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
}
}
send(ev.type, ev.data ?? null);
},
});