refactor(api): split MindSpace client modules
This commit is contained in:
+46
-840
@@ -5,14 +5,12 @@ import type {
|
||||
CapabilityDefinition,
|
||||
CapabilityMap,
|
||||
ChatSaveAnalysis,
|
||||
ChatSaveResult,
|
||||
PolicyDefinition,
|
||||
PolicyMap,
|
||||
SkillDefinition,
|
||||
SkillMap,
|
||||
BillingConfig,
|
||||
HarnessBootstrapResponse,
|
||||
InsufficientBalanceDetails,
|
||||
LedgerEntry,
|
||||
LlmProviderDefinition,
|
||||
LlmProviderKeyRow,
|
||||
@@ -20,17 +18,8 @@ import type {
|
||||
LlmConnectionTestResult,
|
||||
RechargeOrder,
|
||||
Message,
|
||||
MindSpace,
|
||||
MindSpaceAsset,
|
||||
MindSpaceAgentJob,
|
||||
MindSpaceCleanupItem,
|
||||
MindSpaceConversationPackage,
|
||||
MindSpacePage,
|
||||
MindSpacePageDeletePreview,
|
||||
MindSpacePageDeleteResult,
|
||||
MindSpaceQuota,
|
||||
MindSpaceScheduleReminder,
|
||||
MindSpaceSaveCategory,
|
||||
MindSpacePublication,
|
||||
MindSpacePublicationStats,
|
||||
MindSpacePublishCheck,
|
||||
@@ -41,7 +30,6 @@ import type {
|
||||
PlazaCategory,
|
||||
PlazaPostBrief,
|
||||
MindSpaceRedactedCopyResult,
|
||||
MindSpaceUpload,
|
||||
FeedbackContextInput,
|
||||
FeedbackImageInput,
|
||||
FeedbackSubmission,
|
||||
@@ -61,12 +49,55 @@ import type {
|
||||
ActiveSubscription,
|
||||
UserMemorySyncResponse,
|
||||
} from '../types';
|
||||
import { CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES } from '../utils/imageUpload';
|
||||
import { normalizeConversationMessages, normalizeUserMessageForApi } from '../utils/message';
|
||||
import type { AgentRunCreateOptions, AgentRunValidation } from '../utils/agentRunMode';
|
||||
import {
|
||||
API,
|
||||
ApiError,
|
||||
apiFetch,
|
||||
formatNetworkError,
|
||||
notifyUnauthorized,
|
||||
parseErrorResponse,
|
||||
portalFetch,
|
||||
resetUnauthorizedGuard,
|
||||
sanitizeSessionEvent,
|
||||
} from './core';
|
||||
import { getMindSpacePage, type MindSpaceListPage } from './mindspace-pages';
|
||||
export { ApiError, resetUnauthorizedGuard, setUnauthorizedHandler } from './core';
|
||||
export {
|
||||
buildMindSpaceConversationPackageManifestDownloadUrl,
|
||||
claimMindSpaceConversationUploads,
|
||||
deleteMindSpaceAsset,
|
||||
deleteMindSpaceScheduleReminders,
|
||||
getMindSpace,
|
||||
getMindSpaceConversationPackage,
|
||||
ignoreMindSpaceScheduleReminder,
|
||||
listMindSpaceAssets,
|
||||
listMindSpaceCleanupItems,
|
||||
runMindSpaceCleanup,
|
||||
uploadMindSpaceAsset,
|
||||
} from './mindspace-assets';
|
||||
export {
|
||||
bindMindSpacePageLiveEdit,
|
||||
closeMindSpacePageEditSession,
|
||||
createMindSpacePage,
|
||||
createMindSpacePageFromAsset,
|
||||
deleteMindSpacePage,
|
||||
fetchMindSpacePageDraftPreview,
|
||||
forkMindSpacePageEditSession,
|
||||
getMindSpacePage,
|
||||
getMindSpacePageDeletePreview,
|
||||
getMindSpacePageLiveRevision,
|
||||
listMindSpacePages,
|
||||
openMindSpaceDraftPreviewWindow,
|
||||
regenerateMindSpacePageThumbnail,
|
||||
rewriteMindSpacePageDownloadLinks,
|
||||
saveChatMessageAsPage,
|
||||
type MindSpaceListPage,
|
||||
updateMindSpacePage,
|
||||
uploadMindSpacePageThumbnail,
|
||||
} from './mindspace-pages';
|
||||
|
||||
const API = '/api';
|
||||
const DEFAULT_API_TIMEOUT_MS = 20_000;
|
||||
const AGENT_CONNECT_TIMEOUT_MS = 60_000;
|
||||
const QUICK_PLAZA_TIMEOUT_MS = 120_000;
|
||||
const AGENT_RUNS_PATH = '/agent/runs';
|
||||
@@ -143,323 +174,6 @@ function prepareAgentRunUserMessage(message: Message, options: AgentRunCreateOpt
|
||||
);
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
readonly code?: string;
|
||||
readonly details?: InsufficientBalanceDetails | Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
message: string,
|
||||
code?: string,
|
||||
details?: InsufficientBalanceDetails | Record<string, unknown>,
|
||||
) {
|
||||
super(sanitizeUserFacingErrorMessage(message));
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function sanitizeUserFacingErrorMessage(message: string) {
|
||||
const normalized = String(message ?? '').trim();
|
||||
const serviceName = [103, 111, 111, 115, 101]
|
||||
.map((code) => String.fromCharCode(code))
|
||||
.join('');
|
||||
const servicePattern = new RegExp(`${serviceName}d?`, 'i');
|
||||
if (!normalized) return normalized;
|
||||
if (!servicePattern.test(normalized)) return normalized;
|
||||
if (/超时|timeout/i.test(normalized)) {
|
||||
return '后端连接超时,请确认后端服务正常后重试';
|
||||
}
|
||||
if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) {
|
||||
return '后端连接失败,请稍后重试';
|
||||
}
|
||||
const serviceProcessPattern = new RegExp(`\\b${serviceName}d\\b`, 'gi');
|
||||
const servicePatternGlobal = new RegExp(`\\b${serviceName}\\b`, 'gi');
|
||||
return normalized
|
||||
.replace(serviceProcessPattern, '后端服务')
|
||||
.replace(servicePatternGlobal, '后端');
|
||||
}
|
||||
|
||||
async function parseErrorResponse(res: Response): Promise<{
|
||||
message: string;
|
||||
code?: string;
|
||||
details?: InsufficientBalanceDetails | Record<string, unknown>;
|
||||
}> {
|
||||
const text = await res.text().catch(() => '');
|
||||
try {
|
||||
const body = JSON.parse(text) as Record<string, unknown>;
|
||||
const nested =
|
||||
body.error && typeof body.error === 'object'
|
||||
? (body.error as Record<string, unknown>)
|
||||
: body;
|
||||
const message =
|
||||
typeof nested.message === 'string'
|
||||
? nested.message
|
||||
: typeof body.message === 'string'
|
||||
? body.message
|
||||
: text;
|
||||
const code =
|
||||
typeof nested.code === 'string'
|
||||
? nested.code
|
||||
: typeof body.code === 'string'
|
||||
? body.code
|
||||
: undefined;
|
||||
const details = nested.details ?? body.details;
|
||||
if (code === 'INSUFFICIENT_BALANCE') {
|
||||
return {
|
||||
message: sanitizeUserFacingErrorMessage(message),
|
||||
code,
|
||||
details: {
|
||||
code: 'INSUFFICIENT_BALANCE' as const,
|
||||
balanceCents: Number((details as Record<string, unknown>)?.balanceCents ?? body.balanceCents ?? 0),
|
||||
minRechargeCents: Number(
|
||||
(details as Record<string, unknown>)?.minRechargeCents ?? body.minRechargeCents ?? 500,
|
||||
),
|
||||
suggestedTiers: Array.isArray((details as Record<string, unknown>)?.suggestedTiers)
|
||||
? ((details as Record<string, unknown>).suggestedTiers as unknown[]).map((value) => Number(value))
|
||||
: Array.isArray(body.suggestedTiers)
|
||||
? body.suggestedTiers.map((value) => Number(value))
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: sanitizeUserFacingErrorMessage(message),
|
||||
code,
|
||||
details: details && typeof details === 'object'
|
||||
? (details as InsufficientBalanceDetails | Record<string, unknown>)
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return { message: sanitizeUserFacingErrorMessage(text || res.statusText) };
|
||||
}
|
||||
}
|
||||
|
||||
let unauthorizedHandler: (() => void) | null = null;
|
||||
let unauthorizedHandling = false;
|
||||
|
||||
export function setUnauthorizedHandler(handler: (() => void) | null) {
|
||||
unauthorizedHandler = handler;
|
||||
if (handler) unauthorizedHandling = false;
|
||||
}
|
||||
|
||||
export function resetUnauthorizedGuard() {
|
||||
unauthorizedHandling = false;
|
||||
}
|
||||
|
||||
function notifyUnauthorized() {
|
||||
if (!unauthorizedHandler || unauthorizedHandling) return;
|
||||
unauthorizedHandling = true;
|
||||
unauthorizedHandler();
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
timeoutMs = DEFAULT_API_TIMEOUT_MS,
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const upstreamSignal = init?.signal;
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
const abortFromUpstream = () => controller.abort();
|
||||
if (upstreamSignal) {
|
||||
if (upstreamSignal.aborted) controller.abort();
|
||||
else upstreamSignal.addEventListener('abort', abortFromUpstream, { once: true });
|
||||
}
|
||||
|
||||
try {
|
||||
return await fetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
upstreamSignal?.removeEventListener('abort', abortFromUpstream);
|
||||
}
|
||||
}
|
||||
|
||||
async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(path, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
notifyUnauthorized();
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new ApiError(401, text || '未授权,请重新登录');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const parsed = await parseErrorResponse(res);
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
parsed.message || `${res.status} ${res.statusText}`,
|
||||
parsed.code,
|
||||
parsed.details,
|
||||
);
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
const text = await res.text().catch(() => '');
|
||||
if (text.trimStart().startsWith('<')) {
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
`接口 ${path} 返回了页面而非 JSON,请重启后端(pnpm dev 或 node server.mjs)`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
throw new ApiError(res.status, '服务器响应格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
function formatNetworkError(err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '网络请求失败';
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
return '后端连接超时,请确认后端服务正常后重试';
|
||||
}
|
||||
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
|
||||
return '无法连接后端服务,请先运行: pnpm dev 或 node server.mjs';
|
||||
}
|
||||
return sanitizeUserFacingErrorMessage(message);
|
||||
}
|
||||
|
||||
function sanitizeSessionEvent(event: SessionEvent): SessionEvent {
|
||||
if (event.type !== 'Error') return event;
|
||||
return { ...event, error: sanitizeUserFacingErrorMessage(event.error) };
|
||||
}
|
||||
|
||||
async function apiFetch<T>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
options?: { timeoutMs?: number },
|
||||
): Promise<T> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(
|
||||
`${API}${path}`,
|
||||
{
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init?.headers,
|
||||
},
|
||||
},
|
||||
options?.timeoutMs,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
notifyUnauthorized();
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new ApiError(401, text || '未授权,请重新登录');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const parsed = await parseErrorResponse(res);
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
parsed.message || `${res.status} ${res.statusText}`,
|
||||
parsed.code,
|
||||
parsed.details,
|
||||
);
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
const rawText = await res.text().catch(() => '');
|
||||
if (rawText.trimStart().startsWith('<')) {
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
`接口 ${API}${path} 返回了页面而非 JSON,请重启后端(pnpm dev 或 node server.mjs)`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(rawText) as T;
|
||||
} catch {
|
||||
throw new ApiError(res.status, '服务器响应格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
export async function startSession(): Promise<Session> {
|
||||
return apiFetch<Session>(
|
||||
'/agent/start',
|
||||
@@ -728,293 +442,6 @@ export async function purchaseSpaceQuota(sizeMb: number): Promise<{
|
||||
});
|
||||
}
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
export async function getMindSpacePageDeletePreview(
|
||||
pageId: string,
|
||||
): Promise<MindSpacePageDeletePreview> {
|
||||
const result = await apiFetch<{ data: MindSpacePageDeletePreview }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/delete-preview`,
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function deleteMindSpacePage(
|
||||
pageId: string,
|
||||
options?: { removeFromPlaza?: boolean },
|
||||
): Promise<MindSpacePageDeleteResult> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.removeFromPlaza) params.set('remove_from_plaza', 'true');
|
||||
const query = params.toString() ? `?${params.toString()}` : '';
|
||||
const result = await apiFetch<{ data: MindSpacePageDeleteResult }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}${query}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function listMindSpacePages(options?: {
|
||||
status?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
categoryCode?: string;
|
||||
}): Promise<{ items: MindSpacePage[]; page: MindSpaceListPage }> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.status) params.set('status', options.status);
|
||||
if (options?.limit != null) params.set('limit', String(options.limit));
|
||||
if (options?.offset != null) params.set('offset', String(options.offset));
|
||||
if (options?.categoryCode) params.set('category_code', options.categoryCode);
|
||||
const query = params.toString() ? `?${params.toString()}` : '';
|
||||
const result = await apiFetch<{ data: MindSpacePage[]; page?: MindSpaceListPage }>(
|
||||
`/mindspace/v1/pages${query}`,
|
||||
);
|
||||
return { items: result.data, page: result.page ?? {} };
|
||||
}
|
||||
|
||||
export async function getMindSpacePage(pageId: string): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: MindSpacePage }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function bindMindSpacePageLiveEdit(
|
||||
pageId: string,
|
||||
sessionId: string,
|
||||
options?: { parentSessionId?: string },
|
||||
): Promise<{ sessionId: string; pageId: string; parentSessionId?: string }> {
|
||||
const result = await apiFetch<{ data: { sessionId: string; pageId: string; parentSessionId?: string } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/bind`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
...(options?.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function forkMindSpacePageEditSession(
|
||||
pageId: string,
|
||||
parentSessionId: string,
|
||||
h5ApiBase?: string | null,
|
||||
): Promise<{ sessionId: string; pageId: string; parentSessionId: string }> {
|
||||
const result = await apiFetch<{
|
||||
data: { sessionId: string; pageId: string; parentSessionId: string };
|
||||
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/fork-session`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
parent_session_id: parentSessionId,
|
||||
...(h5ApiBase ? { h5_api_base: h5ApiBase } : {}),
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function closeMindSpacePageEditSession(
|
||||
pageId: string,
|
||||
input: {
|
||||
sessionId: string;
|
||||
parentSessionId?: string | null;
|
||||
summary?: string;
|
||||
},
|
||||
): Promise<{ sessionId: string; pageId: string; merged: boolean }> {
|
||||
const result = await apiFetch<{
|
||||
data: { sessionId: string; pageId: string; merged: boolean };
|
||||
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/close-session`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: input.sessionId,
|
||||
parent_session_id: input.parentSessionId ?? undefined,
|
||||
summary: input.summary ?? '',
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function getMindSpacePageLiveRevision(pageId: string): Promise<{
|
||||
pageId: string;
|
||||
versionNo: number;
|
||||
updatedAt: number;
|
||||
liveRevision: number;
|
||||
}> {
|
||||
const result = await apiFetch<{
|
||||
data: { pageId: string; versionNo: number; updatedAt: number; liveRevision: number };
|
||||
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/revision`);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function analyzeChatSave(input: {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
@@ -1174,220 +601,6 @@ export async function fetchPreviewAsset(path: string): Promise<string> {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export async function saveChatMessageAsPage(input: {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
templateId?: string;
|
||||
categoryCode?: MindSpaceSaveCategory;
|
||||
selectedLinkIndex?: number;
|
||||
acknowledgedFindingIds?: string[];
|
||||
replacePageId?: string;
|
||||
saveAsNew?: boolean;
|
||||
}): Promise<ChatSaveResult> {
|
||||
const result = await apiFetch<{ data: ChatSaveResult }>(
|
||||
'/mindspace/v1/pages/save-from-chat',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: input.sessionId,
|
||||
message_id: input.messageId,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
template_id: input.templateId ?? 'editorial',
|
||||
category_code: input.categoryCode ?? 'draft',
|
||||
selected_link_index: input.selectedLinkIndex ?? 0,
|
||||
acknowledged_finding_ids: input.acknowledgedFindingIds,
|
||||
page_type: input.templateId === 'report' ? 'report' : 'article',
|
||||
replace_page_id: input.replacePageId,
|
||||
save_as_new: input.saveAsNew ?? false,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function createMindSpacePageFromAsset(input: {
|
||||
assetId: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
}): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: { page: MindSpacePage } }>(
|
||||
'/mindspace/v1/pages/from-asset',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
asset_id: input.assetId,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data.page;
|
||||
}
|
||||
|
||||
export async function createMindSpacePage(input: {
|
||||
title: string;
|
||||
summary?: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
}): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: MindSpacePage }>('/mindspace/v1/pages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
content: input.content,
|
||||
template_id: input.templateId,
|
||||
page_type: input.templateId === 'report' ? 'report' : 'article',
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function updateMindSpacePage(
|
||||
pageId: string,
|
||||
input: {
|
||||
expectedVersion: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
changeNote?: string;
|
||||
},
|
||||
): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: MindSpacePage }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
expected_version: input.expectedVersion,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
content: input.content,
|
||||
template_id: input.templateId,
|
||||
page_type: input.templateId === 'report' ? 'report' : 'article',
|
||||
change_note: input.changeNote,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function rewriteMindSpacePageDownloadLinks(
|
||||
pageId: string,
|
||||
content: string,
|
||||
): Promise<string> {
|
||||
const result = await apiFetch<{ data: { html: string } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/rewrite-download-links`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
},
|
||||
);
|
||||
return result.data.html;
|
||||
}
|
||||
|
||||
export async function fetchMindSpacePageDraftPreview(
|
||||
pageId: string,
|
||||
input: {
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
},
|
||||
): Promise<string> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api/mindspace/v1/pages/${encodeURIComponent(pageId)}/preview-draft`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
content: input.content,
|
||||
template_id: input.templateId,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
if (res.status === 401) {
|
||||
notifyUnauthorized();
|
||||
throw new ApiError(401, '未授权,请重新登录');
|
||||
}
|
||||
if (!res.ok) {
|
||||
const parsed = await parseErrorResponse(res);
|
||||
throw new ApiError(res.status, parsed.message || `${res.status} ${res.statusText}`, parsed.code);
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
export function openMindSpaceDraftPreviewWindow(html: string) {
|
||||
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const opened = window.open(url, '_blank', 'noopener,noreferrer');
|
||||
if (!opened) {
|
||||
URL.revokeObjectURL(url);
|
||||
throw new ApiError(0, '无法打开新窗口,请检查浏览器是否拦截弹窗');
|
||||
}
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 120_000);
|
||||
}
|
||||
|
||||
export async function uploadMindSpacePageThumbnail(
|
||||
pageId: string,
|
||||
input: {
|
||||
imageBase64: string;
|
||||
mimeType?: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
content?: string;
|
||||
},
|
||||
): Promise<{ updatedAt: number }> {
|
||||
const result = await apiFetch<{ data: { updatedAt: number } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
image_base64: input.imageBase64,
|
||||
mime_type: input.mimeType,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
html: input.content,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function regenerateMindSpacePageThumbnail(
|
||||
pageId: string,
|
||||
input: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
content?: string;
|
||||
useAi?: boolean;
|
||||
instruction?: string;
|
||||
} = {},
|
||||
): Promise<{ updatedAt: number; content?: string | null }> {
|
||||
const result = await apiFetch<{ data: { updatedAt: number; content?: string | null } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/regenerate`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
html: input.content,
|
||||
use_ai: input.useAi ?? false,
|
||||
instruction: input.instruction,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function checkMindSpacePagePublication(
|
||||
pageId: string,
|
||||
input: {
|
||||
@@ -1768,13 +981,6 @@ export async function getMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgen
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export type MindSpaceListPage = {
|
||||
total?: number;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
has_more?: boolean;
|
||||
};
|
||||
|
||||
export async function listMindSpaceAgentJobs(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
import type { InsufficientBalanceDetails, SessionEvent } from '../types';
|
||||
|
||||
export const API = '/api';
|
||||
const DEFAULT_API_TIMEOUT_MS = 20_000;
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number;
|
||||
readonly code?: string;
|
||||
readonly details?: InsufficientBalanceDetails | Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
message: string,
|
||||
code?: string,
|
||||
details?: InsufficientBalanceDetails | Record<string, unknown>,
|
||||
) {
|
||||
super(sanitizeUserFacingErrorMessage(message));
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeUserFacingErrorMessage(message: string) {
|
||||
const normalized = String(message ?? '').trim();
|
||||
const serviceName = [103, 111, 111, 115, 101]
|
||||
.map((code) => String.fromCharCode(code))
|
||||
.join('');
|
||||
const servicePattern = new RegExp(`${serviceName}d?`, 'i');
|
||||
if (!normalized) return normalized;
|
||||
if (!servicePattern.test(normalized)) return normalized;
|
||||
if (/超时|timeout/i.test(normalized)) {
|
||||
return '后端连接超时,请确认后端服务正常后重试';
|
||||
}
|
||||
if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) {
|
||||
return '后端连接失败,请稍后重试';
|
||||
}
|
||||
const serviceProcessPattern = new RegExp(`\\b${serviceName}d\\b`, 'gi');
|
||||
const servicePatternGlobal = new RegExp(`\\b${serviceName}\\b`, 'gi');
|
||||
return normalized
|
||||
.replace(serviceProcessPattern, '后端服务')
|
||||
.replace(servicePatternGlobal, '后端');
|
||||
}
|
||||
|
||||
export async function parseErrorResponse(res: Response): Promise<{
|
||||
message: string;
|
||||
code?: string;
|
||||
details?: InsufficientBalanceDetails | Record<string, unknown>;
|
||||
}> {
|
||||
const text = await res.text().catch(() => '');
|
||||
try {
|
||||
const body = JSON.parse(text) as Record<string, unknown>;
|
||||
const nested =
|
||||
body.error && typeof body.error === 'object'
|
||||
? (body.error as Record<string, unknown>)
|
||||
: body;
|
||||
const message =
|
||||
typeof nested.message === 'string'
|
||||
? nested.message
|
||||
: typeof body.message === 'string'
|
||||
? body.message
|
||||
: text;
|
||||
const code =
|
||||
typeof nested.code === 'string'
|
||||
? nested.code
|
||||
: typeof body.code === 'string'
|
||||
? body.code
|
||||
: undefined;
|
||||
const details = nested.details ?? body.details;
|
||||
if (code === 'INSUFFICIENT_BALANCE') {
|
||||
return {
|
||||
message: sanitizeUserFacingErrorMessage(message),
|
||||
code,
|
||||
details: {
|
||||
code: 'INSUFFICIENT_BALANCE' as const,
|
||||
balanceCents: Number((details as Record<string, unknown>)?.balanceCents ?? body.balanceCents ?? 0),
|
||||
minRechargeCents: Number(
|
||||
(details as Record<string, unknown>)?.minRechargeCents ?? body.minRechargeCents ?? 500,
|
||||
),
|
||||
suggestedTiers: Array.isArray((details as Record<string, unknown>)?.suggestedTiers)
|
||||
? ((details as Record<string, unknown>).suggestedTiers as unknown[]).map((value) => Number(value))
|
||||
: Array.isArray(body.suggestedTiers)
|
||||
? body.suggestedTiers.map((value) => Number(value))
|
||||
: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: sanitizeUserFacingErrorMessage(message),
|
||||
code,
|
||||
details: details && typeof details === 'object'
|
||||
? (details as InsufficientBalanceDetails | Record<string, unknown>)
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return { message: sanitizeUserFacingErrorMessage(text || res.statusText) };
|
||||
}
|
||||
}
|
||||
|
||||
let unauthorizedHandler: (() => void) | null = null;
|
||||
let unauthorizedHandling = false;
|
||||
|
||||
export function setUnauthorizedHandler(handler: (() => void) | null) {
|
||||
unauthorizedHandler = handler;
|
||||
if (handler) unauthorizedHandling = false;
|
||||
}
|
||||
|
||||
export function resetUnauthorizedGuard() {
|
||||
unauthorizedHandling = false;
|
||||
}
|
||||
|
||||
export function notifyUnauthorized() {
|
||||
if (!unauthorizedHandler || unauthorizedHandling) return;
|
||||
unauthorizedHandling = true;
|
||||
unauthorizedHandler();
|
||||
}
|
||||
|
||||
export async function fetchWithTimeout(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
timeoutMs = DEFAULT_API_TIMEOUT_MS,
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const upstreamSignal = init?.signal;
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
const abortFromUpstream = () => controller.abort();
|
||||
if (upstreamSignal) {
|
||||
if (upstreamSignal.aborted) controller.abort();
|
||||
else upstreamSignal.addEventListener('abort', abortFromUpstream, { once: true });
|
||||
}
|
||||
|
||||
try {
|
||||
return await fetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
upstreamSignal?.removeEventListener('abort', abortFromUpstream);
|
||||
}
|
||||
}
|
||||
|
||||
export async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(path, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
notifyUnauthorized();
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new ApiError(401, text || '未授权,请重新登录');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const parsed = await parseErrorResponse(res);
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
parsed.message || `${res.status} ${res.statusText}`,
|
||||
parsed.code,
|
||||
parsed.details,
|
||||
);
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
const text = await res.text().catch(() => '');
|
||||
if (text.trimStart().startsWith('<')) {
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
`接口 ${path} 返回了页面而非 JSON,请重启后端(pnpm dev 或 node server.mjs)`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
throw new ApiError(res.status, '服务器响应格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
export function formatNetworkError(err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '网络请求失败';
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
return '后端连接超时,请确认后端服务正常后重试';
|
||||
}
|
||||
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
|
||||
return '无法连接后端服务,请先运行: pnpm dev 或 node server.mjs';
|
||||
}
|
||||
return sanitizeUserFacingErrorMessage(message);
|
||||
}
|
||||
|
||||
export function sanitizeSessionEvent(event: SessionEvent): SessionEvent {
|
||||
if (event.type !== 'Error') return event;
|
||||
return { ...event, error: sanitizeUserFacingErrorMessage(event.error) };
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
options?: { timeoutMs?: number },
|
||||
): Promise<T> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(
|
||||
`${API}${path}`,
|
||||
{
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...init?.headers,
|
||||
},
|
||||
},
|
||||
options?.timeoutMs,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
notifyUnauthorized();
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new ApiError(401, text || '未授权,请重新登录');
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const parsed = await parseErrorResponse(res);
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
parsed.message || `${res.status} ${res.statusText}`,
|
||||
parsed.code,
|
||||
parsed.details,
|
||||
);
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
const rawText = await res.text().catch(() => '');
|
||||
if (rawText.trimStart().startsWith('<')) {
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
`接口 ${API}${path} 返回了页面而非 JSON,请重启后端(pnpm dev 或 node server.mjs)`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(rawText) as T;
|
||||
} catch {
|
||||
throw new ApiError(res.status, '服务器响应格式错误');
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import type {
|
||||
ChatSaveResult,
|
||||
MindSpacePage,
|
||||
MindSpacePageDeletePreview,
|
||||
MindSpacePageDeleteResult,
|
||||
MindSpaceSaveCategory,
|
||||
} from '../types';
|
||||
import {
|
||||
ApiError,
|
||||
apiFetch,
|
||||
formatNetworkError,
|
||||
notifyUnauthorized,
|
||||
parseErrorResponse,
|
||||
} from './core';
|
||||
|
||||
export type MindSpaceListPage = {
|
||||
total?: number;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
has_more?: boolean;
|
||||
};
|
||||
|
||||
export async function getMindSpacePageDeletePreview(
|
||||
pageId: string,
|
||||
): Promise<MindSpacePageDeletePreview> {
|
||||
const result = await apiFetch<{ data: MindSpacePageDeletePreview }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/delete-preview`,
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function deleteMindSpacePage(
|
||||
pageId: string,
|
||||
options?: { removeFromPlaza?: boolean },
|
||||
): Promise<MindSpacePageDeleteResult> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.removeFromPlaza) params.set('remove_from_plaza', 'true');
|
||||
const query = params.toString() ? `?${params.toString()}` : '';
|
||||
const result = await apiFetch<{ data: MindSpacePageDeleteResult }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}${query}`,
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function listMindSpacePages(options?: {
|
||||
status?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
categoryCode?: string;
|
||||
}): Promise<{ items: MindSpacePage[]; page: MindSpaceListPage }> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.status) params.set('status', options.status);
|
||||
if (options?.limit != null) params.set('limit', String(options.limit));
|
||||
if (options?.offset != null) params.set('offset', String(options.offset));
|
||||
if (options?.categoryCode) params.set('category_code', options.categoryCode);
|
||||
const query = params.toString() ? `?${params.toString()}` : '';
|
||||
const result = await apiFetch<{ data: MindSpacePage[]; page?: MindSpaceListPage }>(
|
||||
`/mindspace/v1/pages${query}`,
|
||||
);
|
||||
return { items: result.data, page: result.page ?? {} };
|
||||
}
|
||||
|
||||
export async function getMindSpacePage(pageId: string): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: MindSpacePage }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function saveChatMessageAsPage(input: {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
templateId?: string;
|
||||
categoryCode?: MindSpaceSaveCategory;
|
||||
selectedLinkIndex?: number;
|
||||
acknowledgedFindingIds?: string[];
|
||||
replacePageId?: string;
|
||||
saveAsNew?: boolean;
|
||||
}): Promise<ChatSaveResult> {
|
||||
const result = await apiFetch<{ data: ChatSaveResult }>(
|
||||
'/mindspace/v1/pages/save-from-chat',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: input.sessionId,
|
||||
message_id: input.messageId,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
template_id: input.templateId ?? 'editorial',
|
||||
category_code: input.categoryCode ?? 'draft',
|
||||
selected_link_index: input.selectedLinkIndex ?? 0,
|
||||
acknowledged_finding_ids: input.acknowledgedFindingIds,
|
||||
page_type: input.templateId === 'report' ? 'report' : 'article',
|
||||
replace_page_id: input.replacePageId,
|
||||
save_as_new: input.saveAsNew ?? false,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function createMindSpacePageFromAsset(input: {
|
||||
assetId: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
}): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: { page: MindSpacePage } }>(
|
||||
'/mindspace/v1/pages/from-asset',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
asset_id: input.assetId,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data.page;
|
||||
}
|
||||
|
||||
export async function createMindSpacePage(input: {
|
||||
title: string;
|
||||
summary?: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
}): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: MindSpacePage }>('/mindspace/v1/pages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
content: input.content,
|
||||
template_id: input.templateId,
|
||||
page_type: input.templateId === 'report' ? 'report' : 'article',
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function updateMindSpacePage(
|
||||
pageId: string,
|
||||
input: {
|
||||
expectedVersion: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
changeNote?: string;
|
||||
},
|
||||
): Promise<MindSpacePage> {
|
||||
const result = await apiFetch<{ data: MindSpacePage }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
expected_version: input.expectedVersion,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
content: input.content,
|
||||
template_id: input.templateId,
|
||||
page_type: input.templateId === 'report' ? 'report' : 'article',
|
||||
change_note: input.changeNote,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function rewriteMindSpacePageDownloadLinks(
|
||||
pageId: string,
|
||||
content: string,
|
||||
): Promise<string> {
|
||||
const result = await apiFetch<{ data: { html: string } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/rewrite-download-links`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
},
|
||||
);
|
||||
return result.data.html;
|
||||
}
|
||||
|
||||
export async function fetchMindSpacePageDraftPreview(
|
||||
pageId: string,
|
||||
input: {
|
||||
title: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
templateId: string;
|
||||
},
|
||||
): Promise<string> {
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api/mindspace/v1/pages/${encodeURIComponent(pageId)}/preview-draft`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
content: input.content,
|
||||
template_id: input.templateId,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
throw new ApiError(0, formatNetworkError(err));
|
||||
}
|
||||
if (res.status === 401) {
|
||||
notifyUnauthorized();
|
||||
throw new ApiError(401, '未授权,请重新登录');
|
||||
}
|
||||
if (!res.ok) {
|
||||
const parsed = await parseErrorResponse(res);
|
||||
throw new ApiError(res.status, parsed.message || `${res.status} ${res.statusText}`, parsed.code);
|
||||
}
|
||||
return res.text();
|
||||
}
|
||||
|
||||
export function openMindSpaceDraftPreviewWindow(html: string) {
|
||||
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const opened = window.open(url, '_blank', 'noopener,noreferrer');
|
||||
if (!opened) {
|
||||
URL.revokeObjectURL(url);
|
||||
throw new ApiError(0, '无法打开新窗口,请检查浏览器是否拦截弹窗');
|
||||
}
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 120_000);
|
||||
}
|
||||
|
||||
export async function uploadMindSpacePageThumbnail(
|
||||
pageId: string,
|
||||
input: {
|
||||
imageBase64: string;
|
||||
mimeType?: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
content?: string;
|
||||
},
|
||||
): Promise<{ updatedAt: number }> {
|
||||
const result = await apiFetch<{ data: { updatedAt: number } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
image_base64: input.imageBase64,
|
||||
mime_type: input.mimeType,
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
html: input.content,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function regenerateMindSpacePageThumbnail(
|
||||
pageId: string,
|
||||
input: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
content?: string;
|
||||
useAi?: boolean;
|
||||
instruction?: string;
|
||||
} = {},
|
||||
): Promise<{ updatedAt: number; content?: string | null }> {
|
||||
const result = await apiFetch<{ data: { updatedAt: number; content?: string | null } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/regenerate`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title: input.title,
|
||||
summary: input.summary,
|
||||
html: input.content,
|
||||
use_ai: input.useAi ?? false,
|
||||
instruction: input.instruction,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function bindMindSpacePageLiveEdit(
|
||||
pageId: string,
|
||||
sessionId: string,
|
||||
options?: { parentSessionId?: string },
|
||||
): Promise<{ sessionId: string; pageId: string; parentSessionId?: string }> {
|
||||
const result = await apiFetch<{ data: { sessionId: string; pageId: string; parentSessionId?: string } }>(
|
||||
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/bind`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
...(options?.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function forkMindSpacePageEditSession(
|
||||
pageId: string,
|
||||
parentSessionId: string,
|
||||
h5ApiBase?: string | null,
|
||||
): Promise<{ sessionId: string; pageId: string; parentSessionId: string }> {
|
||||
const result = await apiFetch<{
|
||||
data: { sessionId: string; pageId: string; parentSessionId: string };
|
||||
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/fork-session`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
parent_session_id: parentSessionId,
|
||||
...(h5ApiBase ? { h5_api_base: h5ApiBase } : {}),
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function closeMindSpacePageEditSession(
|
||||
pageId: string,
|
||||
input: {
|
||||
sessionId: string;
|
||||
parentSessionId?: string | null;
|
||||
summary?: string;
|
||||
},
|
||||
): Promise<{ sessionId: string; pageId: string; merged: boolean }> {
|
||||
const result = await apiFetch<{
|
||||
data: { sessionId: string; pageId: string; merged: boolean };
|
||||
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/close-session`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id: input.sessionId,
|
||||
parent_session_id: input.parentSessionId ?? undefined,
|
||||
summary: input.summary ?? '',
|
||||
}),
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function getMindSpacePageLiveRevision(pageId: string): Promise<{
|
||||
pageId: string;
|
||||
versionNo: number;
|
||||
updatedAt: number;
|
||||
liveRevision: number;
|
||||
}> {
|
||||
const result = await apiFetch<{
|
||||
data: { pageId: string; versionNo: number; updatedAt: number; liveRevision: number };
|
||||
}>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/revision`);
|
||||
return result.data;
|
||||
}
|
||||
Reference in New Issue
Block a user