import { NextResponse } from "next/server"; import { createSupabaseRouteClient } from "@/lib/supabase/server"; import { ensureDefaultWorkspace, fetchWorkspaceSummaries } from "@/lib/workspaces"; import { fetchSidebarDataset } from "@/lib/sidebar-tree"; import type { SidebarInitialData } from "@/components/sidebar/types"; import { isConvexEnabled } from "@/lib/convex/enabled"; import { getConvexHttpClient } from "@/lib/convex/server"; import { api } from "@/lib/convex/api"; import { requireAuthContext } from "@/lib/auth/authContext"; import { randomUUID } from "crypto"; import { detectLocalMindmapFiles, detectLocalMindmapDocs, detectLocalMindmapImageAssetIdsByMindmapId, detectLocalTrashedMindmapAssets, } from "@/lib/server/mindmap-files"; import type { MediaAsset } from "@/types/media"; export const dynamic = "force-dynamic"; function extractMindmapImageAssetIdsFromData(input: unknown): string[] { const root = (() => { if (!input || typeof input !== "object") return input; // eslint-disable-next-line @typescript-eslint/no-explicit-any const record = input as any; // 兼容:某些导图结构为 { root: ... } return record && typeof record === "object" && "root" in record ? record.root : input; })(); const ids: string[] = []; const seen = new Set(); const push = (value: unknown) => { if (typeof value !== "string") return; if (!value.startsWith("asset:")) return; const id = value.slice("asset:".length).trim(); if (!id) return; if (seen.has(id)) return; seen.add(id); ids.push(id); }; const get = (obj: unknown, key: string): unknown => { if (!obj || typeof obj !== "object") return undefined; return (obj as Record)[key]; }; const walk = (node: unknown) => { if (!node || typeof node !== "object") return; const data = get(node, "data"); const image = get(node, "image"); // 常见:node.data.image = "asset:xxx" push(get(data, "image")); // 兼容:node.image = "asset:xxx" push(image); // 兼容:node.image.url = "asset:xxx" push(get(image, "url")); // 兼容:node.data.image.url = "asset:xxx" push(get(get(data, "image"), "url")); const children = get(node, "children"); if (Array.isArray(children)) { children.forEach(walk); } }; walk(root); return ids; } export async function GET(request: Request) { if (isConvexEnabled()) { const auth = requireAuthContext(); const client = getConvexHttpClient(); const url = new URL(request.url); const workspaceIdParam = url.searchParams.get("workspaceId"); const bootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { userId: auth.userId, fallbackName: auth.email ?? auth.name ?? "我的空间", workspaceIdIfCreate: randomUUID(), }); const summaries = await client.query(api.workspaces.fetchWorkspaceSummaries, { userId: auth.userId, }); const workspaces = summaries.workspaces.length > 0 ? summaries.workspaces : bootstrap.workspaces; const activeWorkspaceId = summaries.activeWorkspaceId || bootstrap.activeWorkspaceId; const targetWorkspaceId = workspaceIdParam || activeWorkspaceId; if (!targetWorkspaceId) { return NextResponse.json({ error: "暂无可用工作空间" }, { status: 404 }); } try { const documents = await client.query(api.documents.listByWorkspace, { userId: auth.userId, workspaceId: targetWorkspaceId, }); const trashedDocuments = await client.query(api.documents.listTrashedByWorkspace, { userId: auth.userId, workspaceId: targetWorkspaceId, }); const mindmapRows = await client.query(api.mindmaps.listByWorkspace, { userId: auth.userId, workspaceId: targetWorkspaceId, includeDeleted: true, }); const activeMindmaps = (mindmapRows ?? []).filter((r) => !r.deleted_at); const trashedMindmaps = (mindmapRows ?? []).filter((r) => !!r.deleted_at); const mindmapDocs = Array.from(new Set(activeMindmaps.map((r) => r.document_id))); const mindmapAssetChildren: Record = {}; activeMindmaps.forEach((r) => { const ids = extractMindmapImageAssetIdsFromData(r.data); if (ids.length > 0) { mindmapAssetChildren[r.mindmap_id] = ids; } }); const mindmapAssets: MediaAsset[] = activeMindmaps.map((r) => { const isLegacy = r.mindmap_id.startsWith("legacy-"); return { id: r.mindmap_id, workspace_id: r.workspace_id ?? targetWorkspaceId, document_id: r.document_id, asset_type: "mindmap", file_url: null, thumbnail_url: null, bucket: null, storage_path: null, file_name: isLegacy ? "mindmap.json" : `mindmap-${r.mindmap_id}.json`, file_size: null, mime_type: "application/json", ocr_payload: undefined, ocr_strategy: null, ocr_text: null, ocr_status: null, signed_url: null, created_at: r.created_at ?? "", updated_at: r.updated_at ?? "", }; }); const trashedMindmapAssets: MediaAsset[] = trashedMindmaps.map((r) => { const isLegacy = r.mindmap_id.startsWith("legacy-"); return { id: r.mindmap_id, workspace_id: r.workspace_id ?? targetWorkspaceId, document_id: r.document_id, asset_type: "mindmap", file_url: null, thumbnail_url: null, bucket: null, storage_path: null, file_name: isLegacy ? "mindmap.json" : `mindmap-${r.mindmap_id}.json`, file_size: null, mime_type: "application/json", ocr_payload: undefined, ocr_strategy: null, ocr_text: null, ocr_status: null, deleted_at: r.deleted_at ?? null, deleted_by: r.deleted_by ?? null, purged_at: null, signed_url: null, created_at: r.created_at ?? "", updated_at: r.updated_at ?? "", }; }); const payload: SidebarInitialData = { activeWorkspaceId: targetWorkspaceId, workspaces, documents, trashedDocuments, trashedMediaAssets: [], trashedMindmapAssets, mindmapDocs, mindmapAssets, mindmapAssetChildren, tableAssets: [], mediaAssets: [], }; return NextResponse.json(payload); } catch (error) { return NextResponse.json( { error: error instanceof Error ? error.message : "拉取侧边栏数据失败" }, { status: 500 }, ); } } const supabase = await createSupabaseRouteClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } const url = new URL(request.url); const workspaceIdParam = url.searchParams.get("workspaceId"); await ensureDefaultWorkspace(supabase, session.user.id, session.user.email ?? "我的空间"); const { workspaces, activeWorkspaceId } = await fetchWorkspaceSummaries(supabase, session.user.id); const targetWorkspaceId = workspaceIdParam || activeWorkspaceId; if (!targetWorkspaceId) { return NextResponse.json({ error: "暂无可用工作空间" }, { status: 404 }); } try { const dataset = await fetchSidebarDataset(supabase, targetWorkspaceId); const docIds = dataset.documents.map((d) => d.id); const localMindmapFiles = await detectLocalMindmapFiles(docIds); const mindmapAssetChildren = await detectLocalMindmapImageAssetIdsByMindmapId(localMindmapFiles); const mindmapDocs = Array.from(new Set([...(dataset.mindmapDocs ?? []), ...(await detectLocalMindmapDocs(docIds))])); const trashedMindmapAssets = await detectLocalTrashedMindmapAssets(targetWorkspaceId, docIds); const docById = new Map(dataset.documents.map((d) => [d.id, d])); const mindmapAssets: MediaAsset[] = localMindmapFiles.map((item) => { const doc = docById.get(item.documentId); const workspaceId = doc?.workspace_id ?? targetWorkspaceId; const fileUrlBase = item.source === "legacy" ? `/mindmaps/${item.documentId}` : `/documents/${item.documentId}`; return { id: item.mindmapId, workspace_id: workspaceId, document_id: item.documentId, asset_type: "mindmap", file_url: `${fileUrlBase}/${item.fileName}`, thumbnail_url: null, bucket: null, storage_path: null, file_name: item.fileName, file_size: null, mime_type: "application/json", ocr_payload: undefined, ocr_strategy: null, ocr_text: null, ocr_status: null, signed_url: null, created_at: "", updated_at: "", }; }); const tableAssets: MediaAsset[] = (dataset.tables ?? []).map((row) => { const base = (row.title ?? "未命名表格").trim() || "未命名表格"; const fileName = base.toLowerCase().endsWith(".luckysheet") ? base : `${base}.luckysheet`; return { id: row.id, workspace_id: row.workspace_id ?? targetWorkspaceId, document_id: row.document_id, asset_type: "luckysheet", file_url: `/tables/${row.id}/view`, thumbnail_url: null, bucket: null, storage_path: null, file_name: fileName, file_size: null, mime_type: "application/json", ocr_payload: undefined, ocr_strategy: null, ocr_text: null, ocr_status: null, signed_url: null, created_at: row.created_at ?? "", updated_at: row.updated_at ?? "", }; }); const payload: SidebarInitialData = { activeWorkspaceId: targetWorkspaceId, workspaces, documents: dataset.documents, trashedDocuments: dataset.trashedDocuments, trashedMediaAssets: dataset.trashedMediaAssets ?? [], trashedMindmapAssets, mindmapDocs, mindmapAssets, mindmapAssetChildren, tableAssets, mediaAssets: dataset.mediaAssets ?? [], }; return NextResponse.json(payload); } catch (error) { return NextResponse.json( { error: error instanceof Error ? error.message : "拉取侧边栏数据失败" }, { status: 500 }, ); } }