0.2.1 onlyoffice修复

This commit is contained in:
liaibo
2026-01-17 10:12:53 +08:00
parent 94957dc361
commit 19907bccdc
102 changed files with 7188 additions and 186 deletions
@@ -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 });
}
+233 -19
View File
@@ -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 });