feat: add server file attachment pipeline

This commit is contained in:
kris
2026-03-29 15:26:06 +08:00
parent c3900a11ec
commit aa75506364
7 changed files with 453 additions and 7 deletions

View File

@@ -0,0 +1,45 @@
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { Readable } from "node:stream";
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getAttachmentById } from "@/lib/boss-data";
import { buildAttachmentDownloadHeaders } from "@/lib/boss-attachments";
import { resolveServerFileAttachmentAbsolutePath } from "@/lib/boss-storage-server-file";
export const runtime = "nodejs";
export async function GET(
request: NextRequest,
context: { params: Promise<{ attachmentId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { attachmentId } = await context.params;
const record = await getAttachmentById(attachmentId);
if (!record) {
return NextResponse.json({ ok: false, message: "ATTACHMENT_NOT_FOUND" }, { status: 404 });
}
if (record.attachment.storageBackend !== "server_file") {
return NextResponse.json(
{ ok: false, message: "UNSUPPORTED_ATTACHMENT_STORAGE_BACKEND" },
{ status: 501 },
);
}
const absolutePath = resolveServerFileAttachmentAbsolutePath(record.attachment.storagePath);
try {
await stat(absolutePath);
} catch {
return NextResponse.json({ ok: false, message: "ATTACHMENT_FILE_NOT_FOUND" }, { status: 404 });
}
const stream = createReadStream(absolutePath);
return new NextResponse(Readable.toWeb(stream) as BodyInit, {
headers: buildAttachmentDownloadHeaders(record.attachment),
});
}

View File

@@ -0,0 +1,86 @@
import { randomBytes } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import {
appendAttachmentMessage,
getAttachmentStorageConfig,
readState,
type MessageAttachment,
} from "@/lib/boss-data";
import { detectAttachmentKind, resolveAttachmentAnalysisState } from "@/lib/boss-attachments";
import { getAttachmentStorageProvider } from "@/lib/boss-storage";
export const runtime = "nodejs";
function randomToken(prefix: string) {
return `${prefix}-${randomBytes(4).toString("hex")}`;
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { projectId } = await context.params;
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
if (!project) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ ok: false, message: "FILE_REQUIRED" }, { status: 400 });
}
const bytes = Buffer.from(await file.arrayBuffer());
const attachmentId = randomToken("att");
const messageId = randomToken("msg");
const fileName = file.name || "attachment";
const mimeType = file.type || "application/octet-stream";
const attachmentKind = detectAttachmentKind(fileName, mimeType);
const analysisState = resolveAttachmentAnalysisState(attachmentKind, bytes.byteLength);
const storageConfig = await getAttachmentStorageConfig(session.account);
const storageProvider = getAttachmentStorageProvider(storageConfig);
const stored = await storageProvider.storeAttachment({
account: session.account,
messageId,
attachmentId,
fileName,
mimeType,
buffer: bytes,
});
const attachment: MessageAttachment = {
attachmentId,
fileName,
mimeType,
fileSizeBytes: bytes.byteLength,
attachmentKind,
storageBackend: stored.storageBackend,
storagePath: stored.storagePath,
previewAvailable: attachmentKind === "image" || attachmentKind === "video" || attachmentKind === "pdf",
uploadedAt: new Date().toISOString(),
uploadedBy: session.account,
analysisState,
};
const message = await appendAttachmentMessage({
projectId,
sender: "user",
senderLabel: session.displayName || "你",
attachment,
});
return NextResponse.json({
ok: true,
attachment,
message,
downloadUrl: `/api/v1/attachments/${attachmentId}/download`,
});
}