Files
test1/remote-control/shared/file-uploader.ts
2026-03-20 07:33:46 +00:00

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
}