feat: 完成 rust cutover phase 8 收口
This commit is contained in:
@@ -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 = (() => {
|
||||
|
||||
Reference in New Issue
Block a user