import { randomUUID } from "crypto"; import { NextResponse } from "next/server"; import { createSupabaseRouteClient } from "@/lib/supabase/server"; import { ensureDefaultWorkspace, resolveActiveWorkspaceId } from "@/lib/workspaces"; import type { Json } from "@/types/supabase"; import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content"; import path from "path"; import { promises as fs } from "fs"; import { getDocumentsBaseDir } from "@/lib/server/local-paths"; import { isConvexEnabled } from "@/lib/convex/enabled"; import { getConvexHttpClient } from "@/lib/convex/server"; import { api } from "@/lib/convex/api"; import { requireAuthContext } from "@/lib/auth/authContext"; const documentsBaseDir = getDocumentsBaseDir(); async function ensureDocumentScaffold(id: string, title: string | null) { const folder = path.join(documentsBaseDir, id); const indexFile = path.join(folder, "index.md"); await fs.mkdir(folder, { recursive: true }); try { await fs.access(indexFile); } catch { const safeTitle = title && title.trim() ? title.trim() : "无标题"; const content = `# ${safeTitle}\n`; await fs.writeFile(indexFile, content, "utf8"); } } export async function POST(request: Request) { try { if (isConvexEnabled()) { return await handleCreateRequestConvex(request); } return await handleCreateRequest(request); } catch (error) { console.error("创建页面失败", error); const message = error instanceof Error ? error.message : "创建页面失败,请稍后再试"; return NextResponse.json({ error: message }, { status: 500 }); } } async function handleCreateRequestConvex(request: Request) { const auth = requireAuthContext(); const client = getConvexHttpClient(); const { parentId }: { parentId?: string | null } = await request.json(); const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { userId: auth.userId, fallbackName: auth.email ?? auth.name ?? "我的空间", workspaceIdIfCreate: randomUUID(), }); let workspaceId: string | null = null; let parentContent: Json | null = null; let accessScope: "private" | "shared" | "public" = "private"; if (parentId) { const parentDoc = await client.query(api.documents.getMeta, { userId: auth.userId, id: parentId, }); if (!parentDoc) { return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 }); } workspaceId = parentDoc.workspace_id; accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope; const parentContentRes = await client.query(api.documents.getContent, { userId: auth.userId, id: parentId }); parentContent = (parentContentRes?.content as Json | null) ?? null; } else { workspaceId = workspaceBootstrap.activeWorkspaceId || null; } if (!workspaceId) { return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); } const id = randomUUID(); const data = await client.mutation(api.documents.create, { userId: auth.userId, id, workspaceId, parentId: parentId ?? null, title: "无标题", accessScope, content: [], }); // 为文件树创建本地目录和 index.md if (data?.id) { await ensureDocumentScaffold(data.id, data.title ?? "无标题"); } if (parentId && data) { const existingBlocks = extractBlocksFromContent(parentContent); const pageReferenceBlock = { id: randomUUID(), type: "pageReference", props: { pageId: data.id, title: data.title ?? "无标题", }, }; const nextBlocks = [...existingBlocks, pageReferenceBlock as Json]; const payload = composeContentWithBlocks(parentContent, nextBlocks); await client.mutation(api.documents.updateContent, { userId: auth.userId, id: parentId, content: payload, }); } return NextResponse.json(data); } async function handleCreateRequest(request: Request) { const supabase = await createSupabaseRouteClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } const { parentId }: { parentId?: string | null } = await request.json(); await ensureDefaultWorkspace(supabase, session.user.id, session.user.email ?? "我的空间"); let workspaceId: string | null = null; let parentContent: Json | null = null; let accessScope: "private" | "shared" | "public" = "private"; if (parentId) { const { data: parentDoc, error: parentError } = await supabase .from("documents") .select("workspace_id,access_scope,content,user_id") .eq("id", parentId) .eq("user_id", session.user.id) .single(); if (parentError || !parentDoc) { return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 }); } workspaceId = parentDoc.workspace_id; accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope; parentContent = parentDoc.content; } else { workspaceId = await resolveActiveWorkspaceId(supabase, session.user.id); if (!workspaceId) { return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); } } if (!workspaceId) { return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); } const siblingQuery = supabase .from("documents") .select("id", { head: true, count: "exact" }) .eq("workspace_id", workspaceId); if (parentId) { siblingQuery.eq("parent_id", parentId); } else { siblingQuery.is("parent_id", null); } const { count: rawSiblingCount, error: countError } = await siblingQuery; const siblingCount = rawSiblingCount ?? 0; if (countError) { return NextResponse.json({ error: countError.message }, { status: 500 }); } const { data, error } = await supabase .from("documents") .insert({ user_id: session.user.id, parent_id: parentId ?? null, workspace_id: workspaceId, title: "无标题", content: { blocks: [] }, access_scope: accessScope, sort_order: siblingCount, }) .select( "id,title,parent_id,sort_order,is_starred,created_at,updated_at,workspace_id,access_scope,is_template", ) .single(); if (error) { return NextResponse.json({ error: error.message }, { status: 400 }); } // 为文件树创建本地目录和 index.md if (data?.id) { await ensureDocumentScaffold(data.id, data.title ?? "无标题"); } if (parentId && data) { const existingBlocks = extractBlocksFromContent(parentContent); const pageReferenceBlock = { id: randomUUID(), type: "pageReference", props: { pageId: data.id, title: data.title ?? "无标题", }, }; const nextBlocks = [...existingBlocks, pageReferenceBlock as Json]; const payload = composeContentWithBlocks(parentContent, nextBlocks); const timestamp = new Date().toISOString(); const { error: parentUpdateError } = await supabase .from("documents") .update({ content: payload, updated_at: timestamp }) .eq("id", parentId) .eq("user_id", session.user.id); if (parentUpdateError) { return NextResponse.json({ error: parentUpdateError.message }, { status: 500 }); } } return NextResponse.json(data); }