feat: 提交 task-045 至 task-058 收口产物
- 收口 rust final closure checklist,推进页面/块系统/Mindmap/CLI/AI tools 到最终 cutover 状态 - 按 ai-frontend-simplification-plan-v1 接入 Hermes bridge,合并 AI 面板并清理旧前端编排残留 - 补充 harness 任务与进度记录,加入 CLI smoke 夹具/脚本,并修正文档页 bridge SSR 自请求回退逻辑
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { isPlainObject } from "@/lib/type-guards";
|
||||
|
||||
export type HermesBridgeConfig = {
|
||||
baseUrl: string;
|
||||
apiKey: string | null;
|
||||
};
|
||||
|
||||
export type HermesRunRequest = {
|
||||
input: Array<{ role: string; content: string }> | string;
|
||||
instructions?: string;
|
||||
conversation_history?: Array<{ role: string; content: string }>;
|
||||
session_id?: string;
|
||||
};
|
||||
|
||||
export type HermesRunStarted = {
|
||||
runId: string;
|
||||
};
|
||||
|
||||
export type HermesRunEvent =
|
||||
| { event: "tool.started"; tool: string; preview?: string | null }
|
||||
| { event: "tool.completed"; tool: string; duration?: number; error?: boolean }
|
||||
| { event: "message.delta"; delta: string }
|
||||
| { event: "run.completed"; output?: string; usage?: Record<string, unknown> }
|
||||
| { event: "run.failed"; error?: string }
|
||||
| { event: string; [key: string]: unknown };
|
||||
|
||||
const DEFAULT_BASE_URL = "http://127.0.0.1:8642";
|
||||
|
||||
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "");
|
||||
|
||||
export const readHermesBridgeConfig = (): HermesBridgeConfig => ({
|
||||
baseUrl: trimTrailingSlash((process.env.MNOTE_HERMES_API_BASE_URL || "").trim() || DEFAULT_BASE_URL),
|
||||
apiKey: (process.env.MNOTE_HERMES_API_KEY || "").trim() || null,
|
||||
});
|
||||
|
||||
export const buildHermesHeaders = (config: HermesBridgeConfig, init?: HeadersInit) => {
|
||||
const headers = new Headers(init);
|
||||
headers.set("Content-Type", "application/json");
|
||||
if (config.apiKey) {
|
||||
headers.set("Authorization", `Bearer ${config.apiKey}`);
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const startHermesRun = async (payload: HermesRunRequest): Promise<HermesRunStarted> => {
|
||||
const config = readHermesBridgeConfig();
|
||||
const response = await fetch(`${config.baseUrl}/v1/runs`, {
|
||||
method: "POST",
|
||||
headers: buildHermesHeaders(config),
|
||||
body: JSON.stringify(payload),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new Error(text || `Hermes run 启动失败:HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const json = (await response.json().catch(() => null)) as unknown;
|
||||
const runId = isPlainObject(json) ? String(json.run_id ?? "").trim() : "";
|
||||
if (!runId) throw new Error("Hermes run 响应缺少 run_id");
|
||||
return { runId };
|
||||
};
|
||||
|
||||
export const streamHermesRunEvents = async (
|
||||
runId: string,
|
||||
onEvent: (event: HermesRunEvent) => Promise<void> | void,
|
||||
) => {
|
||||
const config = readHermesBridgeConfig();
|
||||
const response = await fetch(`${config.baseUrl}/v1/runs/${encodeURIComponent(runId)}/events`, {
|
||||
method: "GET",
|
||||
headers: buildHermesHeaders(config),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new Error(text || `Hermes 事件流连接失败:HTTP ${response.status}`);
|
||||
}
|
||||
if (!response.body) throw new Error("Hermes 事件流不支持 body");
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
while (true) {
|
||||
const sep = buffer.indexOf("\n\n");
|
||||
if (sep === -1) break;
|
||||
const raw = buffer.slice(0, sep);
|
||||
buffer = buffer.slice(sep + 2);
|
||||
|
||||
if (raw.trimStart().startsWith(":")) continue;
|
||||
|
||||
const dataLines = raw
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice("data:".length).trimStart());
|
||||
if (dataLines.length === 0) continue;
|
||||
|
||||
let parsed: unknown = null;
|
||||
try {
|
||||
parsed = JSON.parse(dataLines.join("\n"));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!isPlainObject(parsed)) continue;
|
||||
await onEvent(parsed as HermesRunEvent);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAiAgent } from "./runAgent";
|
||||
import type { OpenAiCompatibleChatMessage } from "@/lib/ai/openaiCompatibleChat";
|
||||
|
||||
describe("runAiAgent", () => {
|
||||
it("按顺序执行 docs_search 和 docs_read 工具链", async () => {
|
||||
const events: Array<{ type: string; data: unknown }> = [];
|
||||
const chatCalls: OpenAiCompatibleChatMessage[][] = [];
|
||||
const runTool = vi.fn(async (toolId: string, toolArgs: Record<string, unknown>) => {
|
||||
if (toolId === "docs_search") {
|
||||
expect(toolArgs).toEqual({
|
||||
query: "Rust runtime 收口",
|
||||
limit: 5,
|
||||
});
|
||||
return {
|
||||
query: "Rust runtime 收口",
|
||||
results: [
|
||||
{
|
||||
id: "page_1",
|
||||
title: "Rust 文档",
|
||||
snippet: "这里记录 rust runtime 收口",
|
||||
},
|
||||
],
|
||||
source: "convex",
|
||||
};
|
||||
}
|
||||
|
||||
if (toolId === "docs_read") {
|
||||
expect(toolArgs).toEqual({
|
||||
documentId: "page_1",
|
||||
maxChars: 200,
|
||||
});
|
||||
return {
|
||||
documentId: "page_1",
|
||||
title: "Rust 文档",
|
||||
rawText: "这里记录 rust runtime 收口",
|
||||
rawTextLength: 24,
|
||||
source: "convex",
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`未知工具: ${toolId}`);
|
||||
});
|
||||
|
||||
let chatStep = 0;
|
||||
const chat = vi.fn(async (messages: OpenAiCompatibleChatMessage[]) => {
|
||||
chatCalls.push(messages);
|
||||
chatStep += 1;
|
||||
|
||||
if (chatStep === 1) {
|
||||
return {
|
||||
text: '<docs_search>{"query":"Rust runtime 收口","limit":5}</docs_search>',
|
||||
raw: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (chatStep === 2) {
|
||||
expect(messages.at(-1)?.content).toContain('<tool_result tool="docs_search">');
|
||||
expect(messages.at(-1)?.content).toContain('"id":"page_1"');
|
||||
return {
|
||||
text: '<docs_read>{"documentId":"page_1","maxChars":200}</docs_read>',
|
||||
raw: null,
|
||||
};
|
||||
}
|
||||
|
||||
expect(messages.at(-1)?.content).toContain('<tool_result tool="docs_read">');
|
||||
expect(messages.at(-1)?.content).toContain("rust runtime 收口");
|
||||
return {
|
||||
text: "已找到目标文档并读取原文。",
|
||||
raw: null,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await runAiAgent({
|
||||
userMessages: [{ role: "user", content: "请帮我查找 Rust runtime 收口的相关文档" }],
|
||||
cfg: {
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
apiKey: "",
|
||||
model: "test-model",
|
||||
},
|
||||
chat,
|
||||
allowedToolIds: new Set(["docs_search", "docs_read"]),
|
||||
runTool,
|
||||
maxSteps: 4,
|
||||
onEvent: (event) => events.push(event),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
text: "已找到目标文档并读取原文。",
|
||||
steps: 3,
|
||||
});
|
||||
expect(chat).toHaveBeenCalledTimes(3);
|
||||
expect(runTool).toHaveBeenCalledTimes(2);
|
||||
expect(runTool).toHaveBeenNthCalledWith(1, "docs_search", {
|
||||
query: "Rust runtime 收口",
|
||||
limit: 5,
|
||||
});
|
||||
expect(runTool).toHaveBeenNthCalledWith(2, "docs_read", {
|
||||
documentId: "page_1",
|
||||
maxChars: 200,
|
||||
});
|
||||
|
||||
expect(chatCalls[0]?.[0]?.role).toBe("system");
|
||||
expect(chatCalls[0]?.[1]?.content).toBe("请帮我查找 Rust runtime 收口的相关文档");
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "tool_call",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
tool: "docs_search",
|
||||
args: {
|
||||
query: "Rust runtime 收口",
|
||||
limit: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "tool_result",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
tool: "docs_search",
|
||||
ok: true,
|
||||
ms: expect.any(Number),
|
||||
result: {
|
||||
query: "Rust runtime 收口",
|
||||
results: [
|
||||
{
|
||||
id: "page_1",
|
||||
title: "Rust 文档",
|
||||
snippet: "这里记录 rust runtime 收口",
|
||||
},
|
||||
],
|
||||
source: "convex",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "tool_call",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
tool: "docs_read",
|
||||
args: {
|
||||
documentId: "page_1",
|
||||
maxChars: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "tool_result",
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
tool: "docs_read",
|
||||
ok: true,
|
||||
ms: expect.any(Number),
|
||||
result: {
|
||||
documentId: "page_1",
|
||||
title: "Rust 文档",
|
||||
rawText: "这里记录 rust runtime 收口",
|
||||
rawTextLength: 24,
|
||||
source: "convex",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "assistant_message",
|
||||
data: {
|
||||
text: "已找到目标文档并读取原文。",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// 遗留兼容:runAiAgent 已退出 /api/ai-agent/run 主链,保留此文件仅用于历史测试与渐进清理。
|
||||
import type { OpenAiCompatibleChatMessage, OpenAiCompatibleChatOptions } from "@/lib/ai/openaiCompatibleChat";
|
||||
import { openAiCompatibleChat } from "@/lib/ai/openaiCompatibleChat";
|
||||
import { parseToolTagCalls, formatToolResultTag } from "../protocol/toolTagProtocol";
|
||||
|
||||
@@ -48,7 +48,8 @@ export const createDocsServerTools = (args: {
|
||||
supabase?: DocsSupabaseClient;
|
||||
ctx: DocsToolContext;
|
||||
allowedToolIds: Set<string>;
|
||||
// 说明:Convex 迁移阶段用于“去 Supabase 化”。如果提供该能力,则完全不依赖 Supabase。
|
||||
// 说明:该文件现在主要服务于非 Convex 模式或兼容兜底;Convex 主链下 docs_* 已改由 Rust runtime 产出结果。
|
||||
// 说明:如果提供该能力,则走本地兼容 transport,不依赖 Supabase。
|
||||
searchDocs?: (args: {
|
||||
userId: string;
|
||||
query: string;
|
||||
|
||||
@@ -446,10 +446,10 @@ export const createMindmapServerTools = (args: {
|
||||
invocationKind: "command",
|
||||
toolArgs: withMindmapIds({ ops: normalized, reason }),
|
||||
data: base,
|
||||
target: mindmapTarget(doc),
|
||||
target: mindmapTarget(loaded.doc),
|
||||
reason,
|
||||
});
|
||||
await persistResultData(doc, result);
|
||||
await persistResultData(loaded.doc, result);
|
||||
return result;
|
||||
}
|
||||
const { data: nextData, applied, errors } = applyMindmapOps(base, normalized);
|
||||
@@ -460,7 +460,7 @@ export const createMindmapServerTools = (args: {
|
||||
opCount: normalized.length,
|
||||
});
|
||||
}
|
||||
await persistMindmap(doc, nextData);
|
||||
await persistMindmap(loaded.doc, nextData);
|
||||
return { ok: true, applied, errors, data: nextData, meta: { reason } };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { applyMindmapOps, ensureMindmapUids, type MindmapOp, type MindmapTreeNode, type NodeRef } from "@/lib/mindmap/mindmapOps";
|
||||
import { ensureMindmapUids, type MindmapOp, type MindmapTreeNode, type NodeRef } from "@/lib/mindmap/mindmapOps";
|
||||
import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore";
|
||||
import { buildDocumentBridgeContextWithActor } from "@/lib/documents/bridge";
|
||||
import { executeRustBridgeTool } from "@/lib/documents/rust-runtime";
|
||||
|
||||
type SupabaseRouteClient = {
|
||||
from: (table: string) => any;
|
||||
@@ -229,6 +231,21 @@ export const createOnlyOfficeServerTools = (args: {
|
||||
ctx: OnlyOfficeToolContext;
|
||||
allowedToolIds: Set<string>;
|
||||
}) => {
|
||||
const buildRustContext = () =>
|
||||
buildDocumentBridgeContextWithActor({
|
||||
request: new Request("http://localhost"),
|
||||
actor: {
|
||||
actorType: "service",
|
||||
actorId: "onlyoffice-asset-to-mindmap",
|
||||
sessionId: null,
|
||||
},
|
||||
workspaceId: null,
|
||||
source: {
|
||||
channel: "onlyoffice-asset-to-mindmap",
|
||||
client: "wolai-frontend",
|
||||
},
|
||||
});
|
||||
|
||||
const asset_extract_outline = async (toolArgs: Record<string, unknown>) => {
|
||||
const assetId = String(toolArgs.assetId ?? "").trim();
|
||||
const attachmentRef = String(toolArgs.attachmentRef ?? "").trim();
|
||||
@@ -308,11 +325,13 @@ export const createOnlyOfficeServerTools = (args: {
|
||||
};
|
||||
|
||||
const asset_to_mindmap = async (toolArgs: Record<string, unknown>) => {
|
||||
// 说明:附件大纲提取仍在 TS/MinerU 侧,真正的导图写入由 Rust mindmap_apply_ops 负责。
|
||||
const mindmapId = String(toolArgs.mindmapId ?? "").trim();
|
||||
if (!mindmapId) throw new Error("缺少 mindmapId");
|
||||
const parentUidArg = String(toolArgs.parentUid ?? "").trim();
|
||||
const maxItemsRaw = Number(toolArgs.maxItems ?? 120);
|
||||
const maxItems = Number.isFinite(maxItemsRaw) ? Math.max(10, Math.min(600, Math.floor(maxItemsRaw))) : 120;
|
||||
const reason = String(toolArgs.reason ?? "").trim() || null;
|
||||
|
||||
const documentId = String(args.ctx.documentId ?? "").trim();
|
||||
if (!documentId) throw new Error("缺少 documentId 上下文(OnlyOffice 工具需要落盘到指定文档)");
|
||||
@@ -356,8 +375,39 @@ export const createOnlyOfficeServerTools = (args: {
|
||||
if (!findNodeByUid(base, parentUid)) throw new Error("未找到 parentUid 对应节点");
|
||||
|
||||
const ops = buildOutlineOps({ parentUid, items, attachment });
|
||||
const { data: nextData, applied, errors } = applyMindmapOps(base, ops);
|
||||
await writeMindmapLocal(documentId, mindmapId, nextData, "OnlyOffice 生成导图");
|
||||
|
||||
const rustContext = buildRustContext();
|
||||
const rustResult = await executeRustBridgeTool<{
|
||||
ok: boolean;
|
||||
applied?: number;
|
||||
errors?: string[];
|
||||
data?: unknown;
|
||||
meta?: { reason?: string | null } | null;
|
||||
}>({
|
||||
context: rustContext,
|
||||
toolName: "mindmap_apply_ops",
|
||||
invocationKind: "command",
|
||||
args: {
|
||||
ops,
|
||||
reason,
|
||||
},
|
||||
data: base,
|
||||
target: {
|
||||
pageId: documentId,
|
||||
workspaceId: null,
|
||||
blockId: mindmapId,
|
||||
},
|
||||
reason,
|
||||
});
|
||||
|
||||
const resultData = rustResult.result && typeof rustResult.result === "object" ? (rustResult.result as Record<string, unknown>).data : null;
|
||||
const nextData = resultData && typeof resultData === "object" && !Array.isArray(resultData) ? (resultData as MindmapTreeNode) : base;
|
||||
await writeMindmapLocal(documentId, mindmapId, nextData, "OnlyOffice 生成导图(Rust)");
|
||||
|
||||
const applied = Number((rustResult.result as Record<string, unknown> | undefined)?.applied ?? ops.length) || 0;
|
||||
const errors = Array.isArray((rustResult.result as Record<string, unknown> | undefined)?.errors)
|
||||
? ((rustResult.result as Record<string, unknown>).errors as string[])
|
||||
: [];
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -371,6 +421,8 @@ export const createOnlyOfficeServerTools = (args: {
|
||||
fileName: attachment.title,
|
||||
items: items.length,
|
||||
strategy: String(outlineResult.strategy ?? ""),
|
||||
rustOwner: "mindmap_apply_ops",
|
||||
rustReason: reason,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// 遗留兼容:builtin 声明仍保留给历史兼容与文档对照,前端主链不再从这里继续扩展 Hermes 前置编排。
|
||||
import type { AiAgentTool, AiAgentToolSet } from "../types";
|
||||
|
||||
export const builtinTools: AiAgentTool[] = [
|
||||
@@ -409,6 +410,18 @@ export type BuiltinRustCutoverBinding = {
|
||||
* 完整的一一对应矩阵见 `design/ai-tool-cutover-matrix.md`。
|
||||
*/
|
||||
export const builtinRustCutoverBindings: Record<string, BuiltinRustCutoverBinding> = {
|
||||
docs_search: {
|
||||
rustToolsetId: "toolset.docs_read",
|
||||
rustToolName: "docs_search",
|
||||
status: "rust",
|
||||
note: "跨页文档搜索已切到 Rust runtime,TS 仅负责拉取搜索数据集 transport。",
|
||||
},
|
||||
docs_read: {
|
||||
rustToolsetId: "toolset.docs_read",
|
||||
rustToolName: "docs_read",
|
||||
status: "rust",
|
||||
note: "跨页文档读取结果已由 Rust runtime 统一裁剪与归一化,TS 仅负责读取目标文档 transport。",
|
||||
},
|
||||
search_web: {
|
||||
rustToolsetId: "toolset.readonly",
|
||||
rustToolName: "search_web",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// 遗留兼容:当前 AI 主链已转到 Hermes bridge,这里的 registry 仅保留给历史测试/兼容调用,不再作为前端主编排入口。
|
||||
import type { AiAgentTool, AiAgentToolSet, ToolPermissions } from "./types";
|
||||
|
||||
export type ToolRegistry = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { api } from "@/lib/convex/api";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import {
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
resolveRustBridgeCommandPlan,
|
||||
resolveRustBridgeQueryPlan,
|
||||
} from "@/lib/documents/rust-runtime";
|
||||
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
|
||||
import { extractBlocksFromContent } from "@/lib/document-content";
|
||||
|
||||
type BlockLike = {
|
||||
id: string;
|
||||
@@ -91,20 +90,6 @@ function replaceBlockInTree(blocks: BlockLike[], blockId: string, nextBlock: unk
|
||||
return { ok: true, nextBlocks: nextTop };
|
||||
}
|
||||
|
||||
function buildReferenceBlock(sourceDocumentId: string, blockId: string): BlockLike {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
type: "blockReference",
|
||||
props: {
|
||||
sourceDocumentId,
|
||||
targetBlockId: blockId,
|
||||
display: "embed",
|
||||
},
|
||||
content: [],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function buildBridgeContext(request: Request, workspaceId: string | null): Promise<BridgeContext> {
|
||||
return await buildDocumentBridgeContext({ request, workspaceId });
|
||||
}
|
||||
@@ -183,10 +168,6 @@ export async function executeBlockPatchBridgeCommand(input: {
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
await client.mutation(api.documents.updateContent, {
|
||||
id: input.sourceDocumentId,
|
||||
content: composeContentWithBlocks(doc.content, replaced.nextBlocks as never),
|
||||
});
|
||||
await recordBridgeCommandArtifacts({ context, envelope });
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
@@ -220,9 +201,6 @@ export async function executeBlockMoveBridgeCommand(input: {
|
||||
const sourceBlocks = extractBlocksFromContent(source.content) as BlockLike[];
|
||||
const removedRes = removeBlockSubtree(sourceBlocks, input.blockId);
|
||||
if (!removedRes.removed) throw new Error("源块不存在或无权限");
|
||||
const targetBlocks = extractBlocksFromContent(target.content) as BlockLike[];
|
||||
const nextSourceContent = composeContentWithBlocks(source.content, removedRes.nextBlocks as never);
|
||||
const nextTargetContent = composeContentWithBlocks(target.content, [...targetBlocks, removedRes.removed] as never);
|
||||
const context = await buildBridgeContext(input.request, null);
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "blocks.move",
|
||||
@@ -240,8 +218,6 @@ export async function executeBlockMoveBridgeCommand(input: {
|
||||
const plan = await resolveRustBridgeCommandPlan({ context, envelope });
|
||||
try {
|
||||
await executeRustBridgeMutationTransport({ client, plan });
|
||||
await client.mutation(api.documents.updateContent, { id: input.sourceDocumentId, content: nextSourceContent });
|
||||
await client.mutation(api.documents.updateContent, { id: input.targetDocumentId, content: nextTargetContent });
|
||||
await recordBridgeCommandArtifacts({ context, envelope });
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
@@ -276,18 +252,14 @@ export async function executeBlockEmbedBridgeCommand(input: {
|
||||
const sourceBlocks = extractBlocksFromContent(source.content) as BlockLike[];
|
||||
const hit = findBlockInTree(sourceBlocks, input.blockId);
|
||||
if (!hit) throw new Error("源块不存在或无权限");
|
||||
const targetBlocks = extractBlocksFromContent(target.content) as BlockLike[];
|
||||
const anchorId = (targetMeta as { embed_default_block_id?: string | null } | null)?.embed_default_block_id ?? null;
|
||||
const anchorIndex =
|
||||
typeof anchorId === "string" && anchorId.trim()
|
||||
? targetBlocks.findIndex((block) => String(block.id ?? "") === anchorId)
|
||||
? extractBlocksFromContent(target.content).findIndex((block) => String((block as BlockLike).id ?? "") === anchorId)
|
||||
: -1;
|
||||
const insertIndex = anchorIndex >= 0 ? anchorIndex + 1 : targetBlocks.length;
|
||||
const nextTargetBlocks = [
|
||||
...targetBlocks.slice(0, insertIndex),
|
||||
buildReferenceBlock(input.sourceDocumentId, input.blockId),
|
||||
...targetBlocks.slice(insertIndex),
|
||||
];
|
||||
const targetBlocks = extractBlocksFromContent(target.content) as BlockLike[];
|
||||
void targetBlocks;
|
||||
void anchorIndex;
|
||||
const context = await buildBridgeContext(input.request, null);
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "blocks.embed",
|
||||
@@ -305,10 +277,6 @@ export async function executeBlockEmbedBridgeCommand(input: {
|
||||
const plan = await resolveRustBridgeCommandPlan({ context, envelope });
|
||||
try {
|
||||
await executeRustBridgeMutationTransport({ client, plan });
|
||||
await client.mutation(api.documents.updateContent, {
|
||||
id: input.targetDocumentId,
|
||||
content: composeContentWithBlocks(target.content, nextTargetBlocks as never),
|
||||
});
|
||||
await recordBridgeCommandArtifacts({ context, envelope });
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
|
||||
@@ -12,16 +12,74 @@ type DocumentMetaResponse<T> = {
|
||||
meta: BridgeMeta;
|
||||
};
|
||||
|
||||
function getServerRequestOrigin(headerList: Headers): string {
|
||||
const forwardedProto = headerList.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
||||
const forwardedHost = headerList.get("x-forwarded-host")?.split(",")[0]?.trim();
|
||||
const host = forwardedHost || headerList.get("host");
|
||||
function getHeaderFirstValue(value: string | null): string {
|
||||
return String(value || "")
|
||||
.split(",")[0]
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseHostParts(host: string): { hostname: string; port: string } {
|
||||
try {
|
||||
const parsed = new URL(`http://${host}`);
|
||||
return {
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
hostname: host,
|
||||
port: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalHostname(hostname: string): boolean {
|
||||
const normalized = hostname.replace(/^\[(.*)\]$/, "$1").trim().toLowerCase();
|
||||
return (
|
||||
normalized === "localhost" ||
|
||||
normalized === "127.0.0.1" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "0.0.0.0" ||
|
||||
normalized === "::"
|
||||
);
|
||||
}
|
||||
|
||||
function buildServerRequestOriginCandidates(headerList: Headers): string[] {
|
||||
const forwardedProto = getHeaderFirstValue(headerList.get("x-forwarded-proto"));
|
||||
const forwardedHost = getHeaderFirstValue(headerList.get("x-forwarded-host"));
|
||||
const host = forwardedHost || getHeaderFirstValue(headerList.get("host"));
|
||||
|
||||
if (!host) {
|
||||
throw new Error("缺少 host 头,无法构造 bridge 请求地址");
|
||||
}
|
||||
|
||||
return `${forwardedProto || "http"}://${host}`;
|
||||
const { hostname, port } = parseHostParts(host);
|
||||
const candidates = new Set<string>();
|
||||
const addCandidate = (proto: string, candidateHost: string) => {
|
||||
const normalizedProto = String(proto || "http").trim().toLowerCase() || "http";
|
||||
const normalizedHost = String(candidateHost || "").trim();
|
||||
if (!normalizedHost) return;
|
||||
candidates.add(`${normalizedProto}://${normalizedHost}`);
|
||||
};
|
||||
|
||||
// 说明:优先保留浏览器实际请求的 origin,用于正常的同源自调用。
|
||||
addCandidate(forwardedProto || "http", host);
|
||||
|
||||
// 说明:某些反代会把 x-forwarded-proto 设成 https,但本机 dev server 实际只监听 http。
|
||||
// 这里补一个 http 候选,避免 SSR 自请求被错误协议直接打挂。
|
||||
if ((forwardedProto || "").trim().toLowerCase() === "https") {
|
||||
addCandidate("http", host);
|
||||
}
|
||||
|
||||
// 说明:localhost / 0.0.0.0 / ::1 这类本机地址在 Node 侧自调用时最容易踩解析差异,
|
||||
// 统一补 127.0.0.1 与 localhost 两个稳定候选,避免 fetch 因回环地址选择失败。
|
||||
if (isLocalHostname(hostname)) {
|
||||
const portSuffix = port ? `:${port}` : "";
|
||||
addCandidate("http", `127.0.0.1${portSuffix}`);
|
||||
addCandidate("http", `localhost${portSuffix}`);
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
}
|
||||
|
||||
function copyHeaderIfPresent(target: Headers, source: Headers, name: string) {
|
||||
@@ -31,19 +89,21 @@ function copyHeaderIfPresent(target: Headers, source: Headers, name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDocumentMetaResponse(url: URL, headersToSend: Headers): Promise<Response> {
|
||||
return await fetch(url, {
|
||||
method: "GET",
|
||||
headers: headersToSend,
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchDocumentMetaViaBridge<T>(input: {
|
||||
documentId: string;
|
||||
workspaceId?: string | null;
|
||||
}): Promise<DocumentMetaResponse<T> | null> {
|
||||
const headerList = await headers();
|
||||
const requestHeaders = new Headers();
|
||||
const origin = getServerRequestOrigin(headerList);
|
||||
const url = new URL("/api/documents/meta", origin);
|
||||
|
||||
url.searchParams.set("documentId", input.documentId);
|
||||
if (input.workspaceId?.trim()) {
|
||||
url.searchParams.set("workspaceId", input.workspaceId.trim());
|
||||
}
|
||||
const originCandidates = buildServerRequestOriginCandidates(headerList);
|
||||
|
||||
copyHeaderIfPresent(requestHeaders, headerList, "cookie");
|
||||
copyHeaderIfPresent(requestHeaders, headerList, "authorization");
|
||||
@@ -54,11 +114,34 @@ export async function fetchDocumentMetaViaBridge<T>(input: {
|
||||
copyHeaderIfPresent(requestHeaders, headerList, "x-source-client");
|
||||
copyHeaderIfPresent(requestHeaders, headerList, "user-agent");
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: requestHeaders,
|
||||
cache: "no-store",
|
||||
});
|
||||
let response: Response | null = null;
|
||||
let lastError: unknown = null;
|
||||
const errorMessages: string[] = [];
|
||||
|
||||
for (const origin of originCandidates) {
|
||||
const url = new URL("/api/documents/meta", origin);
|
||||
url.searchParams.set("documentId", input.documentId);
|
||||
if (input.workspaceId?.trim()) {
|
||||
url.searchParams.set("workspaceId", input.workspaceId.trim());
|
||||
}
|
||||
|
||||
try {
|
||||
response = await fetchDocumentMetaResponse(url, requestHeaders);
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
errorMessages.push(`${url.toString()} => ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
const message =
|
||||
errorMessages.length > 0
|
||||
? `bridge 自请求失败:${errorMessages.join(" | ")}`
|
||||
: "bridge 自请求失败:未生成可用的请求地址";
|
||||
throw new Error(message, { cause: lastError instanceof Error ? lastError : undefined });
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
|
||||
@@ -123,6 +123,9 @@ const DOCUMENT_BRIDGE_MUTATION_FUNCTIONS = {
|
||||
"documents.restore": "documents:restore",
|
||||
"documents.duplicate": "documents:duplicateWithMindmaps",
|
||||
"documents.copy_tree": "documents:copyTree",
|
||||
"documents.template": "documents:setTemplate",
|
||||
"documents.emptyTrashByWorkspace": "documents:emptyTrashByWorkspace",
|
||||
"documents.purge": "documents:purge",
|
||||
"blocks.patch": "documents:updateContent",
|
||||
"blocks.move": "documents:updateContent",
|
||||
"blocks.embed": "documents:updateContent",
|
||||
@@ -130,6 +133,10 @@ const DOCUMENT_BRIDGE_MUTATION_FUNCTIONS = {
|
||||
"documents.stats.update": "documents:updateStats",
|
||||
"documents.options.update": "documents:updateOptions",
|
||||
"documents.save": "documents:updateContent",
|
||||
"mindmaps.delete": "mindmaps:softDelete",
|
||||
"mindmaps.restore": "mindmaps:restore",
|
||||
"mindmaps.purge": "mindmaps:purge",
|
||||
"mindmaps.emptyTrashByWorkspace": "mindmaps:emptyTrashByWorkspace",
|
||||
"media.assets.replace_storage": "mediaAssets:replaceStorageFromUpload",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
executeRustBridgeMutationTransport,
|
||||
resolveRustBridgeCommandPlan,
|
||||
} from "@/lib/documents/rust-runtime";
|
||||
import { executeSaveBridgeCommand } from "@/lib/documents/save-command-adapter";
|
||||
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
|
||||
import type { Json } from "@/types/supabase";
|
||||
|
||||
export type DocumentCreatePayload = {
|
||||
@@ -64,6 +66,19 @@ export type DocumentCopyTreePayload = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type DocumentTemplatePayload = {
|
||||
documentId: string;
|
||||
isTemplate: boolean;
|
||||
};
|
||||
|
||||
export type DocumentEmptyTrashPayload = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type DocumentPurgePayload = {
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export type PageCommandExecutionResult<TResult> = {
|
||||
requestId: string;
|
||||
traceId: string;
|
||||
@@ -103,52 +118,37 @@ async function buildRuntimeContext(request: Request, workspaceId: string | null)
|
||||
});
|
||||
}
|
||||
|
||||
async function recordLifecycleArtifacts<T>(input: {
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<T>;
|
||||
client: ConvexHttpClient;
|
||||
}) {
|
||||
await recordBridgeCommandArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
client: input.client,
|
||||
});
|
||||
}
|
||||
|
||||
async function recordLifecycleFailureArtifacts<T>(input: {
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<T>;
|
||||
client: ConvexHttpClient;
|
||||
error: unknown;
|
||||
}) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
client: input.client,
|
||||
error: input.error,
|
||||
});
|
||||
}
|
||||
|
||||
export async function executePageLifecycleBridgeCommand<TPayload, TResult>(input: {
|
||||
context: BridgeContext;
|
||||
envelope: CommandEnvelope<TPayload>;
|
||||
client?: ConvexHttpClient;
|
||||
}): Promise<PageCommandExecutionResult<TResult>> {
|
||||
const client = input.client ?? (await getAuthedConvexClient()).client;
|
||||
const plan = await resolveRustBridgeCommandPlan({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
});
|
||||
const result = await executeRustBridgeMutationTransport<TResult>({
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
const plan = await resolveRustBridgeCommandPlan({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
});
|
||||
result = await executeRustBridgeMutationTransport<TResult>({
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
|
||||
await recordBridgeCommandArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
client,
|
||||
});
|
||||
await recordBridgeCommandArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
client,
|
||||
});
|
||||
} catch (error) {
|
||||
await recordBridgeCommandFailureArtifacts({
|
||||
context: input.context,
|
||||
envelope: input.envelope,
|
||||
client,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
requestId: input.context.requestId,
|
||||
@@ -200,7 +200,7 @@ export async function executeDocumentCreateChildBridgeCommand(request: Request):
|
||||
const pageId = safeRandomId();
|
||||
const context = await buildRuntimeContext(request, workspaceId);
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.createChild",
|
||||
name: "documents.create",
|
||||
payload: {
|
||||
documentId: pageId,
|
||||
workspaceId,
|
||||
@@ -216,40 +216,23 @@ export async function executeDocumentCreateChildBridgeCommand(request: Request):
|
||||
},
|
||||
});
|
||||
|
||||
let created;
|
||||
try {
|
||||
created = await client.mutation(api.documents.create, {
|
||||
id: pageId,
|
||||
workspaceId,
|
||||
parentId,
|
||||
title: resolvedTitle,
|
||||
accessScope,
|
||||
content: contentPayload,
|
||||
});
|
||||
|
||||
await recordLifecycleArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
} catch (error) {
|
||||
await recordLifecycleFailureArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const created = await executePageLifecycleBridgeCommand<DocumentCreatePayload, {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
}>({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
pageId: created.id,
|
||||
title: created.title ?? resolvedTitle,
|
||||
pageId: created.result.id,
|
||||
title: created.result.title ?? resolvedTitle,
|
||||
meta: {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
requestId: created.requestId,
|
||||
traceId: created.traceId,
|
||||
commandId: created.commandId,
|
||||
commandName: created.commandName,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -304,12 +287,23 @@ export async function executeDocumentEmbedBridgeCommand(request: Request): Promi
|
||||
const payload: Json = composeContentWithBlocks(targetContent.content, nextBlocks);
|
||||
const workspaceId = normalizeWorkspaceId(sourceDoc.workspace_id) ?? normalizeWorkspaceId((targetMeta as { workspace_id?: string | null } | null)?.workspace_id);
|
||||
const context = await buildRuntimeContext(request, workspaceId);
|
||||
const savePayload = buildDocumentSavePayload({
|
||||
documentId: normalizedTargetId,
|
||||
workspaceId,
|
||||
revision:
|
||||
typeof targetContent.revision === "number" && Number.isInteger(targetContent.revision)
|
||||
? targetContent.revision
|
||||
: null,
|
||||
content: payload,
|
||||
conflictDetectionKey:
|
||||
typeof targetContent.conflict_detection_key === "string"
|
||||
? targetContent.conflict_detection_key
|
||||
: null,
|
||||
blockCount: nextBlocks.length,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "documents.embed",
|
||||
payload: {
|
||||
sourceId: normalizedSourceId,
|
||||
targetId: normalizedTargetId,
|
||||
},
|
||||
name: "documents.save",
|
||||
payload: savePayload,
|
||||
context,
|
||||
target: {
|
||||
workspaceId,
|
||||
@@ -317,34 +311,18 @@ export async function executeDocumentEmbedBridgeCommand(request: Request): Promi
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await client.mutation(api.documents.updateContent, {
|
||||
id: normalizedTargetId,
|
||||
content: payload,
|
||||
});
|
||||
|
||||
await recordLifecycleArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
} catch (error) {
|
||||
await recordLifecycleFailureArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const result = await executeSaveBridgeCommand({
|
||||
context,
|
||||
envelope,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
meta: {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
requestId: result.requestId,
|
||||
traceId: result.traceId,
|
||||
commandId: result.commandId,
|
||||
commandName: result.commandName,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -384,34 +362,22 @@ export async function executeDocumentTemplateBridgeCommand(request: Request): Pr
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await client.mutation(api.documents.setTemplate, {
|
||||
id: normalizedDocumentId,
|
||||
isTemplate: payload.isTemplate,
|
||||
});
|
||||
|
||||
await recordLifecycleArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
} catch (error) {
|
||||
await recordLifecycleFailureArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const result = await executePageLifecycleBridgeCommand<DocumentTemplatePayload, {
|
||||
ok?: boolean;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
meta: {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
requestId: result.requestId,
|
||||
traceId: result.traceId,
|
||||
commandId: result.commandId,
|
||||
commandName: result.commandName,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -442,31 +408,23 @@ export async function executeDocumentEmptyTrashBridgeCommand(request: Request):
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await client.mutation(api.documents.emptyTrashByWorkspace, { workspaceId });
|
||||
|
||||
await recordLifecycleArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
} catch (error) {
|
||||
await recordLifecycleFailureArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const result = await executePageLifecycleBridgeCommand<DocumentEmptyTrashPayload, {
|
||||
ok?: boolean;
|
||||
deletedCount?: number;
|
||||
}>({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
removed: typeof result.result?.deletedCount === "number" ? result.result.deletedCount : 0,
|
||||
meta: {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
requestId: result.requestId,
|
||||
traceId: result.traceId,
|
||||
commandId: result.commandId,
|
||||
commandName: result.commandName,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -501,31 +459,24 @@ export async function executeDocumentPurgeBridgeCommand(request: Request): Promi
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await client.mutation(api.documents.purge, { id: normalizedDocumentId });
|
||||
|
||||
await recordLifecycleArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
} catch (error) {
|
||||
await recordLifecycleFailureArtifacts({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const result = await executePageLifecycleBridgeCommand<DocumentPurgePayload, {
|
||||
ok?: boolean;
|
||||
purged?: boolean;
|
||||
purged_at?: string | null;
|
||||
}>({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
purged: result.result?.purged ?? true,
|
||||
meta: {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
commandId: envelope.commandId,
|
||||
commandName: envelope.name,
|
||||
requestId: result.requestId,
|
||||
traceId: result.traceId,
|
||||
commandId: result.commandId,
|
||||
commandName: result.commandName,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -606,6 +606,19 @@ export async function executeRustBridgeMutationTransport<TResult>(input: {
|
||||
newId: assertStringArg(input.plan.argsJson, "newId"),
|
||||
title: readOptionalStringArg(input.plan.argsJson, "title"),
|
||||
});
|
||||
case "documents:setTemplate":
|
||||
return mutation(api.documents.setTemplate, {
|
||||
id: assertStringArg(input.plan.argsJson, "id"),
|
||||
isTemplate: Boolean(input.plan.argsJson.isTemplate),
|
||||
});
|
||||
case "documents:emptyTrashByWorkspace":
|
||||
return mutation(api.documents.emptyTrashByWorkspace, {
|
||||
workspaceId: assertStringArg(input.plan.argsJson, "workspaceId"),
|
||||
});
|
||||
case "documents:purge":
|
||||
return mutation(api.documents.purge, {
|
||||
id: assertStringArg(input.plan.argsJson, "id"),
|
||||
});
|
||||
case "documents:updateTitle":
|
||||
return mutation(api.documents.updateTitle, {
|
||||
id: assertStringArg(input.plan.argsJson, "id"),
|
||||
@@ -628,6 +641,25 @@ export async function executeRustBridgeMutationTransport<TResult>(input: {
|
||||
? input.plan.argsJson.createOnly
|
||||
: undefined,
|
||||
});
|
||||
case "mindmaps:softDelete":
|
||||
return mutation(api.mindmaps.softDelete, {
|
||||
docId: assertStringArg(input.plan.argsJson, "docId"),
|
||||
mindmapId: assertStringArg(input.plan.argsJson, "mindmapId"),
|
||||
});
|
||||
case "mindmaps:restore":
|
||||
return mutation(api.mindmaps.restore, {
|
||||
docId: assertStringArg(input.plan.argsJson, "docId"),
|
||||
mindmapId: assertStringArg(input.plan.argsJson, "mindmapId"),
|
||||
});
|
||||
case "mindmaps:purge":
|
||||
return mutation(api.mindmaps.purge, {
|
||||
docId: assertStringArg(input.plan.argsJson, "docId"),
|
||||
mindmapId: assertStringArg(input.plan.argsJson, "mindmapId"),
|
||||
});
|
||||
case "mindmaps:emptyTrashByWorkspace":
|
||||
return mutation(api.mindmaps.emptyTrashByWorkspace, {
|
||||
workspaceId: assertStringArg(input.plan.argsJson, "workspaceId"),
|
||||
});
|
||||
default:
|
||||
throw new DocumentBridgeError(
|
||||
`未注册的 Rust mutation transport: ${input.plan.functionName}`,
|
||||
|
||||
Reference in New Issue
Block a user