import { v } from "convex/values"; import { query } from "./_generated/server"; import { api } from "./_generated/api"; import { requireUserId } from "./_utils/auth"; type MindmapRow = { mindmap_id: string; workspace_id?: string | null; document_id: string; data?: unknown; created_at?: string | null; updated_at?: string | null; deleted_at?: string | null; deleted_by?: string | null; }; type TableRow = { id: string; workspace_id?: string | null; document_id: string; title?: string | null; created_at?: string | null; updated_at?: string | null; deleted_at?: string | null; deleted_by?: string | null; purged_at?: string | null; is_archived?: boolean | null; }; function normalizeStringArray(values: Iterable): string[] { return Array.from(new Set(values)).filter((value) => value.trim().length > 0); } function extractMindmapImageAssetIdsFromData(input: unknown): string[] { const root = (() => { if (!input || typeof input !== "object") return input; const record = input as Record; 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 || 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"); push(get(data, "image")); push(image); push(get(image, "url")); push(get(get(data, "image"), "url")); const children = get(node, "children"); if (Array.isArray(children)) { children.forEach(walk); } }; walk(root); return ids; } function toMindmapAsset(row: MindmapRow, workspaceId: string) { const isLegacy = row.mindmap_id.startsWith("legacy-"); const suffix = row.mindmap_id.match(/(\d{4,})$/)?.[1]?.slice(-4) ?? row.mindmap_id.replace(/^mindmap[_-]?/i, "").slice(-6); const fileName = suffix ? `思维导图-${suffix}.json` : "思维导图.json"; return { id: row.mindmap_id, workspace_id: row.workspace_id ?? workspaceId, document_id: row.document_id, asset_type: "mindmap", file_url: null, thumbnail_url: null, bucket: null, storage_path: null, file_name: isLegacy ? "思维导图.json" : 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 ?? "", }; } function toTrashedMindmapAsset(row: MindmapRow, workspaceId: string) { return { ...toMindmapAsset(row, workspaceId), deleted_at: row.deleted_at ?? null, deleted_by: row.deleted_by ?? null, purged_at: null, }; } function toTableAsset(row: TableRow, workspaceId: string) { const base = String(row.title ?? "未命名表格").trim() || "未命名表格"; const fileName = base.toLowerCase().endsWith(".luckysheet") ? base : `${base}.luckysheet`; return { id: row.id, workspace_id: row.workspace_id ?? workspaceId, document_id: row.document_id, asset_type: "luckysheet", file_url: null, 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 ?? "", }; } function toTrashedTableAsset(row: TableRow, workspaceId: string) { return { ...toTableAsset(row, workspaceId), deleted_at: row.deleted_at ?? row.updated_at ?? null, deleted_by: row.deleted_by ?? null, purged_at: row.purged_at ?? null, }; } export const datasetList = query({ args: { workspaceId: v.string(), }, handler: async (ctx, args) => { const userId = await requireUserId(ctx); // 说明:这条查询作为 Sidebar 的单一数据集入口,先复用现有稳定 query, // 把前端原先“多 query + 多处拼装”收口成一条主查询契约。 const [ workspacesResult, documents, trashedDocuments, mindmaps, mediaAssets, trashedMediaAssets, tables, ] = await Promise.all([ ctx.runQuery(api.workspaces.fetchWorkspaceSummaries, {}), ctx.runQuery(api.documents.listByWorkspace, { workspaceId: args.workspaceId, }), ctx.runQuery(api.documents.listTrashedByWorkspace, { workspaceId: args.workspaceId, }), ctx.runQuery(api.mindmaps.listByWorkspace, { workspaceId: args.workspaceId, includeDeleted: true, }), ctx.runQuery(api.mediaAssets.listByWorkspace, { userId, workspaceId: args.workspaceId, limit: 200, }), ctx.runQuery(api.mediaAssets.listDeletedByWorkspace, { userId, workspaceId: args.workspaceId, limit: 2000, }), ctx.runQuery(api.tables.listByWorkspaceForSearch, { userId, workspaceId: args.workspaceId, includeArchived: true, limit: 3000, }), ]); const activeMindmaps = (mindmaps as MindmapRow[]).filter((row) => !row.deleted_at); const trashedMindmaps = (mindmaps as MindmapRow[]).filter((row) => Boolean(row.deleted_at)); const activeTables = (tables as TableRow[]).filter((row) => !row.is_archived); const trashedTables = (tables as TableRow[]).filter((row) => Boolean(row.is_archived)); const mindmapAssetChildren: Record = {}; activeMindmaps.forEach((row) => { const ids = extractMindmapImageAssetIdsFromData(row.data); if (ids.length > 0) { mindmapAssetChildren[row.mindmap_id] = ids; } }); return { active_workspace_id: args.workspaceId, workspaces: workspacesResult.workspaces, documents, trashed_documents: trashedDocuments, media_assets: mediaAssets ?? [], trashed_media_assets: trashedMediaAssets ?? [], mindmap_assets: activeMindmaps.map((row) => toMindmapAsset(row, args.workspaceId)), trashed_mindmap_assets: trashedMindmaps.map((row) => toTrashedMindmapAsset(row, args.workspaceId), ), table_assets: activeTables.map((row) => toTableAsset(row, args.workspaceId)), trashed_table_assets: trashedTables.map((row) => toTrashedTableAsset(row, args.workspaceId)), mindmap_docs: normalizeStringArray(activeMindmaps.map((row) => row.document_id)), mindmap_asset_children: mindmapAssetChildren, }; }, });