0.3.3 外网下载修复
This commit is contained in:
@@ -16,6 +16,18 @@ import { createOnlyOfficeServerTools, type OnlyOfficeSupabaseClient } from "@/li
|
||||
import { buildClientToolKey, registerClientToolCall } from "@/lib/ai-agent/runtime/clientToolBridge";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import type { ConvexHttpClient } from "convex/browser";
|
||||
import {
|
||||
DEFAULT_AGENT_MAX_STEPS,
|
||||
MAX_AGENT_STEPS,
|
||||
MIN_AGENT_STEPS,
|
||||
DEFAULT_CLIENT_TOOL_TIMEOUT_MS,
|
||||
MAX_MINDMAP_ATTACHMENTS,
|
||||
MAX_SELECTED_NODES,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
} from "@/lib/constants";
|
||||
import { safeGetJsonBody, errorResponses, validateRequestBody } from "@/lib/api-utils";
|
||||
import { isPlainObject, hasProperty, toRecord } from "@/lib/type-guards";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -39,8 +51,46 @@ type RequestPayload = {
|
||||
options?: { searxng?: boolean; ai?: { provider?: "online" | "local"; model?: string } };
|
||||
};
|
||||
|
||||
const DEFAULT_MAX_STEPS = 10;
|
||||
const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 60_000;
|
||||
/** 按作用域分组的工具集 ID 映射 */
|
||||
const SCOPE_TOOLSET_MAP: Record<AgentScope, string[]> = {
|
||||
mindmap: [
|
||||
"toolset.readonly",
|
||||
"toolset.rag_read",
|
||||
"toolset.media_read",
|
||||
"toolset.mindmap_read",
|
||||
"toolset.mindmap_write",
|
||||
],
|
||||
document: [
|
||||
"toolset.readonly",
|
||||
"toolset.rag_read",
|
||||
"toolset.media_read",
|
||||
"toolset.docs_read",
|
||||
"toolset.doc_read",
|
||||
"toolset.doc_write",
|
||||
"toolset.slash_write",
|
||||
],
|
||||
onlyoffice: [
|
||||
"toolset.readonly",
|
||||
"toolset.rag_read",
|
||||
"toolset.media_read",
|
||||
"toolset.docs_read",
|
||||
"toolset.onlyoffice_read",
|
||||
"toolset.onlyoffice_write",
|
||||
"toolset.onlyoffice_editor",
|
||||
],
|
||||
global: [
|
||||
"toolset.readonly",
|
||||
"toolset.rag_read",
|
||||
"toolset.media_read",
|
||||
"toolset.docs_read",
|
||||
"toolset.slash_write",
|
||||
],
|
||||
};
|
||||
|
||||
/** 获取指定作用域允许的工具集 ID */
|
||||
const getToolSetIdsForScope = (scope: AgentScope): string[] => {
|
||||
return SCOPE_TOOLSET_MAP[scope] ?? SCOPE_TOOLSET_MAP.global;
|
||||
};
|
||||
|
||||
const makeRunId = () => {
|
||||
try {
|
||||
@@ -67,22 +117,31 @@ const toSseFrame = (event: string, data: unknown) => {
|
||||
};
|
||||
|
||||
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 });
|
||||
const payload = await safeGetJsonBody<RequestPayload>(request);
|
||||
if (!payload) {
|
||||
return errorResponses.badRequest("请求体不能为空");
|
||||
}
|
||||
const validationError = validateRequestBody(payload, ["messages"] as const);
|
||||
if (validationError) {
|
||||
return validationError;
|
||||
}
|
||||
if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
|
||||
return errorResponses.badRequest("缺少 messages");
|
||||
}
|
||||
|
||||
// v1:鉴权(Convex 迁移阶段使用固定开发用户;非 Convex 模式仍走 Supabase session)
|
||||
const convexOn = isConvexEnabled();
|
||||
const { userId, supabase, convexClient } = await (async () => {
|
||||
if (convexOn) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
return { userId: auth.userId, supabase: null as any, convexClient: client };
|
||||
}
|
||||
return { userId: "", supabase: null as any, convexClient: null as any };
|
||||
})();
|
||||
const supabase = null as unknown;
|
||||
let userId = "";
|
||||
let convexClient: ConvexHttpClient | null = null;
|
||||
|
||||
if (convexOn) {
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
userId = auth.userId ?? "";
|
||||
convexClient = client;
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
return errorResponses.unauthorized();
|
||||
}
|
||||
|
||||
const provider = payload.options?.ai?.provider === "local" ? "local" : "online";
|
||||
@@ -92,11 +151,7 @@ export async function POST(request: Request) {
|
||||
? 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 });
|
||||
return errorResponses.aiConfigError(provider);
|
||||
}
|
||||
|
||||
const registry = createToolRegistry({ tools: builtinTools, toolSets: builtinToolSets });
|
||||
@@ -116,15 +171,8 @@ export async function POST(request: Request) {
|
||||
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"]
|
||||
: 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"];
|
||||
// v1:按"使用位置"隔离工具,避免工具混淆/误调用(即使用户手动传入,也会被过滤)
|
||||
const allowToolSetIds = getToolSetIdsForScope(scope);
|
||||
const allowlist = new Set<string>();
|
||||
for (const sid of allowToolSetIds) {
|
||||
const s = registry.toolSetsById.get(sid);
|
||||
@@ -139,12 +187,12 @@ export async function POST(request: Request) {
|
||||
|
||||
// v1:mindmap 工具必须在提供上下文时才允许,避免模型盲调导致误操作
|
||||
const selectedUids = Array.isArray(payload.context?.selectedUids)
|
||||
? payload.context!.selectedUids!.map((x) => String(x)).filter(Boolean).slice(0, 6)
|
||||
? payload.context!.selectedUids!.map((x) => String(x)).filter(Boolean).slice(0, MAX_SELECTED_NODES)
|
||||
: [];
|
||||
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 attachments = Array.isArray(payload.attachments) ? payload.attachments.slice(0, MAX_MINDMAP_ATTACHMENTS) : [];
|
||||
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");
|
||||
@@ -251,8 +299,8 @@ export async function POST(request: Request) {
|
||||
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 }),
|
||||
convexClient.query(api.mindmaps.get, { docId: documentId, mindmapId }),
|
||||
convexClient.query(api.documents.getMeta, { id: documentId }),
|
||||
]);
|
||||
const title = meta?.title ?? null;
|
||||
const workspaceId = meta?.workspace_id ?? (mm as any)?.meta?.workspace_id ?? null;
|
||||
@@ -263,7 +311,7 @@ export async function POST(request: Request) {
|
||||
},
|
||||
saveMindmap: async ({ data }) => {
|
||||
if (!convexClient) throw new Error("Convex 未初始化");
|
||||
await convexClient.mutation(api.mindmaps.put, { userId, docId: documentId, mindmapId, data });
|
||||
await convexClient.mutation(api.mindmaps.put, { docId: documentId, mindmapId, data });
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -286,13 +334,13 @@ export async function POST(request: Request) {
|
||||
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 res = await convexClient.query(api.documents.getContent, { 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 });
|
||||
await convexClient.mutation(api.documents.updateContent, { id: documentId, content: blocks });
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -318,15 +366,15 @@ export async function POST(request: Request) {
|
||||
if (!convexClient) throw new Error("Convex 未初始化");
|
||||
const wsIds = workspaceId
|
||||
? [workspaceId]
|
||||
: ((await convexClient.query(api.workspaces.fetchWorkspaceSummaries, { userId }))?.workspaces ?? []).map((w: any) =>
|
||||
: ((await convexClient.query(api.workspaces.fetchWorkspaceSummaries, {}))?.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 docs = await convexClient.query(api.documents.listByWorkspace, { workspaceId: wid });
|
||||
const extra = includeDeleted
|
||||
? await convexClient.query(api.documents.listTrashedByWorkspace, { userId, workspaceId: wid }).catch(() => [])
|
||||
? await convexClient.query(api.documents.listTrashedByWorkspace, { workspaceId: wid }).catch(() => [])
|
||||
: [];
|
||||
const all = [...(Array.isArray(docs) ? docs : []), ...(Array.isArray(extra) ? extra : [])];
|
||||
for (const d of all) {
|
||||
@@ -348,9 +396,9 @@ export async function POST(request: Request) {
|
||||
},
|
||||
readDoc: async ({ documentId: rid, maxChars, includeContent }) => {
|
||||
if (!convexClient) throw new Error("Convex 未初始化");
|
||||
const meta = await convexClient.query(api.documents.getMeta, { userId, id: rid });
|
||||
const meta = await convexClient.query(api.documents.getMeta, { id: rid });
|
||||
if (!meta) throw new Error("页面不存在");
|
||||
const contentRes = await convexClient.query(api.documents.getContent, { userId, id: rid });
|
||||
const contentRes = await convexClient.query(api.documents.getContent, { id: rid });
|
||||
const blocks = normalizeBlocksForTools(contentRes?.content ?? null);
|
||||
const rawText = extractPlainTextFromBlocks(blocks, maxChars);
|
||||
return {
|
||||
@@ -403,21 +451,20 @@ export async function POST(request: Request) {
|
||||
allowedToolIds,
|
||||
...(convexOn
|
||||
? {
|
||||
loadWorkspaceIds: async (uid: string) => {
|
||||
loadWorkspaceIds: async (_uid: string) => {
|
||||
if (!convexClient) throw new Error("Convex 未初始化");
|
||||
const res = await convexClient.query(api.workspaces.fetchWorkspaceSummaries, { userId: uid });
|
||||
const res = await convexClient.query(api.workspaces.fetchWorkspaceSummaries, {});
|
||||
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 });
|
||||
const meta = await convexClient.query(api.documents.getMeta, { 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,
|
||||
@@ -436,8 +483,8 @@ export async function POST(request: Request) {
|
||||
},
|
||||
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 });
|
||||
await convexClient.mutation(api.documents.updateTitle, { id: did, title });
|
||||
const meta = await convexClient.query(api.documents.getMeta, { id: did });
|
||||
if (!meta) throw new Error("页面不存在");
|
||||
return {
|
||||
id: String((meta as any).id ?? did),
|
||||
@@ -458,8 +505,8 @@ export async function POST(request: Request) {
|
||||
}
|
||||
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);
|
||||
const count = Number(toolArgs.count ?? DEFAULT_SEARCH_COUNT);
|
||||
return await searchSearxng(query, Number.isFinite(count) ? count : DEFAULT_SEARCH_COUNT);
|
||||
}
|
||||
if (toolId.startsWith("rag_")) {
|
||||
if (!ragTools) throw new Error(`工具未初始化:${toolId}`);
|
||||
@@ -492,9 +539,9 @@ export async function POST(request: Request) {
|
||||
};
|
||||
|
||||
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 raw = Number(payload.maxSteps ?? DEFAULT_AGENT_MAX_STEPS);
|
||||
if (!Number.isFinite(raw)) return DEFAULT_AGENT_MAX_STEPS;
|
||||
return Math.max(MIN_AGENT_STEPS, Math.min(MAX_AGENT_STEPS, Math.floor(raw)));
|
||||
})();
|
||||
|
||||
const stream = payload.stream !== false;
|
||||
|
||||
Reference in New Issue
Block a user