chore: init monorepo snapshot

This commit is contained in:
liaibo
2025-11-23 10:55:04 +08:00
commit c70ff52869
941 changed files with 246586 additions and 0 deletions
@@ -0,0 +1,92 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { MediaAsset } from "@/types/media";
export const dynamic = "force-dynamic";
export async function GET(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const workspaceId = searchParams.get("workspaceId");
const limit = Number.parseInt(searchParams.get("limit") ?? "12", 10);
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
let query = supabase
.from("media_assets")
.select("*")
.eq("workspace_id", workspaceId)
.order("created_at", { ascending: false })
.limit(Number.isNaN(limit) ? 12 : limit);
const assetType = searchParams.get("assetType");
if (assetType) {
query = query.eq("asset_type", assetType);
}
const { data, error } = await query;
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ items: (data ?? []) as MediaAsset[] });
}
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const payload = (await request.json()) as {
workspaceId: string;
documentId: string;
fileUrl: string;
thumbnailUrl?: string;
assetType?: string;
fileName?: string;
fileSize?: number;
mimeType?: string;
};
if (!payload.workspaceId || !payload.documentId || !payload.fileUrl) {
return NextResponse.json({ error: "参数不完整" }, { status: 400 });
}
const { data, error } = await supabase
.from("media_assets")
.insert({
workspace_id: payload.workspaceId,
document_id: payload.documentId,
file_url: payload.fileUrl,
thumbnail_url: payload.thumbnailUrl ?? payload.fileUrl,
asset_type: payload.assetType ?? "image",
file_name: payload.fileName,
file_size: payload.fileSize ?? null,
mime_type: payload.mimeType ?? null,
created_by: session.user.id,
})
.select("*")
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ asset: data as MediaAsset });
}
@@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { assetId } = (await request.json()) as { assetId?: string };
if (!assetId) {
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
}
const { error } = await supabase
.from("media_assets")
.update({ ocr_status: "processing" })
.eq("id", assetId)
.limit(1);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
if (backendUrl) {
void fetch(`${backendUrl}/api/v1/tasks/media-ocr`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ asset_id: assetId }),
}).catch((err) => {
console.warn("触发后端 OCR 失败", err);
});
}
return NextResponse.json({ ok: true });
}
@@ -0,0 +1,82 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { MediaAsset } from "@/types/media";
import { extname } from "path";
export const dynamic = "force-dynamic";
const MEDIA_BUCKET = process.env.NEXT_PUBLIC_SUPABASE_MEDIA_BUCKET ?? "media";
const resolveAssetType = (mime: string): "image" | "video" | "audio" | "file" => {
if (mime.startsWith("image/")) return "image";
if (mime.startsWith("video/")) return "video";
if (mime.startsWith("audio/")) return "audio";
return "file";
};
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const formData = await request.formData();
const file = formData.get("file");
const workspaceId = String(formData.get("workspaceId") ?? "");
const documentId = String(formData.get("documentId") ?? "");
if (!(file instanceof File) || !workspaceId || !documentId) {
return NextResponse.json({ error: "缺少必要参数" }, { status: 400 });
}
try {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const extension = extname(file.name || "").replace(/\s+/g, "");
const uniqueId = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
const path = `${workspaceId}/${Date.now()}-${uniqueId}${extension}`;
const assetType = resolveAssetType(file.type || "");
const { error: uploadError } = await supabase.storage.from(MEDIA_BUCKET).upload(path, buffer, {
contentType: file.type,
upsert: false,
});
if (uploadError) {
return NextResponse.json({ error: uploadError.message }, { status: 500 });
}
const {
data: { publicUrl },
} = supabase.storage.from(MEDIA_BUCKET).getPublicUrl(path);
const { data: asset, error } = await supabase
.from("media_assets")
.insert({
workspace_id: workspaceId,
document_id: documentId,
file_url: publicUrl,
thumbnail_url: publicUrl,
asset_type: assetType,
file_name: file.name,
file_size: file.size,
mime_type: file.type,
created_by: session.user.id,
})
.select("*")
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ asset: asset as MediaAsset });
} catch (error) {
console.error(error);
return NextResponse.json({ error: "上传失败" }, { status: 500 });
}
}