4-26 树rust-2

This commit is contained in:
lix-2026
2026-04-26 04:29:23 +08:00
parent 94631f3636
commit 338bb2e20f
58 changed files with 11718 additions and 1256 deletions
@@ -23,6 +23,7 @@ export async function recordBridgeCommandArtifacts<T>(input: {
context: BridgeContext;
envelope: CommandEnvelope<T>;
client?: ConvexHttpClient;
commandPayload?: unknown;
status?: BridgeCommandLogStatus;
eventStatus?: BridgeDomainEventStatus;
error?: string | null;
@@ -35,7 +36,7 @@ export async function recordBridgeCommandArtifacts<T>(input: {
const commandLogId = `clog_${input.envelope.commandId}`;
const eventId = `evt_${input.envelope.commandId}`;
const now = input.now ?? new Date().toISOString();
const payload = input.envelope.payload as Record<string, unknown>;
const payload = input.commandPayload ?? input.envelope.payload;
const status = input.status ?? "succeeded";
const eventStatus =
input.eventStatus ??
@@ -70,6 +70,7 @@ export type CommandEnvelope<T> = {
source: BridgeSource;
target: BridgeTarget | null;
payload: T;
preflightData?: Record<string, unknown> | null;
reason: string | null;
refs: string[];
dryRun: boolean;
@@ -275,6 +276,7 @@ export function buildDocumentCommandEnvelope<T>(input: {
payload: T;
context: BridgeContext;
target?: BridgeTarget | null;
preflightData?: Record<string, unknown> | null;
reason?: string | null;
refs?: string[];
}): CommandEnvelope<T> {
@@ -286,6 +288,7 @@ export function buildDocumentCommandEnvelope<T>(input: {
source: input.context.source,
target: input.target ?? null,
payload: input.payload,
preflightData: input.preflightData ?? null,
reason: input.reason ?? null,
refs: input.refs ?? [],
dryRun: input.context.dryRun,
@@ -3,6 +3,7 @@ import {
compareDocumentCanonicalOrder,
getCanonicalDocumentByBusinessId,
getCanonicalParentDocumentId,
pickCanonicalDocumentRecordsByBusinessId,
pickCanonicalDocumentRecord,
} from "../../../convex/_utils/documentRecord";
@@ -176,3 +177,37 @@ describe("canonical document helper", () => {
expect(parentId).toBe("parent_alive");
});
});
describe("pickCanonicalDocumentRecordsByBusinessId", () => {
it("同一 workspace 扫描结果里 business id 重复时应先折叠成 canonical 记录,供树命令写链复用", () => {
const records = pickCanonicalDocumentRecordsByBusinessId([
{
_id: "doc_old",
id: "doc_1",
parent_id: "parent_old",
deleted_at: null,
created_at: "2026-04-14T00:00:00.000Z",
updated_at: "2026-04-14T00:00:01.000Z",
},
{
_id: "doc_new",
id: "doc_1",
parent_id: "parent_new",
deleted_at: null,
created_at: "2026-04-14T00:00:02.000Z",
updated_at: "2026-04-14T00:00:03.000Z",
},
{
_id: "doc_2",
id: "doc_2",
parent_id: null,
deleted_at: null,
created_at: "2026-04-14T00:00:04.000Z",
updated_at: "2026-04-14T00:00:05.000Z",
},
]);
expect(records).toHaveLength(2);
expect(records.map((record) => record._id)).toEqual(["doc_new", "doc_2"]);
});
});
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { buildParentById, isAncestorOf } from "../../../convex/_utils/documentTree";
describe("document tree helper", () => {
it("构建父链映射时保留页面与父页面关系,供树命令 legality 复用", () => {
const parentById = buildParentById([
{ id: "root", parent_id: null },
{ id: "child", parent_id: "root" },
{ id: "leaf", parent_id: "child" },
]);
expect(parentById.get("root")).toBeNull();
expect(parentById.get("child")).toBe("root");
expect(parentById.get("leaf")).toBe("child");
});
it("祖先判断应能识别多级后代,避免把页面移动到自己的子树下面", () => {
const parentById = buildParentById([
{ id: "root", parent_id: null },
{ id: "child", parent_id: "root" },
{ id: "leaf", parent_id: "child" },
]);
expect(isAncestorOf("root", "leaf", parentById)).toBe(true);
expect(isAncestorOf("child", "leaf", parentById)).toBe(true);
expect(isAncestorOf("leaf", "root", parentById)).toBe(false);
expect(isAncestorOf("missing", "leaf", parentById)).toBe(false);
});
});
@@ -1,5 +1,18 @@
import { describe, expect, it, vi } from "vitest";
import { buildParentById } from "@/lib/file-tree/dnd";
import { getAuthedConvexClient } from "@/lib/convex/route";
import {
buildDocumentBridgeContextWithActor,
buildDocumentCommandEnvelope,
documentBridgeErrorResponse,
} from "@/lib/documents/bridge";
import {
recordBridgeCommandFailureArtifacts,
} from "@/lib/documents/bridge-log";
import {
executeRustBridgeMutationTransport,
resolveRustBridgeCommandPlan,
} from "@/lib/documents/rust-runtime";
vi.mock("next/server", () => ({
NextResponse: {
@@ -42,6 +55,7 @@ vi.mock("@/lib/server/local-paths", () => ({
}));
const {
handleDocumentMoveRequest,
normalizeDocumentCopyTreePayload,
normalizeDocumentMovePayload,
resolveSubtreeMoveLegality,
@@ -128,4 +142,244 @@ describe("page-lifecycle-command-adapter", () => {
isInvalid: false,
});
});
it("documents.move 应把 movePreflight 透传给 Rust plan", async () => {
const client = {
query: vi.fn(async (name: string, args: { id: string }) => {
if (name !== "documents:getMeta") {
return null;
}
if (args.id === "doc_1") {
return {
id: "doc_1",
workspace_id: "ws_1",
parent_id: null,
};
}
if (args.id === "child_1") {
return {
id: "child_1",
workspace_id: "ws_1",
parent_id: "doc_1",
};
}
return null;
}),
mutation: vi.fn(),
};
vi.mocked(getAuthedConvexClient).mockResolvedValue({
auth: {
userId: "user_1",
email: "dev@example.com",
name: "开发用户",
},
client: client as never,
});
vi.mocked(buildDocumentBridgeContextWithActor).mockReturnValue({
deploymentId: null,
projectId: null,
workspaceId: "ws_1",
requestId: "req_move_1",
traceId: "trace_move_1",
actor: {
actorType: "user",
actorId: "user_1",
sessionId: null,
},
source: {
channel: "next-route",
client: "wolai-frontend",
},
tenantId: null,
authToken: null,
idempotencyKey: null,
validateOnly: false,
dryRun: false,
});
vi.mocked(buildDocumentCommandEnvelope).mockImplementation((input: unknown) => input as never);
vi.mocked(resolveRustBridgeCommandPlan).mockResolvedValue({
kind: "command",
commandName: "documents.move",
commandId: "cmd_move_1",
functionName: "documents:move",
workspaceId: "ws_1",
requestId: "req_move_1",
traceId: "trace_move_1",
actorId: "user_1",
idempotencyKey: null,
payloadJson: "{\"kind\":\"command\"}",
argsJson: {
id: "doc_1",
parentId: "child_1",
sortOrder: 0,
},
});
vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({ ok: true } as never);
const response = await handleDocumentMoveRequest(
new Request("http://127.0.0.1:3000/api/documents/move", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
documentId: "doc_1",
parentId: "child_1",
position: 0,
}),
}),
);
expect(response.status).toBe(200);
expect(resolveRustBridgeCommandPlan).toHaveBeenCalledWith({
context: expect.objectContaining({
requestId: "req_move_1",
}),
envelope: expect.objectContaining({
name: "documents.move",
preflightData: {
sourceDocument: {
id: "doc_1",
workspaceId: "ws_1",
parentId: null,
},
targetParentDocument: {
id: "child_1",
workspaceId: "ws_1",
parentId: "doc_1",
},
targetAncestorIds: ["doc_1"],
},
payload: {
documentId: "doc_1",
parentId: "child_1",
sortOrder: 0,
movePreflight: {
sourceDocument: {
id: "doc_1",
workspaceId: "ws_1",
parentId: null,
},
targetParentDocument: {
id: "child_1",
workspaceId: "ws_1",
parentId: "doc_1",
},
targetAncestorIds: ["doc_1"],
},
},
}),
});
});
it("documents.move 失败时记录的 failure artifact 仍应保持 move envelope", async () => {
const client = {
query: vi.fn(async (name: string, args: { id: string }) => {
if (name !== "documents:getMeta") {
return null;
}
if (args.id === "doc_1") {
return {
id: "doc_1",
workspace_id: "ws_1",
parent_id: null,
};
}
if (args.id === "child_1") {
return {
id: "child_1",
workspace_id: "ws_1",
parent_id: "doc_1",
};
}
return null;
}),
mutation: vi.fn(),
};
vi.mocked(getAuthedConvexClient).mockResolvedValue({
auth: {
userId: "user_1",
email: "dev@example.com",
name: "开发用户",
},
client: client as never,
});
vi.mocked(buildDocumentBridgeContextWithActor).mockReturnValue({
deploymentId: null,
projectId: null,
workspaceId: "ws_1",
requestId: "req_move_2",
traceId: "trace_move_2",
actor: {
actorType: "user",
actorId: "user_1",
sessionId: null,
},
source: {
channel: "next-route",
client: "wolai-frontend",
},
tenantId: null,
authToken: null,
idempotencyKey: null,
validateOnly: false,
dryRun: false,
});
vi.mocked(buildDocumentCommandEnvelope).mockImplementation((input: unknown) => input as never);
vi.mocked(resolveRustBridgeCommandPlan).mockRejectedValue(new Error("move failed"));
vi.mocked(documentBridgeErrorResponse).mockImplementation((error: unknown) => ({
body: { error: error instanceof Error ? error.message : String(error) },
status: 500,
}) as never);
const response = await handleDocumentMoveRequest(
new Request("http://127.0.0.1:3000/api/documents/move", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
documentId: "doc_1",
parentId: "child_1",
position: 0,
}),
}),
);
expect(response.status).toBe(500);
expect(recordBridgeCommandFailureArtifacts).toHaveBeenCalledWith(
expect.objectContaining({
envelope: expect.objectContaining({
name: "documents.move",
preflightData: {
sourceDocument: {
id: "doc_1",
workspaceId: "ws_1",
parentId: null,
},
targetParentDocument: {
id: "child_1",
workspaceId: "ws_1",
parentId: "doc_1",
},
targetAncestorIds: ["doc_1"],
},
payload: {
documentId: "doc_1",
parentId: "child_1",
sortOrder: 0,
movePreflight: {
sourceDocument: {
id: "doc_1",
workspaceId: "ws_1",
parentId: null,
},
targetParentDocument: {
id: "child_1",
workspaceId: "ws_1",
parentId: "doc_1",
},
targetAncestorIds: ["doc_1"],
},
},
}),
}),
);
});
});
@@ -29,6 +29,18 @@ type MovePayload = {
position?: number | null;
};
type MovePreflightDocument = {
id: string;
workspaceId: string | null;
parentId: string | null;
};
type MovePreflightPayload = {
sourceDocument: MovePreflightDocument;
targetParentDocument: MovePreflightDocument | null;
targetAncestorIds: string[];
};
type DeletePayload = {
documentId?: string | null;
};
@@ -136,6 +148,41 @@ export function resolveSubtreeMoveLegality(input: {
};
}
async function buildMovePreflight(args: {
client: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"];
sourceDocument: MovePreflightDocument;
targetParentId: string | null;
}) : Promise<MovePreflightPayload> {
let targetParentDocument: MovePreflightDocument | null = null;
const targetAncestorIds: string[] = [];
if (args.targetParentId) {
const targetParentDoc = await args.client.query(api.documents.getMeta, { id: args.targetParentId });
if (!targetParentDoc) {
throw new Error("目标父页面不存在或无权限");
}
targetParentDocument = {
id: targetParentDoc.id,
workspaceId: trimOrNull(targetParentDoc.workspace_id),
parentId: trimOrNull(targetParentDoc.parent_id),
};
let cursor = trimOrNull(targetParentDoc.parent_id);
let depth = 0;
while (cursor && depth < 256) {
targetAncestorIds.push(cursor);
const parentDoc = await args.client.query(api.documents.getMeta, { id: cursor });
if (!parentDoc) break;
cursor = trimOrNull(parentDoc.parent_id);
depth += 1;
}
}
return {
sourceDocument: args.sourceDocument,
targetParentDocument,
targetAncestorIds,
};
}
function safeRandomId() {
return typeof crypto.randomUUID === "function"
? crypto.randomUUID()
@@ -328,28 +375,47 @@ export async function handleDocumentCreateRequest(request: Request): Promise<Nex
export async function handleDocumentMoveRequest(request: Request): Promise<NextResponse> {
assertServerEnvironment();
const requestClone = request.clone();
let normalizedMove: NormalizedDocumentMovePayload | null = null;
let failureClient: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"] | null = null;
let failureAuthUserId: string | null = null;
let failureSourceDocument: MovePreflightDocument | null = null;
try {
const payload = (await request.json()) as MovePayload;
const normalizedMove = normalizeDocumentMovePayload(payload);
normalizedMove = normalizeDocumentMovePayload(payload);
const documentId = normalizedMove.documentId;
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const { auth, client } = await getAuthedConvexClient();
failureClient = client;
failureAuthUserId = auth.userId;
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
failureSourceDocument = {
id: sourceDoc.id,
workspaceId: trimOrNull(sourceDoc.workspace_id),
parentId: trimOrNull(sourceDoc.parent_id),
};
const context = await buildBridgeContext(request, sourceDoc.workspace_id ?? null, auth.userId);
const movePreflight = await buildMovePreflight({
client,
sourceDocument: failureSourceDocument,
targetParentId: normalizedMove.parentId,
});
const envelope = buildDocumentCommandEnvelope({
name: "documents.move",
payload: {
documentId,
parentId: normalizedMove.parentId,
sortOrder: normalizedMove.sortOrder,
movePreflight,
},
preflightData: movePreflight,
context,
target: {
pageId: documentId,
@@ -373,18 +439,49 @@ export async function handleDocumentMoveRequest(request: Request): Promise<NextR
return NextResponse.json({ ok: true });
} catch (error) {
try {
const payload = (await request.clone().json().catch(() => ({}))) as DeletePayload;
const documentId = trimOrNull(payload.documentId);
const fallbackMove = normalizedMove
?? normalizeDocumentMovePayload(
(await requestClone.json().catch(() => ({}))) as MovePayload,
);
const documentId = fallbackMove.documentId;
if (documentId) {
const { auth, client } = await getAuthedConvexClient();
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
const context = await buildBridgeContext(request, sourceDoc?.workspace_id ?? null, auth.userId);
let client = failureClient;
let authUserId = failureAuthUserId;
if (!client || !authUserId) {
const authedClient = await getAuthedConvexClient();
client = authedClient.client;
authUserId = authedClient.auth.userId;
}
let sourceDocument = failureSourceDocument;
if (!sourceDocument) {
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
throw error;
}
sourceDocument = {
id: sourceDoc.id,
workspaceId: trimOrNull(sourceDoc.workspace_id),
parentId: trimOrNull(sourceDoc.parent_id),
};
}
const context = await buildBridgeContext(request, sourceDocument.workspaceId ?? null, authUserId);
const movePreflight = await buildMovePreflight({
client,
sourceDocument,
targetParentId: fallbackMove.parentId,
});
const envelope = buildDocumentCommandEnvelope({
name: "documents.delete",
payload: { documentId },
name: "documents.move",
payload: {
documentId,
parentId: fallbackMove.parentId,
sortOrder: fallbackMove.sortOrder,
movePreflight,
},
preflightData: movePreflight,
context,
target: {
workspaceId: sourceDoc?.workspace_id ?? null,
workspaceId: sourceDocument.workspaceId ?? null,
pageId: documentId,
},
});
@@ -69,6 +69,7 @@ const mockContext: BridgeContext = {
describe("page-write-command-adapter", () => {
it("标题命令应走 rust bridge transport", async () => {
const { getAuthedConvexClient } = await import("@/lib/convex/route");
const { recordBridgeCommandArtifacts } = await import("@/lib/documents/bridge-log");
const {
resolveRustBridgeCommandPlan,
executeRustBridgeMutationTransport,
@@ -94,7 +95,10 @@ describe("page-write-command-adapter", () => {
title: "新标题",
},
});
vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({ ok: true });
vi.mocked(executeRustBridgeMutationTransport).mockResolvedValue({
ok: true,
updated_at: "2026-04-24T00:00:00.000Z",
});
const result = await executePageWriteBridgeCommand({
context: mockContext,
@@ -116,6 +120,25 @@ describe("page-write-command-adapter", () => {
name: "page.head.updateTitle",
}),
});
expect(recordBridgeCommandArtifacts).toHaveBeenCalledWith({
context: mockContext,
envelope: expect.objectContaining({
name: "page.head.updateTitle",
}),
commandPayload: {
documentId: "doc_1",
workspaceId: "ws_1",
title: "新标题",
streamDelta: {
op: "upsert_document",
document: {
id: "doc_1",
title: "新标题",
updated_at: "2026-04-24T00:00:00.000Z",
},
},
},
});
expect(result.commandName).toBe("page.head.updateTitle");
expect(result.revision).toBeNull();
expect(result.conflictDetectionKey).toBeNull();
@@ -40,6 +40,26 @@ export type PageWriteCommandExecutionResult = {
conflictDetectionKey: string | null;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function attachStreamDelta(commandPayload: unknown, streamDelta?: Record<string, unknown> | null) {
if (!streamDelta) {
return commandPayload;
}
if (isRecord(commandPayload)) {
return {
...commandPayload,
streamDelta,
};
}
return {
payload: commandPayload,
streamDelta,
};
}
function mapDocumentOptionsToConvexArgs(payload: DocumentOptionsUpdatePayload) {
return {
id: payload.documentId,
@@ -98,6 +118,33 @@ function normalizePersistedMeta(result: unknown): Pick<PageWriteCommandExecution
};
}
function normalizeUpdatedAt(result: unknown): string | null {
const record = isRecord(result) ? result : null;
const updatedAt = record?.updated_at;
return typeof updatedAt === "string" && updatedAt.trim() ? updatedAt.trim() : null;
}
function buildPageWriteCommandPayload<TPayload extends PageWritePayload>(input: {
envelope: CommandEnvelope<TPayload>;
transportResult?: unknown;
}) {
if (input.envelope.name !== "page.head.updateTitle") {
return input.envelope.payload;
}
const payload = input.envelope.payload as DocumentTitleUpdatePayload;
const updatedAt = normalizeUpdatedAt(input.transportResult);
return attachStreamDelta(payload, {
op: "upsert_document",
document: {
id: payload.documentId,
title: payload.title,
...(updatedAt ? { updated_at: updatedAt } : {}),
},
});
}
export async function executePageWriteBridgeCommand<TPayload extends PageWritePayload>(input: {
context: BridgeContext;
envelope: CommandEnvelope<TPayload>;
@@ -120,6 +167,10 @@ export async function executePageWriteBridgeCommand<TPayload extends PageWritePa
await recordBridgeCommandArtifacts({
context: input.context,
envelope: input.envelope,
commandPayload: buildPageWriteCommandPayload({
envelope: input.envelope,
transportResult,
}),
});
return {
@@ -479,7 +479,10 @@ export async function resolveRustBridgeCommandPlan<TPayload>(input: {
const response = await runRustRuntime({
kind: "command",
context: input.context,
command: input.envelope,
command: {
...input.envelope,
preflightData: input.envelope.preflightData ?? null,
},
});
if (!("plan" in response) || response.plan.kind !== "command") {
@@ -44,11 +44,11 @@ describe("tree-command-client", () => {
"/api/tree/commands",
"/api/tree/commands",
"/api/tree/commands",
"/api/documents/delete",
"/api/documents/restore",
"/api/documents/purge",
"/api/documents/embed",
"/api/documents/copy-tree",
"/api/tree/commands",
"/api/tree/commands",
"/api/tree/commands",
"/api/tree/commands",
"/api/tree/commands",
"/api/documents/title",
"/api/documents/options",
]);
@@ -70,6 +70,30 @@ describe("tree-command-client", () => {
sortOrder: 0,
workspaceId: null,
});
expect(JSON.parse(String(fetchMock.mock.calls[3]?.[1]?.body))).toEqual({
action: "archive",
documentId: "doc_1",
workspaceId: null,
});
expect(JSON.parse(String(fetchMock.mock.calls[4]?.[1]?.body))).toEqual({
action: "restore",
documentId: "doc_1",
workspaceId: null,
});
expect(JSON.parse(String(fetchMock.mock.calls[5]?.[1]?.body))).toEqual({
action: "purge",
documentId: "doc_1",
});
expect(JSON.parse(String(fetchMock.mock.calls[6]?.[1]?.body))).toEqual({
action: "embed",
sourceId: "doc_1",
targetId: "doc_2",
});
expect(JSON.parse(String(fetchMock.mock.calls[7]?.[1]?.body))).toEqual({
action: "copy",
targetParentId: null,
items: [{ documentId: "doc_1", recursive: true }],
});
});
it("在后端返回错误时抛出统一异常", async () => {
@@ -96,7 +96,15 @@ type MoveDocumentInput = {
workspaceId?: string | null;
};
type TreeCommandAction = "create" | "rename" | "move";
type TreeCommandAction =
| "create"
| "rename"
| "move"
| "archive"
| "restore"
| "purge"
| "embed"
| "copy";
type DeleteDocumentInput = {
documentId: string;
@@ -161,12 +169,16 @@ type TreeCommandResponse = {
title?: string | null;
sortOrder?: number | null;
updatedAt?: string | null;
execution?: {
access_scope?: "private" | "shared" | "public";
is_template?: boolean;
created_at?: string | null;
updated_at?: string | null;
} | null;
items?: Array<{ oldId: string; newId: string }>;
execution?:
| ({
access_scope?: "private" | "shared" | "public";
is_template?: boolean;
created_at?: string | null;
updated_at?: string | null;
purged?: boolean;
} & Record<string, unknown>)
| null;
} | null;
};
@@ -275,52 +287,92 @@ export async function moveDocumentCommand(input: MoveDocumentInput): Promise<{ o
export async function deleteDocumentCommand(
input: DeleteDocumentInput,
): Promise<{ success: true; meta?: DocumentCommandMeta }> {
return postDocumentCommand<{ success: true; meta?: DocumentCommandMeta }>(
"/api/documents/delete",
const response = await postTreeCommand<TreeCommandResponse>(
{
action: "archive",
documentId: input.documentId,
workspaceId: input.workspaceId ?? null,
},
"删除失败,请稍后再试",
);
return {
success: true,
meta: {
requestId: response.requestId,
traceId: response.traceId,
commandName: TREE_COMMAND_PROTOCOL.archive.preferredCommandName,
},
};
}
export async function restoreDocumentCommand(
input: RestoreDocumentInput,
): Promise<{ success: true; meta?: DocumentCommandMeta }> {
return postDocumentCommand<{ success: true; meta?: DocumentCommandMeta }>(
"/api/documents/restore",
const response = await postTreeCommand<TreeCommandResponse>(
{
action: "restore",
documentId: input.documentId,
workspaceId: input.workspaceId ?? null,
},
"恢复失败,请稍后再试",
);
return {
success: true,
meta: {
requestId: response.requestId,
traceId: response.traceId,
commandName: TREE_COMMAND_PROTOCOL.restore.preferredCommandName,
},
};
}
export async function purgeDocumentCommand(
input: PurgeDocumentInput,
): Promise<{ success: true; purged?: boolean; meta?: DocumentCommandMeta }> {
return postDocumentCommand<{ success: true; purged?: boolean; meta?: DocumentCommandMeta }>(
"/api/documents/purge",
const response = await postTreeCommand<TreeCommandResponse>(
{
action: "purge",
documentId: input.documentId,
},
"彻底删除失败,请稍后再试",
);
return {
success: true,
purged:
typeof response.result?.execution?.purged === "boolean"
? response.result.execution.purged
: undefined,
meta: {
requestId: response.requestId,
traceId: response.traceId,
commandName: TREE_COMMAND_PROTOCOL.purge.preferredCommandName,
},
};
}
export async function embedDocumentCommand(
input: EmbedDocumentInput,
): Promise<{ ok: true; meta?: DocumentCommandMeta }> {
return postDocumentCommand<{ ok: true; meta?: DocumentCommandMeta }>(
"/api/documents/embed",
const response = await postTreeCommand<TreeCommandResponse>(
{
action: "embed",
sourceId: input.sourceId,
targetId: input.targetId,
},
"嵌入失败,请稍后再试",
);
return {
ok: true,
meta: {
requestId: response.requestId,
traceId: response.traceId,
commandName: TREE_COMMAND_PROTOCOL.embed.preferredCommandName,
},
};
}
export async function copyTreeCommand(
@@ -329,15 +381,21 @@ export async function copyTreeCommand(
items: Array<{ oldId: string; newId: string }>;
meta?: DocumentCommandMeta;
}> {
return postDocumentCommand<{
items: Array<{ oldId: string; newId: string }>;
meta?: DocumentCommandMeta;
}>(
"/api/documents/copy-tree",
const response = await postTreeCommand<TreeCommandResponse>(
{
action: "copy",
targetParentId: input.targetParentId,
items: input.items,
},
"复制页面失败,请稍后再试",
);
return {
items: response.result?.items ?? [],
meta: {
requestId: response.requestId,
traceId: response.traceId,
commandName: TREE_COMMAND_PROTOCOL.copy.preferredCommandName,
},
};
}