refactor(api): split MindSpace client modules

This commit is contained in:
john
2026-07-14 15:26:10 +08:00
parent f2d0c99f6c
commit f6dcf5b14a
4 changed files with 900 additions and 840 deletions
+246
View File
@@ -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' });
}