247 lines
8.0 KiB
TypeScript
247 lines
8.0 KiB
TypeScript
import type {
|
|
MindSpace,
|
|
MindSpaceAsset,
|
|
MindSpaceCleanupItem,
|
|
MindSpaceConversationPackage,
|
|
MindSpaceQuota,
|
|
MindSpaceScheduleReminder,
|
|
MindSpaceUpload,
|
|
} from '../types';
|
|
import { CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES } from '../utils/imageUpload';
|
|
import { API, ApiError, apiFetch } from './core';
|
|
|
|
function formatBytes(bytes: number) {
|
|
if (!Number.isFinite(bytes) || bytes < 0) return '0B';
|
|
if (bytes >= 1024 * 1024) {
|
|
const mb = bytes / 1024 / 1024;
|
|
return `${Number.isInteger(mb) ? mb : mb.toFixed(1)}MB`;
|
|
}
|
|
if (bytes >= 1024) {
|
|
const kb = bytes / 1024;
|
|
return `${Number.isInteger(kb) ? kb : kb.toFixed(1)}KB`;
|
|
}
|
|
return `${bytes}B`;
|
|
}
|
|
|
|
function readNumberDetail(details: ApiError['details'], key: string) {
|
|
if (!details || typeof details !== 'object') return null;
|
|
const value = (details as Record<string, unknown>)[key];
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
function isImageUploadFile(file: File) {
|
|
if (file.type.startsWith('image/')) return true;
|
|
return /\.(png|jpe?g|webp|gif)$/i.test(file.name);
|
|
}
|
|
|
|
function normalizeMindSpaceUploadError(error: unknown, file: File): Error {
|
|
if (!(error instanceof ApiError)) {
|
|
return error instanceof Error ? error : new Error('上传失败,请重试');
|
|
}
|
|
|
|
const imageMax = formatBytes(CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES);
|
|
if (error.code === 'quota_exceeded' || error.status === 429) {
|
|
const requiredBytes = readNumberDetail(error.details, 'requiredBytes');
|
|
const availableBytes = readNumberDetail(error.details, 'availableBytes');
|
|
const detail =
|
|
requiredBytes !== null && availableBytes !== null
|
|
? `当前剩余 ${formatBytes(availableBytes)},本次需要 ${formatBytes(requiredBytes)}。`
|
|
: '';
|
|
return new ApiError(
|
|
error.status,
|
|
`剩余空间不足,${detail}请减少图片数量或压缩后重试。`,
|
|
error.code,
|
|
error.details,
|
|
);
|
|
}
|
|
|
|
if (error.code === 'file_too_large' || error.status === 413) {
|
|
const message = isImageUploadFile(file)
|
|
? `图片文件过大,单张图片不能超过 ${imageMax},请压缩后重试。`
|
|
: `文件过大,请压缩到单文件上限以内后重试。`;
|
|
return new ApiError(error.status, message, error.code, error.details);
|
|
}
|
|
|
|
if (/MindSpace\s*服务异常/.test(error.message) || error.code === 'internal_error') {
|
|
return new ApiError(
|
|
error.status,
|
|
`上传失败,请减少图片数量或压缩图片后重试;单张图片上限 ${imageMax}。`,
|
|
error.code,
|
|
error.details,
|
|
);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
export async function getMindSpace(): Promise<MindSpace> {
|
|
const result = await apiFetch<{ data: MindSpace }>('/mindspace/v1/space');
|
|
return result.data;
|
|
}
|
|
|
|
export async function getMindSpaceConversationPackage(
|
|
sessionId: string,
|
|
): Promise<MindSpaceConversationPackage> {
|
|
const result = await apiFetch<{ data: MindSpaceConversationPackage }>(
|
|
`/mindspace/v1/conversation-packages/${encodeURIComponent(sessionId)}`,
|
|
);
|
|
return result.data;
|
|
}
|
|
|
|
export function buildMindSpaceConversationPackageManifestDownloadUrl(sessionId: string): string {
|
|
return `${API}/mindspace/v1/conversation-packages/${encodeURIComponent(sessionId)}/manifest.json`;
|
|
}
|
|
|
|
export async function ignoreMindSpaceScheduleReminder(reminderId: string) {
|
|
const result = await apiFetch<{ data: MindSpaceScheduleReminder }>(
|
|
`/mindspace/v1/schedule/reminders/${encodeURIComponent(reminderId)}/ignore`,
|
|
{ method: 'POST' },
|
|
);
|
|
return result.data;
|
|
}
|
|
|
|
export async function deleteMindSpaceScheduleReminders(ids: string[]) {
|
|
const result = await apiFetch<{ data: { deleted: number } }>(
|
|
'/mindspace/v1/schedule/reminders/bulk-delete',
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ ids }),
|
|
},
|
|
);
|
|
return result.data;
|
|
}
|
|
|
|
export async function listMindSpaceCleanupItems(): Promise<{
|
|
items: MindSpaceCleanupItem[];
|
|
totalBytes: number;
|
|
}> {
|
|
const result = await apiFetch<{
|
|
data: { items: MindSpaceCleanupItem[]; totalBytes: number };
|
|
}>('/mindspace/v1/space/cleanup');
|
|
return result.data;
|
|
}
|
|
|
|
export async function runMindSpaceCleanup(itemIds: string[]): Promise<{
|
|
removedCount: number;
|
|
freedBytes: number;
|
|
quota?: MindSpaceQuota;
|
|
}> {
|
|
const result = await apiFetch<{
|
|
data: { removedCount: number; freedBytes: number; quota?: MindSpaceQuota };
|
|
}>('/mindspace/v1/space/cleanup', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ item_ids: itemIds }),
|
|
});
|
|
return result.data;
|
|
}
|
|
|
|
export async function listMindSpaceAssets(
|
|
categoryCode?: string,
|
|
): Promise<MindSpaceAsset[]> {
|
|
const query = categoryCode ? `?category_code=${encodeURIComponent(categoryCode)}` : '';
|
|
const result = await apiFetch<{ data: MindSpaceAsset[] }>(`/mindspace/v1/assets${query}`);
|
|
return result.data;
|
|
}
|
|
|
|
export async function uploadMindSpaceAsset(
|
|
categoryId: string,
|
|
file: File,
|
|
options: {
|
|
maxImageBytes?: number;
|
|
onProgress?: (progress: number) => void;
|
|
sessionId?: string | null;
|
|
messageId?: string | null;
|
|
} = {},
|
|
): Promise<MindSpaceAsset> {
|
|
const maxImageBytes = options.maxImageBytes ?? CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES;
|
|
if (file.type.startsWith('image/') && file.size > maxImageBytes) {
|
|
throw new ApiError(
|
|
413,
|
|
`图片文件过大,当前 ${(file.size / 1024 / 1024).toFixed(2)}MB,超过 ${maxImageBytes / 1024 / 1024}MB 上传上限。`,
|
|
);
|
|
}
|
|
|
|
let created: { data: MindSpaceUpload };
|
|
try {
|
|
created = await apiFetch<{ data: MindSpaceUpload }>('/mindspace/v1/uploads', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
category_id: categoryId,
|
|
filename: file.name,
|
|
size_bytes: file.size,
|
|
declared_mime_type: file.type || null,
|
|
...(options.sessionId ? { session_id: options.sessionId } : {}),
|
|
...(options.messageId ? { message_id: options.messageId } : {}),
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
throw normalizeMindSpaceUploadError(error, file);
|
|
}
|
|
|
|
try {
|
|
await uploadFileContent(created.data.uploadUrl, file, options.onProgress);
|
|
const completed = await apiFetch<{ data: MindSpaceAsset }>(
|
|
`/mindspace/v1/uploads/${created.data.id}/complete`,
|
|
{ method: 'POST', body: JSON.stringify({}) },
|
|
);
|
|
return completed.data;
|
|
} catch (error) {
|
|
await apiFetch(`/mindspace/v1/uploads/${created.data.id}`, {
|
|
method: 'DELETE',
|
|
}).catch(() => {});
|
|
throw normalizeMindSpaceUploadError(error, file);
|
|
}
|
|
}
|
|
|
|
export async function claimMindSpaceConversationUploads(
|
|
sessionId: string,
|
|
messageId: string,
|
|
): Promise<{ claimedCount: number }> {
|
|
const result = await apiFetch<{ data: { claimedCount: number } }>(
|
|
`/mindspace/v1/conversation-packages/${encodeURIComponent(sessionId)}/claim-uploads`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({ message_id: messageId }),
|
|
},
|
|
);
|
|
return result.data;
|
|
}
|
|
|
|
function uploadFileContent(
|
|
url: string,
|
|
file: File,
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('PUT', url);
|
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
|
xhr.upload.onprogress = (event) => {
|
|
if (!event.lengthComputable || !onProgress) return;
|
|
onProgress(Math.min(0.99, Math.max(0, event.loaded / event.total)));
|
|
};
|
|
xhr.onload = () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
onProgress?.(1);
|
|
resolve();
|
|
return;
|
|
}
|
|
let message = '文件内容上传失败';
|
|
try {
|
|
const body = JSON.parse(xhr.responseText || '{}') as { error?: { message?: string } };
|
|
message = body?.error?.message ?? message;
|
|
} catch {
|
|
// Keep the generic upload error when the response is not JSON.
|
|
}
|
|
reject(new ApiError(xhr.status, message));
|
|
};
|
|
xhr.onerror = () => reject(new ApiError(0, '文件内容上传失败'));
|
|
xhr.onabort = () => reject(new ApiError(0, '文件上传已取消'));
|
|
xhr.send(file);
|
|
});
|
|
}
|
|
|
|
export async function deleteMindSpaceAsset(assetId: string): Promise<void> {
|
|
await apiFetch(`/mindspace/v1/assets/${assetId}`, { method: 'DELETE' });
|
|
}
|