mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 10:31:50 +08:00
ff1e3d9c94
#### PR Dependency Tree * **PR #15485** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Upgraded built-in AI actions and chat model selection to Gemini 3.7 Flash. * Transcript attachments now retain storage keys and support secure presigned URLs during processing. * Completed transcript results preserve attachment metadata for reliable access. * **Bug Fixes** * Improved transcript handling across uploads, retries, and native processing. * Added safeguards for invalid or missing attachment URLs. * **Maintenance** * Added a migration to backfill storage keys for existing transcript records without altering their URLs. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
|
|
import { Injectable } from '@nestjs/common';
|
|
|
|
import {
|
|
type BlobInputType,
|
|
BlobQuotaExceeded,
|
|
CallMetric,
|
|
type FileUpload,
|
|
OneMB,
|
|
readBuffer,
|
|
toBuffer,
|
|
URLHelper,
|
|
} from '../../base';
|
|
import { QuotaService } from '../../core/quota';
|
|
import {
|
|
type StorageRuntimeGetObjectResult,
|
|
StorageRuntimeProvider,
|
|
} from '../../core/storage-runtime';
|
|
import { fetchRemoteAttachment } from '../../native';
|
|
|
|
const REMOTE_BLOB_MAX_BYTES = 20 * OneMB;
|
|
|
|
@Injectable()
|
|
export class CopilotStorage {
|
|
constructor(
|
|
private readonly url: URLHelper,
|
|
private readonly rt: StorageRuntimeProvider,
|
|
private readonly quota: QuotaService
|
|
) {}
|
|
|
|
@CallMetric('ai', 'blob_put')
|
|
async put(
|
|
userId: string,
|
|
workspaceId: string,
|
|
key: string,
|
|
blob: BlobInputType,
|
|
mimeType = 'image/png'
|
|
) {
|
|
const name = `${userId}/${workspaceId}/${key}`;
|
|
const buffer = await toBuffer(blob);
|
|
await this.rt.putObject('copilot', name, buffer, {
|
|
contentType: mimeType,
|
|
contentLength: buffer.length,
|
|
});
|
|
if (!env.prod) {
|
|
// return image base64url for dev environment
|
|
return `data:${mimeType};base64,${buffer.toString('base64')}`;
|
|
}
|
|
return this.url.link(`/api/copilot/blob/${name}`);
|
|
}
|
|
|
|
@CallMetric('ai', 'blob_get')
|
|
async get(
|
|
userId: string,
|
|
workspaceId: string,
|
|
key: string,
|
|
signedUrl?: boolean
|
|
): Promise<StorageRuntimeGetObjectResult> {
|
|
const name = `${userId}/${workspaceId}/${key}`;
|
|
if (signedUrl) {
|
|
const redirectUrl = await this.presignGet(userId, workspaceId, key);
|
|
if (redirectUrl) return { redirectUrl };
|
|
}
|
|
return this.rt.getObject('copilot', name);
|
|
}
|
|
|
|
async presignGet(userId: string, workspaceId: string, key: string) {
|
|
return (
|
|
await this.rt.presignGet('copilot', `${userId}/${workspaceId}/${key}`)
|
|
)?.url;
|
|
}
|
|
|
|
keyFromUrl(userId: string, workspaceId: string, url: string) {
|
|
try {
|
|
const parsed = new URL(url);
|
|
const prefix = `/api/copilot/blob/${encodeURIComponent(userId)}/${encodeURIComponent(workspaceId)}/`;
|
|
if (parsed.pathname.startsWith(prefix)) {
|
|
return decodeURIComponent(parsed.pathname.slice(prefix.length));
|
|
}
|
|
} catch {
|
|
return;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
@CallMetric('ai', 'blob_delete')
|
|
async delete(userId: string, workspaceId: string, key: string) {
|
|
await this.rt.deleteObject('copilot', `${userId}/${workspaceId}/${key}`);
|
|
}
|
|
|
|
@CallMetric('ai', 'blob_upload')
|
|
async handleUpload(userId: string, blob: FileUpload) {
|
|
const checkExceeded = await this.quota.getUserQuotaCalculator(userId);
|
|
|
|
if (checkExceeded(0)) {
|
|
throw new BlobQuotaExceeded();
|
|
}
|
|
|
|
const buffer = await readBuffer(blob.createReadStream(), checkExceeded);
|
|
|
|
return {
|
|
buffer,
|
|
filename: blob.filename,
|
|
};
|
|
}
|
|
|
|
@CallMetric('ai', 'blob_proxy_remote_url')
|
|
async handleRemoteLink(userId: string, workspaceId: string, link: string) {
|
|
const { body, mimeType } = await fetchRemoteAttachment({
|
|
url: link,
|
|
maxBytes: REMOTE_BLOB_MAX_BYTES,
|
|
expectedContentTypePrefix: 'image/',
|
|
maxImageHeight: 4096,
|
|
maxImageWidth: 4096,
|
|
});
|
|
const buffer = Buffer.from(body);
|
|
const filename = createHash('sha256').update(buffer).digest('base64url');
|
|
return this.put(userId, workspaceId, filename, buffer, mimeType);
|
|
}
|
|
}
|