feat: 完成 rust cutover phase 8 收口

This commit is contained in:
lix-2026
2026-04-15 20:01:12 +08:00
parent 822c730cd6
commit 98db79b301
104 changed files with 18777 additions and 3025 deletions
+204 -18
View File
@@ -6,7 +6,6 @@ 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 { 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";
import { createRagServerTools } from "@/lib/ai-agent/tools/builtins/rag/lightragServerTools";
@@ -18,6 +17,13 @@ import { buildClientToolKey, registerClientToolCall } from "@/lib/ai-agent/runti
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import type { ConvexHttpClient } from "convex/browser";
import {
buildDocumentBridgeContextWithActor,
buildDocumentCommandEnvelope,
DocumentBridgeError,
} from "@/lib/documents/bridge";
import { recordBridgeCommandFailureArtifacts } from "@/lib/documents/bridge-log";
import { executeRustBridgeTool } from "@/lib/documents/rust-runtime";
import {
DEFAULT_AGENT_MAX_STEPS,
MAX_AGENT_STEPS,
@@ -106,6 +112,26 @@ const makeRunId = () => {
};
const isOnlyOfficeClientTool = (toolId: string) => toolId.startsWith("oo_");
const isDocRustTool = (toolId: string): toolId is "doc_get" | "doc_find" | "doc_insert_blocks" | "doc_replace_range" =>
toolId === "doc_get" ||
toolId === "doc_find" ||
toolId === "doc_insert_blocks" ||
toolId === "doc_replace_range";
const isMindmapWriteTool = (toolId: string) =>
toolId === "mindmap_put" ||
toolId === "mindmap_apply_ops" ||
toolId === "mindmap_add_child" ||
toolId === "mindmap_add_sibling_after" ||
toolId === "mindmap_update_node_text" ||
toolId === "mindmap_set_hyperlink" ||
toolId === "mindmap_append_note" ||
toolId === "mindmap_set_refs" ||
toolId === "mindmap_delete_node" ||
toolId === "mindmap_add_attachment_ref" ||
toolId === "mindmap_add_attachment_child" ||
toolId === "mindmap_add_image_child" ||
toolId === "mindmap_append_image_note" ||
toolId === "mindmap_expand_node";
const sseHeaders = {
"Content-Type": "text/event-stream; charset=utf-8",
@@ -179,6 +205,20 @@ export async function POST(request: Request) {
return errorResponses.unauthorized();
}
const aiBridgeContext = buildDocumentBridgeContextWithActor({
request,
actor: {
actorType: "user",
actorId: userId,
sessionId: null,
},
workspaceId: null,
source: {
channel: "ai-agent-route",
client: "wolai-frontend",
},
});
const provider = normalizeProvider(payload.options?.ai?.provider);
const modelOverride = String(payload.options?.ai?.model ?? "").trim() || null;
@@ -587,10 +627,6 @@ export async function POST(request: Request) {
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, { id: documentId, content: blocks });
},
}
: {}),
})
@@ -668,12 +704,12 @@ export async function POST(request: Request) {
: null;
const mediaTools = allowedToolIds.has("image_read")
? createMediaServerTools({
supabase: supabase as unknown as MediaSupabaseClient,
ctx: { userId, attachments },
allowedToolIds,
...(convexOn
? {
? createMediaServerTools({
supabase: supabase as unknown as MediaSupabaseClient,
ctx: { userId, attachments },
allowedToolIds,
...(convexOn
? {
loadById: async (id: string) => {
if (!convexClient) throw new Error("Convex 未初始化");
return await convexClient.query(api.mediaAssets.getById, { userId, id });
@@ -694,7 +730,7 @@ export async function POST(request: Request) {
: null;
const slashTools = allowedToolIds.has("slash_run")
? createSlashServerTools({
? createSlashServerTools({
supabase: supabase as unknown as SlashSupabaseClient,
ctx: { userId, currentDocumentId: documentId || undefined },
allowedToolIds,
@@ -755,7 +791,20 @@ export async function POST(request: Request) {
if (toolId === "search_web") {
const query = String(toolArgs.query ?? "").trim();
const count = Number(toolArgs.count ?? DEFAULT_SEARCH_COUNT);
return await searchSearxng(query, Number.isFinite(count) ? count : DEFAULT_SEARCH_COUNT);
const rustCount = Number.isFinite(count) ? count : DEFAULT_SEARCH_COUNT;
const rustResult = await executeRustBridgeTool<unknown>({
context: aiBridgeContext,
toolName: "search_web",
invocationKind: "query",
args: {
query,
count: rustCount,
},
data: {
source: "ai-agent-route",
},
});
return rustResult.result;
}
if (toolId.startsWith("rag_")) {
if (!ragTools) throw new Error(`工具未初始化:${toolId}`);
@@ -766,25 +815,162 @@ export async function POST(request: Request) {
return await docsTools.run(toolId, toolArgs);
}
if (toolId === "image_read") {
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
return await mediaTools.run(toolId, toolArgs);
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
const transport = await mediaTools.resolveImageReadTransport(toolArgs);
const rustResult = await executeRustBridgeTool<typeof transport>({
context: aiBridgeContext,
toolName: "image_read",
invocationKind: "query",
args: {
assetId: typeof toolArgs.assetId === "string" ? toolArgs.assetId : undefined,
fileUrl: typeof toolArgs.fileUrl === "string" ? toolArgs.fileUrl : undefined,
attachmentRef: typeof toolArgs.attachmentRef === "string" ? toolArgs.attachmentRef : undefined,
},
data: {
source: "ai-agent-route",
...transport,
},
});
return rustResult.result;
}
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}`);
return await slashTools.run(toolId, toolArgs);
if (!slashTools) throw new Error(`工具未初始化:${toolId}`);
const rustResult = await executeRustBridgeTool<{
ok: boolean;
source?: string;
parsed?: unknown;
}>({
context: aiBridgeContext,
toolName: "slash_run",
invocationKind: "command",
args: {
text: typeof toolArgs.text === "string" ? toolArgs.text : undefined,
command: typeof toolArgs.command === "string" ? toolArgs.command : undefined,
params:
isPlainObject(toolArgs.params) || Array.isArray(toolArgs.params)
? toolArgs.params
: undefined,
},
data: {
source: "ai-agent-route",
},
});
const parsed = rustResult.result?.parsed;
if (!isPlainObject(parsed) || parsed.ok !== true) {
throw new Error("Rust runtime 未返回有效的 slash_run 解析结果");
}
return await slashTools.executeSlashTransport(parsed as {
ok: true;
command: "new_doc" | "rename_doc";
params: Record<string, unknown>;
});
}
if (toolId.startsWith("doc_")) {
if (!hasDocumentContext) throw new Error(`工具需要 document 上下文:${toolId}`);
if (convexOn && convexClient && isDocRustTool(toolId)) {
const base = normalizeBlocksForTools(documentBlocks);
const runtimeContext = buildDocumentBridgeContextWithActor({
request,
actor: {
actorType: "user",
actorId: userId,
sessionId: null,
},
workspaceId: null,
source: {
channel: "ai-agent-route",
client: "wolai-frontend",
},
});
const result = await executeRustBridgeTool({
context: runtimeContext,
toolName: toolId,
invocationKind:
toolId === "doc_get" || toolId === "doc_find" ? "query" : "command",
args: toolArgs,
data: {
source: base.length > 0 ? "client" : "convex",
blocks:
base.length > 0
? base
: normalizeBlocksForTools(
(await convexClient.query(api.documents.getContent, { id: documentId }))?.content ?? null,
),
},
target: {
pageId: documentId,
workspaceId: null,
blockId:
typeof toolArgs.blockId === "string"
? toolArgs.blockId
: typeof toolArgs.afterBlockId === "string"
? toolArgs.afterBlockId
: typeof toolArgs.beforeBlockId === "string"
? toolArgs.beforeBlockId
: null,
},
reason:
typeof toolArgs.reason === "string" && toolArgs.reason.trim()
? toolArgs.reason.trim()
: `ai-agent:${toolId}`,
refs: ["task-031", "ai-agent"],
});
return result.result;
}
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);
try {
return await mindmapTools.run(toolId, toolArgs);
} catch (error) {
if (isMindmapWriteTool(toolId) && hasMindmapContext) {
const envelope = buildDocumentCommandEnvelope({
name: "documents.save",
payload: {
documentId,
workspaceId: null,
reason:
typeof toolArgs.reason === "string" && toolArgs.reason.trim()
? toolArgs.reason.trim()
: `ai-agent:${toolId}`,
toolId,
toolArgs,
},
context: aiBridgeContext,
target: {
workspaceId: null,
pageId: documentId,
blockId: mindmapId,
},
reason:
typeof toolArgs.reason === "string" && toolArgs.reason.trim()
? toolArgs.reason.trim()
: `ai-agent:${toolId}`,
refs: ["task-042", "ai-agent", toolId],
});
await recordBridgeCommandFailureArtifacts({
context: aiBridgeContext,
envelope,
client: convexClient ?? undefined,
error:
error instanceof DocumentBridgeError
? error
: new DocumentBridgeError(
error instanceof Error ? error.message : String(error),
500,
"TRANSPORT_ERROR",
{ reason: "mindmap_failed", toolId },
),
});
}
throw error;
}
};
const maxSteps = (() => {