164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
import { createCustomOSSUploader } from '@bty/uploader'
|
|
import type { STSTokenResponse } from '@/package/uploader/src/index'
|
|
import { AESUtils } from '@/package/utils/crypto'
|
|
import { tryParseToObject } from '@/package/utils/parse'
|
|
import { isSupportedFileType, getMimeByExtension } from './file-types'
|
|
import { HTTPClient } from '@/http'
|
|
|
|
const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
|
|
|
|
export interface BotAttachment {
|
|
url: string
|
|
fileName: string
|
|
mimeType?: string
|
|
size?: number
|
|
}
|
|
|
|
interface UploadResult {
|
|
fileId: string
|
|
fileName: string
|
|
}
|
|
|
|
// Reuse a single HTTPClient for Nova API calls
|
|
const novaClient = new HTTPClient({
|
|
baseURL: process.env.NOVA_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Tenant-Id': process.env.NOVA_TENANT_ID!,
|
|
'Authorization': process.env.NOVA_ACCESS_KEY!,
|
|
},
|
|
})
|
|
|
|
async function getServerSTSToken(): Promise<STSTokenResponse> {
|
|
const env = process.env.NODE_ENV
|
|
const res = await novaClient.get<string | { data?: string }>('/v1/oss/upload_sts')
|
|
|
|
const dataToDecrypt = typeof res === 'string'
|
|
? res
|
|
: res?.data
|
|
|
|
if (typeof dataToDecrypt !== 'string') {
|
|
throw new Error('Invalid encrypted STS payload from Nova API')
|
|
}
|
|
|
|
const decryptedData = new AESUtils(env).decryptAES_CBC(dataToDecrypt)
|
|
return tryParseToObject(decryptedData) as STSTokenResponse
|
|
}
|
|
|
|
async function downloadFile(
|
|
url: string,
|
|
headers?: Record<string, string>,
|
|
): Promise<{ buffer: Buffer; size: number }> {
|
|
const response = await fetch(url, { headers })
|
|
if (!response.ok) {
|
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
|
}
|
|
const arrayBuffer = await response.arrayBuffer()
|
|
const buffer = Buffer.from(arrayBuffer)
|
|
return { buffer, size: buffer.length }
|
|
}
|
|
|
|
async function uploadSingleAttachment(
|
|
attachment: BotAttachment,
|
|
headers?: Record<string, string>,
|
|
): Promise<UploadResult | null> {
|
|
// 1. Validate file type
|
|
if (!isSupportedFileType(attachment.fileName)) {
|
|
console.warn(`[File Uploader] Unsupported file type: ${attachment.fileName}`)
|
|
return null
|
|
}
|
|
|
|
// 2. Pre-check size if available
|
|
if (attachment.size && attachment.size > MAX_FILE_SIZE) {
|
|
console.warn(`[File Uploader] File too large (${attachment.size} bytes): ${attachment.fileName}`)
|
|
return null
|
|
}
|
|
|
|
// 3. Download file
|
|
const { buffer, size } = await downloadFile(attachment.url, headers)
|
|
|
|
if (size > MAX_FILE_SIZE) {
|
|
console.warn(`[File Uploader] Downloaded file too large (${size} bytes): ${attachment.fileName}`)
|
|
return null
|
|
}
|
|
|
|
// 4. Determine MIME type
|
|
const mimeType = attachment.mimeType || getMimeByExtension(attachment.fileName) || 'application/octet-stream'
|
|
|
|
// 5. Construct OSS path (same pattern as frontend useFileUploader)
|
|
const uuid = crypto.randomUUID()
|
|
const timestamp = Date.now()
|
|
const filePath = `super_agent/user_upload_file/${uuid}/_${timestamp}_${attachment.fileName}`
|
|
|
|
// 6. Upload to OSS
|
|
const ossUploader = createCustomOSSUploader(getServerSTSToken)
|
|
|
|
// Note: We use `upload` (which maps to `put` in ali-oss) instead of `multipartUpload`
|
|
// and pass the raw `Buffer` directly. In Node.js, ali-oss natively supports Buffer
|
|
// for `put`, whereas `multipartUpload` expects a file path or a browser File object
|
|
// (which causes "FileReader is not defined" when polyfilled).
|
|
await ossUploader.upload({
|
|
filePath,
|
|
file: buffer as unknown as File,
|
|
options: {
|
|
headers: {
|
|
'Content-Type': mimeType,
|
|
'Content-Disposition': 'inline',
|
|
},
|
|
},
|
|
})
|
|
|
|
// 7. Create file record via Nova API
|
|
const lastDotIndex = attachment.fileName.lastIndexOf('.')
|
|
const splitName = lastDotIndex !== -1
|
|
? [attachment.fileName.substring(0, lastDotIndex), attachment.fileName.substring(lastDotIndex + 1)]
|
|
: [attachment.fileName]
|
|
|
|
const safeName = `${splitName[0]}-${Math.random().toString(36).substring(2, 5)}${splitName.length > 1 ? `.${splitName[1]}` : ''}`
|
|
|
|
const res = await novaClient.post<{ file_upload_record_id: string }>(
|
|
'/v1/super_agent/file_upload_record/create',
|
|
{
|
|
file_url: filePath,
|
|
file_type: mimeType,
|
|
file_name: safeName,
|
|
file_byte_size: size,
|
|
conversation_id: uuid,
|
|
},
|
|
)
|
|
|
|
console.log(`[File Uploader] Uploaded: ${safeName} → ${res.file_upload_record_id}`)
|
|
|
|
return {
|
|
fileId: res.file_upload_record_id,
|
|
fileName: safeName,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload multiple bot attachments to Nova OSS and create file records.
|
|
* Returns array of upload_file_ids (only successful uploads).
|
|
* Never throws — individual failures are logged and skipped.
|
|
*/
|
|
export async function uploadBotAttachments(
|
|
attachments: BotAttachment[],
|
|
headers?: Record<string, string>,
|
|
): Promise<string[]> {
|
|
if (attachments.length === 0) return []
|
|
|
|
const results = await Promise.allSettled(
|
|
attachments.map(att => uploadSingleAttachment(att, headers)),
|
|
)
|
|
|
|
const fileIds: string[] = []
|
|
for (const result of results) {
|
|
if (result.status === 'fulfilled' && result.value) {
|
|
fileIds.push(result.value.fileId)
|
|
} else if (result.status === 'rejected') {
|
|
console.error(`[File Uploader] Attachment upload failed:`, result.reason)
|
|
}
|
|
}
|
|
|
|
return fileIds
|
|
}
|