fix: harden attachment analysis delivery
This commit is contained in:
@@ -199,6 +199,10 @@ try {
|
||||
"queued task should carry attachment file name",
|
||||
);
|
||||
assert.ok(textUpload.analysisTask.attachmentDownloadUrl, "queued task should expose attachment download url");
|
||||
assert.ok(
|
||||
textUpload.analysisTask.attachmentDownloadUrl.startsWith(currentServer.baseUrl),
|
||||
"queued task should use the current runtime origin for attachment download",
|
||||
);
|
||||
const promptDownloadUrlMatch = textUpload.analysisTask.executionPrompt.match(/downloadUrl:\s+(http[^\s]+)/);
|
||||
assert.ok(promptDownloadUrlMatch, "execution prompt should include attachment download url");
|
||||
const unauthDownloadResponse = await fetch(textUpload.analysisTask.attachmentDownloadUrl);
|
||||
|
||||
@@ -53,14 +53,30 @@ export async function GET(
|
||||
}
|
||||
|
||||
if (record.attachment.storageBackend === "aliyun_oss") {
|
||||
const storageConfig = await getAttachmentStorageConfig(record.attachment.uploadedBy);
|
||||
if (storageConfig.mode !== "oss" || storageConfig.ossProvider !== "aliyun_oss" || !storageConfig.aliyunOss) {
|
||||
const aliyunConfig =
|
||||
record.attachment.storageSnapshot?.provider === "aliyun_oss"
|
||||
? {
|
||||
enabled: true,
|
||||
accessKeyId: record.attachment.storageSnapshot.accessKeyId,
|
||||
accessKeySecretEncrypted: record.attachment.storageSnapshot.accessKeySecretEncrypted,
|
||||
bucket: record.attachment.storageSnapshot.bucket,
|
||||
endpoint: record.attachment.storageSnapshot.endpoint,
|
||||
region: record.attachment.storageSnapshot.region,
|
||||
prefix: record.attachment.storageSnapshot.prefix,
|
||||
}
|
||||
: null;
|
||||
const storageConfig = aliyunConfig ? null : await getAttachmentStorageConfig(record.attachment.uploadedBy);
|
||||
const resolvedConfig =
|
||||
aliyunConfig ??
|
||||
(storageConfig?.mode === "oss" &&
|
||||
storageConfig.ossProvider === "aliyun_oss" &&
|
||||
storageConfig.aliyunOss
|
||||
? storageConfig.aliyunOss
|
||||
: null);
|
||||
if (!resolvedConfig) {
|
||||
return NextResponse.json({ ok: false, message: "ATTACHMENT_STORAGE_CONFIG_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
const signedUrl = await getAliyunOssSignedDownloadUrl(
|
||||
storageConfig.aliyunOss,
|
||||
record.attachment.storagePath,
|
||||
);
|
||||
const signedUrl = await getAliyunOssSignedDownloadUrl(resolvedConfig, record.attachment.storagePath);
|
||||
return NextResponse.redirect(signedUrl, {
|
||||
status: 307,
|
||||
headers: buildAttachmentDownloadHeaders(record.attachment),
|
||||
|
||||
@@ -6,6 +6,13 @@ import { queueAttachmentAnalysisTask } from "@/lib/boss-master-agent";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
function resolveRequestPublicBaseUrl(request: NextRequest) {
|
||||
const protocol =
|
||||
request.headers.get("x-forwarded-proto") ?? request.nextUrl.protocol.replace(/:$/, "") ?? "http";
|
||||
const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.host;
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string; attachmentId: string }> },
|
||||
@@ -45,6 +52,7 @@ export async function POST(
|
||||
requestedBy: session.displayName || "你",
|
||||
requestedByAccount: session.account,
|
||||
markProcessing: true,
|
||||
publicBaseUrl: resolveRequestPublicBaseUrl(request),
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, taskId: task.taskId, task });
|
||||
|
||||
@@ -18,6 +18,13 @@ function randomToken(prefix: string) {
|
||||
return `${prefix}-${randomBytes(4).toString("hex")}`;
|
||||
}
|
||||
|
||||
function resolveRequestPublicBaseUrl(request: NextRequest) {
|
||||
const protocol =
|
||||
request.headers.get("x-forwarded-proto") ?? request.nextUrl.protocol.replace(/:$/, "") ?? "http";
|
||||
const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? request.nextUrl.host;
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string }> },
|
||||
@@ -69,6 +76,21 @@ export async function POST(
|
||||
attachmentKind,
|
||||
storageBackend: stored.storageBackend,
|
||||
storagePath: stored.storagePath,
|
||||
storageSnapshot:
|
||||
stored.storageBackend === "aliyun_oss" &&
|
||||
storageConfig.mode === "oss" &&
|
||||
storageConfig.ossProvider === "aliyun_oss" &&
|
||||
storageConfig.aliyunOss
|
||||
? {
|
||||
provider: "aliyun_oss",
|
||||
accessKeyId: storageConfig.aliyunOss.accessKeyId,
|
||||
accessKeySecretEncrypted: storageConfig.aliyunOss.accessKeySecretEncrypted,
|
||||
bucket: storageConfig.aliyunOss.bucket,
|
||||
endpoint: storageConfig.aliyunOss.endpoint,
|
||||
region: storageConfig.aliyunOss.region,
|
||||
prefix: storageConfig.aliyunOss.prefix,
|
||||
}
|
||||
: undefined,
|
||||
previewAvailable: attachmentKind === "image" || attachmentKind === "video" || attachmentKind === "pdf",
|
||||
uploadedAt: new Date().toISOString(),
|
||||
uploadedBy: session.account,
|
||||
@@ -90,6 +112,7 @@ export async function POST(
|
||||
requestMessageId: message.id,
|
||||
requestedBy: session.displayName || "你",
|
||||
requestedByAccount: session.account,
|
||||
publicBaseUrl: resolveRequestPublicBaseUrl(request),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
|
||||
const LARGE_ATTACHMENT_THRESHOLD_BYTES = 20 * 1024 * 1024;
|
||||
const MAX_ATTACHMENT_TEXT_EXCERPT_CHARS = 12_000;
|
||||
const MAX_ATTACHMENT_TEXT_EXCERPT_SOURCE_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
function extensionOf(fileName: string) {
|
||||
return path.extname(fileName).toLowerCase();
|
||||
@@ -83,6 +84,12 @@ export function canExtractAttachmentText(attachment: Pick<MessageAttachment, "at
|
||||
);
|
||||
}
|
||||
|
||||
export function canInlineAttachmentText(
|
||||
attachment: Pick<MessageAttachment, "attachmentKind" | "mimeType" | "fileSizeBytes">,
|
||||
) {
|
||||
return canExtractAttachmentText(attachment) && attachment.fileSizeBytes <= MAX_ATTACHMENT_TEXT_EXCERPT_SOURCE_BYTES;
|
||||
}
|
||||
|
||||
export function extractAttachmentTextExcerpt(buffer: Buffer | Uint8Array) {
|
||||
const normalized = Buffer.from(buffer)
|
||||
.toString("utf8")
|
||||
|
||||
@@ -41,6 +41,15 @@ export interface MessageAttachment {
|
||||
attachmentKind: AttachmentKind;
|
||||
storageBackend: AttachmentStorageBackend;
|
||||
storagePath: string;
|
||||
storageSnapshot?: {
|
||||
provider: "aliyun_oss";
|
||||
accessKeyId: string;
|
||||
accessKeySecretEncrypted: string;
|
||||
bucket: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
prefix?: string;
|
||||
};
|
||||
previewAvailable: boolean;
|
||||
uploadedAt: string;
|
||||
uploadedBy: string;
|
||||
@@ -1920,6 +1929,18 @@ function normalizeMessageAttachment(raw: Partial<MessageAttachment>): MessageAtt
|
||||
attachmentKind: raw.attachmentKind ?? "binary",
|
||||
storageBackend: raw.storageBackend ?? "server_file",
|
||||
storagePath: raw.storagePath ?? "",
|
||||
storageSnapshot:
|
||||
raw.storageSnapshot?.provider === "aliyun_oss"
|
||||
? {
|
||||
provider: "aliyun_oss",
|
||||
accessKeyId: raw.storageSnapshot.accessKeyId ?? "",
|
||||
accessKeySecretEncrypted: raw.storageSnapshot.accessKeySecretEncrypted ?? "",
|
||||
bucket: raw.storageSnapshot.bucket ?? "",
|
||||
endpoint: raw.storageSnapshot.endpoint ?? "",
|
||||
region: raw.storageSnapshot.region ?? "",
|
||||
prefix: raw.storageSnapshot.prefix,
|
||||
}
|
||||
: undefined,
|
||||
previewAvailable: raw.previewAvailable ?? false,
|
||||
uploadedAt: raw.uploadedAt ?? nowIso(),
|
||||
uploadedBy: raw.uploadedBy ?? "system",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
updateAttachmentAnalysisResult,
|
||||
updateAiAccountHealth,
|
||||
} from "@/lib/boss-data";
|
||||
import { canExtractAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
|
||||
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
|
||||
import { readAliyunOssObjectBuffer } from "@/lib/boss-storage-aliyun-oss";
|
||||
import { readServerFileAttachmentBuffer } from "@/lib/boss-storage-server-file";
|
||||
|
||||
@@ -236,14 +236,33 @@ async function buildAttachmentAnalysisContext(params: {
|
||||
const attachment = params.attachment;
|
||||
let excerpt = "";
|
||||
try {
|
||||
if (canExtractAttachmentText(attachment)) {
|
||||
if (canInlineAttachmentText(attachment)) {
|
||||
let buffer: Buffer | Uint8Array = Buffer.alloc(0);
|
||||
if (attachment.storageBackend === "server_file") {
|
||||
buffer = await readServerFileAttachmentBuffer(attachment.storagePath);
|
||||
} else if (attachment.storageBackend === "aliyun_oss") {
|
||||
const config = await getAttachmentStorageConfig(attachment.uploadedBy);
|
||||
if (config.mode === "oss" && config.ossProvider === "aliyun_oss" && config.aliyunOss) {
|
||||
buffer = await readAliyunOssObjectBuffer(config.aliyunOss, attachment.storagePath);
|
||||
if (attachment.storageSnapshot?.provider === "aliyun_oss") {
|
||||
buffer = await readAliyunOssObjectBuffer(
|
||||
{
|
||||
enabled: true,
|
||||
accessKeyId: attachment.storageSnapshot.accessKeyId,
|
||||
accessKeySecretEncrypted: attachment.storageSnapshot.accessKeySecretEncrypted,
|
||||
bucket: attachment.storageSnapshot.bucket,
|
||||
endpoint: attachment.storageSnapshot.endpoint,
|
||||
region: attachment.storageSnapshot.region,
|
||||
prefix: attachment.storageSnapshot.prefix,
|
||||
},
|
||||
attachment.storagePath,
|
||||
);
|
||||
} else {
|
||||
const currentConfig = await getAttachmentStorageConfig(attachment.uploadedBy);
|
||||
if (
|
||||
currentConfig.mode === "oss" &&
|
||||
currentConfig.ossProvider === "aliyun_oss" &&
|
||||
currentConfig.aliyunOss
|
||||
) {
|
||||
buffer = await readAliyunOssObjectBuffer(currentConfig.aliyunOss, attachment.storagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
excerpt = extractAttachmentTextExcerpt(buffer);
|
||||
@@ -311,6 +330,7 @@ export async function queueAttachmentAnalysisTask(params: {
|
||||
requestedBy: string;
|
||||
requestedByAccount: string;
|
||||
markProcessing?: boolean;
|
||||
publicBaseUrl?: string;
|
||||
}) {
|
||||
const record = await getProjectAttachment(params.projectId, params.attachmentId);
|
||||
if (!record) {
|
||||
@@ -322,7 +342,7 @@ export async function queueAttachmentAnalysisTask(params: {
|
||||
const attachmentDownloadToken = randomBytes(12).toString("hex");
|
||||
const attachmentDownloadExpiresAt = new Date(Date.now() + 30 * 60_000).toISOString();
|
||||
const attachmentDownloadUrl =
|
||||
`${resolveBossPublicBaseUrl()}/api/v1/attachments/${record.attachment.attachmentId}/download` +
|
||||
`${params.publicBaseUrl?.trim() || resolveBossPublicBaseUrl()}/api/v1/attachments/${record.attachment.attachmentId}/download` +
|
||||
`?taskId=${taskId}&token=${attachmentDownloadToken}`;
|
||||
const attachmentContext = await buildAttachmentAnalysisContext({
|
||||
attachment: record.attachment,
|
||||
|
||||
Reference in New Issue
Block a user