0.2.1 onlyoffice修复
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { requireAuthContext } from "@/lib/auth/authContext";
|
||||
import {
|
||||
buildClientToolKey,
|
||||
resolveClientToolCall,
|
||||
@@ -26,11 +28,26 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "缺少 requestId/callId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (!session) return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
const userId = (() => {
|
||||
if (isConvexEnabled()) {
|
||||
const auth = requireAuthContext();
|
||||
return auth.userId;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
const resolvedUserId = async () => {
|
||||
if (userId) return userId;
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (!session) return null;
|
||||
return session.user.id;
|
||||
};
|
||||
|
||||
const finalUserId = await resolvedUserId();
|
||||
if (!finalUserId) return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
|
||||
const result: ClientToolResult = payload.ok
|
||||
? { ok: true, result: "result" in payload ? payload.result : null }
|
||||
@@ -39,7 +56,7 @@ export async function POST(request: Request) {
|
||||
const key = buildClientToolKey(requestId, callId);
|
||||
const resolved = resolveClientToolCall({
|
||||
key,
|
||||
userId: session.user.id,
|
||||
userId: finalUserId,
|
||||
result,
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
@@ -48,4 +65,3 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createToolRegistry, resolveAllowedToolIds } from "@/lib/ai-agent/tools/
|
||||
import { builtinTools, builtinToolSets } from "@/lib/ai-agent/tools/builtins/registryBuiltins";
|
||||
import { runAiAgent } from "@/lib/ai-agent/runtime/runAgent";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
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";
|
||||
@@ -14,6 +15,8 @@ import { createMediaServerTools, type MediaSupabaseClient } from "@/lib/ai-agent
|
||||
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";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { api } from "@/lib/convex/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -70,12 +73,21 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "缺少 messages" }, { status: 400 });
|
||||
}
|
||||
|
||||
// v1:先要求登录(避免在生产环境暴露推理能力);后续可做更细的权限控制
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (!session) {
|
||||
// v1:鉴权(Convex 迁移阶段使用固定开发用户;非 Convex 模式仍走 Supabase session)
|
||||
const convexOn = isConvexEnabled();
|
||||
const { userId, supabase, convexClient } = await (async () => {
|
||||
if (convexOn) {
|
||||
const { auth, client } = getAuthedConvexClient();
|
||||
return { userId: auth.userId, supabase: null as any, convexClient: client };
|
||||
}
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
if (!session) return { userId: "", supabase: null as any, convexClient: null as any };
|
||||
return { userId: session.user.id, supabase, convexClient: null as any };
|
||||
})();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -167,6 +179,25 @@ export async function POST(request: Request) {
|
||||
allowedToolIds.delete("doc_replace_range");
|
||||
}
|
||||
|
||||
// 说明: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);
|
||||
}
|
||||
}
|
||||
|
||||
const systemContextText = (() => {
|
||||
const lines: string[] = [];
|
||||
if (documentId) lines.push(`documentId=${documentId}`);
|
||||
@@ -181,12 +212,67 @@ export async function POST(request: Request) {
|
||||
return lines.join("\n").trim();
|
||||
})();
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const mindmapTools = hasMindmapContext
|
||||
? createMindmapServerTools({
|
||||
supabase: supabase as unknown as MindmapSupabaseClient,
|
||||
ctx: { documentId, mindmapId, userId: session.user.id, selectedUids, attachments },
|
||||
ctx: { documentId, mindmapId, userId, selectedUids, attachments },
|
||||
cfg: { ...cfg, model: modelOverride ?? cfg.model },
|
||||
allowedToolIds,
|
||||
...(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 });
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -198,14 +284,30 @@ export async function POST(request: Request) {
|
||||
allowedToolIds.has("doc_replace_range"))
|
||||
? createDocServerTools({
|
||||
supabase: supabase as unknown as DocSupabaseClient,
|
||||
ctx: { documentId, userId: session.user.id, baseBlocks: documentBlocks },
|
||||
ctx: { documentId, userId, baseBlocks: documentBlocks },
|
||||
allowedToolIds,
|
||||
...(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 });
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: null;
|
||||
|
||||
const ragTools = allowedToolIds.has("rag_lightrag_query")
|
||||
? createRagServerTools({
|
||||
ctx: { userId: session.user.id },
|
||||
ctx: { userId },
|
||||
allowedToolIds,
|
||||
})
|
||||
: null;
|
||||
@@ -214,24 +316,88 @@ export async function POST(request: Request) {
|
||||
allowedToolIds.has("docs_search") || allowedToolIds.has("docs_read")
|
||||
? createDocsServerTools({
|
||||
supabase: supabase as unknown as DocsSupabaseClient,
|
||||
ctx: { userId: session.user.id },
|
||||
ctx: { userId },
|
||||
allowedToolIds,
|
||||
...(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 } : {}),
|
||||
};
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: null;
|
||||
|
||||
const mediaTools = allowedToolIds.has("image_read")
|
||||
? createMediaServerTools({
|
||||
supabase: supabase as unknown as MediaSupabaseClient,
|
||||
ctx: { userId: session.user.id, attachments },
|
||||
ctx: { userId, attachments },
|
||||
allowedToolIds,
|
||||
...(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,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: null;
|
||||
|
||||
const onlyofficeTools =
|
||||
allowedToolIds.has("asset_extract_outline") || allowedToolIds.has("asset_to_mindmap")
|
||||
!convexOn && (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 },
|
||||
ctx: { userId, documentId: documentId || undefined, attachments },
|
||||
allowedToolIds,
|
||||
})
|
||||
: null;
|
||||
@@ -239,8 +405,56 @@ export async function POST(request: Request) {
|
||||
const slashTools = allowedToolIds.has("slash_run")
|
||||
? createSlashServerTools({
|
||||
supabase: supabase as unknown as SlashSupabaseClient,
|
||||
ctx: { userId: session.user.id, currentDocumentId: documentId || undefined },
|
||||
ctx: { userId, currentDocumentId: documentId || undefined },
|
||||
allowedToolIds,
|
||||
...(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,
|
||||
};
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -323,11 +537,11 @@ export async function POST(request: Request) {
|
||||
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,
|
||||
});
|
||||
const wait = registerClientToolCall({
|
||||
key,
|
||||
userId,
|
||||
timeoutMs: DEFAULT_CLIENT_TOOL_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// 说明:客户端收到该事件后,需要执行插件 API 并回调 /api/ai-agent/client-tool-result
|
||||
send("client_tool_call", { requestId, callId, tool: toolId, args: toolArgs });
|
||||
|
||||
Reference in New Issue
Block a user