refactor(api): split MindSpace client modules
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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' });
|
||||
}
|
||||
Reference in New Issue
Block a user