feat: cut over rust web main shell

This commit is contained in:
lix-2026
2026-04-29 12:24:44 +08:00
parent 7965c6c107
commit 048fe28a4d
97 changed files with 9396 additions and 1263 deletions
@@ -1,94 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type CopyTreeItem = {
documentId: string;
recursive: boolean;
};
type CopyTreePayload = {
items?: CopyTreeItem[] | null;
targetParentId?: string | null;
};
type TreeCopyResponse = {
requestId?: string;
traceId?: string;
result?: {
items?: Array<{
oldId: string;
newId: string;
}>;
} | null;
};
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const payload = (await request.json()) as CopyTreePayload;
const normalizedItems = (payload.items ?? [])
.filter((item) => item?.documentId)
.map((item) => ({
documentId: item.documentId.trim(),
recursive: Boolean(item.recursive),
}))
.filter((item) => item.documentId.length > 0);
if (normalizedItems.length === 0) {
return NextResponse.json({ error: "items 为空" }, { status: 400 });
}
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "copy",
targetParentId:
typeof payload.targetParentId === "string" ? payload.targetParentId.trim() || null : null,
items: normalizedItems,
}),
});
const result = (await response.json().catch(() => null)) as
| TreeCopyResponse
| { error?: string }
| null;
if (!response.ok) {
return NextResponse.json(
{
error:
result && typeof result === "object" && "error" in result && typeof result.error === "string"
? result.error
: "复制页面失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
items: result?.result?.items ?? [],
meta: {
requestId: result?.requestId,
traceId: result?.traceId,
commandName: "tree.subtree.copy",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "复制页面失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -1,83 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type TreeCreateResponse = {
requestId?: string;
traceId?: string;
result?: {
documentId?: string;
parentId?: string | null;
title?: string | null;
sortOrder?: number | null;
workspaceId?: string | null;
execution?: {
access_scope?: "private" | "shared" | "public";
is_template?: boolean;
created_at?: string | null;
updated_at?: string | null;
} | null;
} | null;
};
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const { parentId }: { parentId?: string | null } = await request.json();
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "create",
parentId: typeof parentId === "string" ? parentId.trim() || null : null,
}),
});
const payload = (await response.json().catch(() => null)) as TreeCreateResponse | { error?: string } | null;
if (!response.ok) {
return NextResponse.json(
{
error:
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: "创建页面失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
id: payload?.result?.documentId ?? "",
title: payload?.result?.title ?? "无标题",
parent_id: payload?.result?.parentId ?? null,
sort_order: payload?.result?.sortOrder ?? null,
workspace_id: payload?.result?.workspaceId ?? undefined,
access_scope: payload?.result?.execution?.access_scope ?? "private",
is_template: payload?.result?.execution?.is_template ?? false,
created_at: payload?.result?.execution?.created_at ?? null,
updated_at:
payload?.result?.execution?.updated_at ??
payload?.result?.execution?.created_at ??
null,
meta: {
requestId: payload?.requestId,
traceId: payload?.traceId,
commandName: "tree.node.create",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "创建页面失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -1,75 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type TreeArchiveResponse = {
requestId?: string;
traceId?: string;
};
type DeletePayload = {
documentId?: string | null;
workspaceId?: string | null;
};
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const { documentId, workspaceId }: DeletePayload = await request.json();
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
if (!normalizedDocumentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "archive",
documentId: normalizedDocumentId,
workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null,
}),
});
const payload = (await response.json().catch(() => null)) as
| TreeArchiveResponse
| { error?: string }
| null;
if (!response.ok) {
return NextResponse.json(
{
error:
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: "删除失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
success: true,
meta: {
requestId: payload?.requestId,
traceId: payload?.traceId,
commandName: "tree.node.archive",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "删除失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -1,74 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type TreeEmbedResponse = {
requestId?: string;
traceId?: string;
};
type EmbedPayload = {
sourceId?: string | null;
targetId?: string | null;
};
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const { sourceId, targetId }: EmbedPayload = await request.json();
const normalizedSourceId = typeof sourceId === "string" ? sourceId.trim() : "";
const normalizedTargetId = typeof targetId === "string" ? targetId.trim() : "";
if (!normalizedSourceId || !normalizedTargetId) {
return NextResponse.json({ error: "缺少 sourceId 或 targetId" }, { status: 400 });
}
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "embed",
sourceId: normalizedSourceId,
targetId: normalizedTargetId,
}),
});
const payload = (await response.json().catch(() => null)) as
| TreeEmbedResponse
| { error?: string }
| null;
if (!response.ok) {
return NextResponse.json(
{
error:
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: "嵌入失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
ok: true,
meta: {
requestId: payload?.requestId,
traceId: payload?.traceId,
commandName: "tree.node.embed",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "嵌入失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -1,79 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type TreeMoveResponse = {
requestId?: string;
traceId?: string;
result?: {
documentId?: string;
parentId?: string | null;
sortOrder?: number | null;
} | null;
};
type MovePayload = {
documentId?: string | null;
parentId?: string | null;
position?: number | null;
workspaceId?: string | null;
};
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const { documentId, parentId = null, position, workspaceId }: MovePayload = await request.json();
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
if (!normalizedDocumentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "move",
documentId: normalizedDocumentId,
parentId: typeof parentId === "string" ? parentId.trim() || null : null,
sortOrder: typeof position === "number" && Number.isFinite(position) ? Math.floor(position) : 0,
workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null,
}),
});
const payload = (await response.json().catch(() => null)) as TreeMoveResponse | { error?: string } | null;
if (!response.ok) {
return NextResponse.json(
{
error:
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: "移动失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
ok: true,
meta: {
requestId: payload?.requestId,
traceId: payload?.traceId,
commandName: "tree.subtree.move",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "移动失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -1,77 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type TreePurgeResponse = {
requestId?: string;
traceId?: string;
result?: {
purged?: boolean;
} | null;
};
type PurgePayload = {
documentId?: string | null;
};
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const { documentId }: PurgePayload = await request.json();
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
if (!normalizedDocumentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "purge",
documentId: normalizedDocumentId,
}),
});
const payload = (await response.json().catch(() => null)) as
| TreePurgeResponse
| { error?: string }
| null;
if (!response.ok) {
return NextResponse.json(
{
error:
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: "彻底删除失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
success: true,
purged: payload?.result?.purged ?? true,
meta: {
requestId: payload?.requestId,
traceId: payload?.traceId,
commandName: "tree.node.purge",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "彻底删除失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -1,75 +0,0 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
type TreeRestoreResponse = {
requestId?: string;
traceId?: string;
};
type RestorePayload = {
documentId?: string | null;
workspaceId?: string | null;
};
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const { documentId, workspaceId }: RestorePayload = await request.json();
const normalizedDocumentId = typeof documentId === "string" ? documentId.trim() : "";
if (!normalizedDocumentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const upstreamUrl = new URL("/api/tree/commands", request.url);
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
body: JSON.stringify({
action: "restore",
documentId: normalizedDocumentId,
workspaceId: typeof workspaceId === "string" ? workspaceId.trim() || null : null,
}),
});
const payload = (await response.json().catch(() => null)) as
| TreeRestoreResponse
| { error?: string }
| null;
if (!response.ok) {
return NextResponse.json(
{
error:
payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
? payload.error
: "恢复失败,请稍后再试",
},
{ status: response.status },
);
}
return NextResponse.json({
success: true,
meta: {
requestId: payload?.requestId,
traceId: payload?.traceId,
commandName: "tree.node.restore",
},
});
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "恢复失败,请稍后再试",
},
{ status: 500 },
);
}
}
export const runtime = "nodejs";
@@ -214,7 +214,11 @@ describe("documents route adapters", () => {
const response = await postTitle(new Request("http://localhost/api/documents/title", {
method: "POST",
headers: { "content-type": "application/json" },
headers: {
"authorization": "Bearer test-token",
"content-type": "application/json",
"cookie": "convex-auth=test-cookie",
},
body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }),
}));
const payload = await response.json() as {
@@ -235,6 +239,15 @@ describe("documents route adapters", () => {
}),
}),
);
const renameCall = fetchMock.mock.calls.find(([, init]) => {
if (!init || typeof init.body !== "string") {
return false;
}
return init.body.includes('"action":"rename"');
});
const forwardedHeaders = renameCall?.[1]?.headers as Headers;
expect(forwardedHeaders.get("authorization")).toBe("Bearer test-token");
expect(forwardedHeaders.get("cookie")).toBe("convex-auth=test-cookie");
expect(payload.ok).toBe(true);
expect(payload.meta.commandName).toBe("tree.node.rename");
});
@@ -35,11 +35,20 @@ export async function POST(request: Request) {
if (commandName !== PAGE_COMMAND_NAMES.updateTitle) {
const upstreamUrl = new URL("/api/tree/commands", request.url);
const upstreamHeaders = new Headers({
"content-type": "application/json",
});
const authorization = request.headers.get("authorization");
const cookie = request.headers.get("cookie");
if (authorization) {
upstreamHeaders.set("authorization", authorization);
}
if (cookie) {
upstreamHeaders.set("cookie", cookie);
}
const response = await fetch(upstreamUrl.toString(), {
method: "POST",
headers: new Headers({
"content-type": "application/json",
}),
headers: upstreamHeaders,
body: JSON.stringify({
action: "rename",
documentId: normalizedDocumentId,