// Nanobot API client — single-user direct mode. import type { AuthzBackendRecord, AuthzChannelSettings, AuthzRegisterBackendResponse, AuthzStatus, AuthUser, ChatMessage, CronJob, FileAttachment, Marketplace, MarketplacePlugin, PluginInfo, Session, SessionDetail, Skill, SlashCommand, SystemStatus, TokenResponse, OutlookConnectionPayload, OutlookConnectionTestResult, OutlookConnectResult, OutlookEventListResponse, OutlookMessageDetail, OutlookMessageListResponse, OutlookOverview, OutlookStatus, UiAgentDescriptor, UiMcpServerDescriptor, WsEvent, } from '@/types'; const API_URL = process.env.NEXT_PUBLIC_API_URL?.trim(); const WS_URL = process.env.NEXT_PUBLIC_WS_URL?.trim(); const DEFAULT_API_URL = 'http://127.0.0.1:18080'; const ACCESS_TOKEN_KEY = 'nanobot_access_token'; const REFRESH_TOKEN_KEY = 'nanobot_refresh_token'; const REQUEST_TIMEOUT_MS = 8000; const OUTLOOK_REQUEST_TIMEOUT_MS = 45000; function isBrowser(): boolean { return typeof window !== 'undefined'; } function normalizeBaseUrl(value?: string | null): string | null { const trimmed = value?.trim(); if (!trimmed) return null; return trimmed.replace(/\/+$/, ''); } export function buildAuthHandoffUrl(response: TokenResponse, nextPath: string): string | null { const targetBaseUrl = normalizeBaseUrl( response.backend_connection?.frontend_base_url || response.backend_connection?.public_base_url || response.backend_connection?.api_base_url || response.local_backend?.public_base_url ); if (!targetBaseUrl) return null; const handoffCode = response.handoff_code?.trim(); if (!handoffCode) return null; const target = new URL('/handoff', targetBaseUrl); target.searchParams.set('code', handoffCode); if (nextPath) { target.searchParams.set('next', nextPath); } return target.toString(); } function getApiBaseUrl(): string { if (API_URL) return API_URL; if (isBrowser()) return window.location.origin; return DEFAULT_API_URL; } function buildApiUrl(path: string): string { if (!API_URL && isBrowser()) { return path; } return `${getApiBaseUrl()}${path}`; } type FetchJsonOptions = RequestInit & { timeoutMs?: number; }; function withTimeout( signal?: AbortSignal, timeoutMs: number = REQUEST_TIMEOUT_MS ): { signal: AbortSignal; cleanup: () => void } { const controller = new AbortController(); const timeoutId = globalThis.setTimeout(() => { controller.abort(new DOMException('请求超时', 'AbortError')); }, timeoutMs); const forwardAbort = () => controller.abort(signal?.reason); signal?.addEventListener('abort', forwardAbort, { once: true }); return { signal: controller.signal, cleanup: () => { globalThis.clearTimeout(timeoutId); signal?.removeEventListener('abort', forwardAbort); }, }; } // --------------------------------------------------------------------------- // Token management // --------------------------------------------------------------------------- export function getAccessToken(): string | null { if (!isBrowser()) return null; return localStorage.getItem(ACCESS_TOKEN_KEY); } export function getRefreshToken(): string | null { if (!isBrowser()) return null; return localStorage.getItem(REFRESH_TOKEN_KEY); } export function setTokens(access: string, refresh: string): void { if (!isBrowser()) return; localStorage.setItem(ACCESS_TOKEN_KEY, access); localStorage.setItem(REFRESH_TOKEN_KEY, refresh); } export function clearTokens(): void { if (!isBrowser()) return; localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); } export function isLoggedIn(): boolean { return !!getAccessToken(); } // --------------------------------------------------------------------------- // HTTP helpers // --------------------------------------------------------------------------- function authHeaders(includeJsonContentType: boolean = true): Record { const headers: Record = {}; if (includeJsonContentType) { headers['Content-Type'] = 'application/json'; } const token = getAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } return headers; } async function fetchJSON(path: string, options?: FetchJsonOptions): Promise { const mergedHeaders = { ...authHeaders(), ...(options?.headers as Record | undefined), }; const { signal, cleanup } = withTimeout(options?.signal ?? undefined, options?.timeoutMs); let res: Response; try { res = await fetch(buildApiUrl(path), { headers: mergedHeaders, ...options, signal, }); } catch (error) { cleanup(); if (error instanceof DOMException && error.name === 'AbortError') { throw new Error('请求超时'); } throw error; } cleanup(); if (!res.ok) { const text = await res.text(); if (res.status === 401) { clearTokens(); } let detail = text; try { const parsed = JSON.parse(text); if (parsed && typeof parsed.detail === 'string') { detail = parsed.detail; } } catch { // keep raw text } throw new Error(`接口错误 ${res.status}: ${detail}`); } return res.json(); } // --------------------------------------------------------------------------- // Auth API // --------------------------------------------------------------------------- export async function register(username: string, email: string, password: string): Promise { return fetchJSON('/api/auth/register', { method: 'POST', body: JSON.stringify({ username, email, password }), }); } export async function login(username: string, password: string): Promise { return fetchJSON('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }), }); } export async function consumeHandoffCode(code: string): Promise { return fetchJSON('/api/auth/handoff/consume', { method: 'POST', body: JSON.stringify({ code }), }); } export async function logout(): Promise { try { await fetchJSON('/api/auth/logout', { method: 'POST' }); } catch { // ignore logout network failures } finally { clearTokens(); } } export async function getMe(): Promise { return fetchJSON('/api/auth/me'); } // --------------------------------------------------------------------------- // Chat (proxied via /api/) // --------------------------------------------------------------------------- export async function sendMessage( message: string, sessionId: string = 'web:default', attachments?: FileAttachment[] ): Promise<{ response?: string; status?: string; session_id: string }> { const body: Record = { message, session_id: sessionId }; if (attachments && attachments.length > 0) { body.attachments = attachments; } return fetchJSON('/api/chat', { method: 'POST', body: JSON.stringify(body), }); } export function streamMessage( message: string, sessionId: string, onChunk: (content: string) => void, onDone: () => void, onError: (error: string) => void ): () => void { const controller = new AbortController(); (async () => { try { const res = await fetch(buildApiUrl('/api/chat/stream'), { method: 'POST', headers: authHeaders(), body: JSON.stringify({ message, session_id: sessionId }), signal: controller.signal, }); if (!res.ok || !res.body) { onError(`HTTP 错误 ${res.status}`); return; } const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const data = line.slice(6); try { const parsed = JSON.parse(data); if (parsed.type === 'content') { onChunk(parsed.content); } else if (parsed.type === 'done') { onDone(); } else if (parsed.type === 'error') { onError(parsed.error); } } catch { // skip parse errors } } } } catch (err: any) { if (err.name !== 'AbortError') { onError(err.message || '流式请求失败'); } } })(); return () => controller.abort(); } // --------------------------------------------------------------------------- // WebSocket Manager // --------------------------------------------------------------------------- export type WsStatus = 'disconnected' | 'connecting' | 'connected'; export type WsMessageHandler = (data: WsEvent | Record) => void; export type WsStatusListener = (status: WsStatus) => void; function getWsUrl(): string { const baseUrl = WS_URL || (API_URL ? API_URL : isBrowser() ? window.location.origin : DEFAULT_API_URL); const url = new URL(baseUrl); let protocol: 'ws:' | 'wss:'; if (url.protocol === 'ws:' || url.protocol === 'wss:') { protocol = url.protocol; } else if (url.protocol === 'https:') { protocol = 'wss:'; } else { protocol = 'ws:'; } return `${protocol}//${url.host}`; } class WebSocketManager { private ws: WebSocket | null = null; private sessionId: string | null = null; private messageHandlers: WsMessageHandler[] = []; private statusListeners: WsStatusListener[] = []; private status: WsStatus = 'disconnected'; private reconnectTimer: ReturnType | null = null; private pingTimer: ReturnType | null = null; private reconnectDelay = 1000; private maxReconnectDelay = 30000; private intentionalClose = false; connect(sessionId: string): void { if (this.sessionId === sessionId && this.ws?.readyState === globalThis.WebSocket?.OPEN) { return; } this.intentionalClose = false; this.sessionId = sessionId; this.reconnectDelay = 1000; this._connect(); } disconnect(): void { this.intentionalClose = true; this._cleanup(); this._setStatus('disconnected'); } sendMessage(content: string): void { if (this.ws?.readyState === globalThis.WebSocket?.OPEN) { this.ws.send(JSON.stringify({ type: 'message', content })); } } sendRaw(payload: Record): void { if (this.ws?.readyState === globalThis.WebSocket?.OPEN) { this.ws.send(JSON.stringify(payload)); } } onMessage(handler: WsMessageHandler): () => void { this.messageHandlers.push(handler); return () => { this.messageHandlers = this.messageHandlers.filter((h) => h !== handler); }; } onStatusChange(listener: WsStatusListener): () => void { this.statusListeners.push(listener); listener(this.status); return () => { this.statusListeners = this.statusListeners.filter((l) => l !== listener); }; } getStatus(): WsStatus { return this.status; } private _connect(): void { this._cleanup(); if (!this.sessionId) return; this._setStatus('connecting'); const wsUrl = getWsUrl(); const token = getAccessToken(); const query = token ? `?token=${encodeURIComponent(token)}` : ''; const ws = new globalThis.WebSocket(`${wsUrl}/ws/${this.sessionId}${query}`); ws.onopen = () => { this.reconnectDelay = 1000; this._setStatus('connected'); this._startPing(); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'pong') return; for (const handler of this.messageHandlers) { handler(data); } } catch { // ignore parse errors } }; ws.onclose = () => { this._stopPing(); if (!this.intentionalClose) { this._setStatus('disconnected'); this._scheduleReconnect(); } }; ws.onerror = () => { // onclose will fire after onerror }; this.ws = ws; } private _cleanup(): void { this._stopPing(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.onopen = null; this.ws.onmessage = null; this.ws.onclose = null; this.ws.onerror = null; if (this.ws.readyState === globalThis.WebSocket?.OPEN || this.ws.readyState === globalThis.WebSocket?.CONNECTING) { this.ws.close(); } this.ws = null; } } private _scheduleReconnect(): void { if (this.reconnectTimer) return; this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this._connect(); }, this.reconnectDelay); this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay); } private _startPing(): void { this._stopPing(); this.pingTimer = setInterval(() => { if (this.ws?.readyState === globalThis.WebSocket?.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); } private _stopPing(): void { if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = null; } } private _setStatus(status: WsStatus): void { if (this.status === status) return; this.status = status; for (const listener of this.statusListeners) { listener(status); } } } export const wsManager = new WebSocketManager(); // --------------------------------------------------------------------------- // Sessions (proxied) // --------------------------------------------------------------------------- export async function listSessions(): Promise { return fetchJSON('/api/sessions'); } export async function createSession(key: string): Promise { return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`, { method: 'POST' }); } export async function getSession(key: string): Promise { return fetchJSON(`/api/sessions/${encodeURIComponent(key)}`); } export async function deleteSession(key: string): Promise { await fetchJSON(`/api/sessions/${encodeURIComponent(key)}`, { method: 'DELETE' }); } // --------------------------------------------------------------------------- // Status (proxied) // --------------------------------------------------------------------------- export async function getStatus(): Promise { return fetchJSON('/api/status'); } // --------------------------------------------------------------------------- // Cron (proxied) // --------------------------------------------------------------------------- export async function listCronJobs(includeDisabled: boolean = true): Promise { return fetchJSON(`/api/cron/jobs?include_disabled=${includeDisabled}`); } export async function addCronJob(params: { name: string; message: string; every_seconds?: number; cron_expr?: string; at_iso?: string; session_key?: string; }): Promise { return fetchJSON('/api/cron/jobs', { method: 'POST', body: JSON.stringify(params), }); } export async function removeCronJob(jobId: string): Promise { await fetchJSON(`/api/cron/jobs/${jobId}`, { method: 'DELETE' }); } export async function toggleCronJob(jobId: string, enabled: boolean): Promise { return fetchJSON(`/api/cron/jobs/${jobId}/toggle`, { method: 'PUT', body: JSON.stringify({ enabled }), }); } export async function runCronJob(jobId: string): Promise { await fetchJSON(`/api/cron/jobs/${jobId}/run`, { method: 'POST' }); } export async function ping(): Promise<{ message: string }> { return fetchJSON('/api/ping'); } // --------------------------------------------------------------------------- // Skills (proxied) // --------------------------------------------------------------------------- export async function listSkills(): Promise { return fetchJSON('/api/skills'); } export async function listCommands(): Promise { return fetchJSON('/api/commands'); } export async function listPlugins(): Promise { return fetchJSON('/api/plugins'); } export async function listAgents(): Promise { return fetchJSON('/api/agents'); } export async function addAgent(payload: { id?: string; name?: string; description?: string; protocol?: string; base_url?: string; endpoint?: string; card_url?: string; auth_env?: string; auth_mode?: string; auth_audience?: string; auth_scopes?: string[]; enabled?: boolean; tags?: string[]; aliases?: string[]; metadata?: Record; }): Promise { return fetchJSON('/api/agents', { method: 'POST', body: JSON.stringify(payload), }); } export async function deleteAgent(agentId: string): Promise { await fetchJSON(`/api/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' }); } export async function refreshAgents(): Promise<{ agents: UiAgentDescriptor[] }> { return fetchJSON('/api/agents/refresh', { method: 'POST' }); } export async function cancelDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> { return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/cancel`, { method: 'POST', }); } export async function listMcpServers(): Promise { return fetchJSON('/api/mcp/servers'); } export async function addMcpServer(payload: { id: string; command?: string; args?: string[]; env?: Record; url?: string; headers?: Record; auth_mode?: string; auth_audience?: string; auth_scopes?: string[]; tool_timeout?: number; }): Promise { return fetchJSON('/api/mcp/servers', { method: 'POST', body: JSON.stringify(payload), }); } export async function updateMcpServer( serverId: string, payload: { id: string; command?: string; args?: string[]; env?: Record; url?: string; headers?: Record; auth_mode?: string; auth_audience?: string; auth_scopes?: string[]; tool_timeout?: number; } ): Promise { return fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}`, { method: 'PUT', body: JSON.stringify(payload), }); } export async function deleteMcpServer(serverId: string): Promise { await fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}`, { method: 'DELETE', }); } export async function getAuthzStatus(): Promise { return fetchJSON('/api/authz/status'); } export async function bindLocalBackendIdentity(payload: { backend_id: string; client_id: string; client_secret: string; name?: string; public_base_url?: string; authz_base_url?: string; authz_enabled?: boolean; }): Promise> { return fetchJSON('/api/authz/local-backend/bind', { method: 'POST', body: JSON.stringify(payload), }); } export async function listAuthzBackends(): Promise { return fetchJSON('/api/authz/backends'); } export async function registerAuthzBackend(payload: { name?: string; backend_id?: string; base_url?: string; save_to_backend?: boolean; authz_base_url?: string; }): Promise { return fetchJSON('/api/authz/backends/register', { method: 'POST', body: JSON.stringify(payload), }); } export async function getAuthzBackend(backendId: string): Promise { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}`); } export async function enableAuthzBackend(backendId: string): Promise { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/enable`, { method: 'POST', }); } export async function disableAuthzBackend(backendId: string): Promise { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/disable`, { method: 'POST', }); } export async function rotateAuthzBackendSecret(backendId: string): Promise> { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/rotate-secret`, { method: 'POST', }); } export async function getAuthzBackendPermissions(backendId: string): Promise> { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`); } export async function setAuthzBackendPermissions( backendId: string, payload: Record ): Promise> { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`, { method: 'POST', body: JSON.stringify(payload), }); } export async function getAuthzBackendOutlookSettings(backendId: string): Promise> { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`); } export async function setAuthzBackendOutlookSettings( backendId: string, payload: Record ): Promise> { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, { method: 'POST', body: JSON.stringify(payload), }); } export async function deleteAuthzBackendOutlookSettings(backendId: string): Promise> { return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, { method: 'DELETE', }); } export async function listAuthzChannelSettings(): Promise> { return fetchJSON('/api/authz/channel-settings'); } export async function getAuthzChannelSettings(channelId: string): Promise { return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`); } export async function setAuthzChannelSettings( channelId: string, payload: { configured?: boolean; config?: Record; secrets?: Record; } ): Promise { return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, { method: 'POST', body: JSON.stringify(payload), }); } export async function deleteAuthzChannelSettings(channelId: string): Promise> { return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, { method: 'DELETE', }); } export async function testMcpServer(serverId: string): Promise> { return fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}/test`, { method: 'POST', }); } export async function listMcpTools(): Promise> }>> { return fetchJSON('/api/mcp/tools'); } export async function getOutlookStatus(): Promise { return fetchJSON('/api/integrations/outlook/status', { timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function testOutlookConnection( payload: OutlookConnectionPayload ): Promise { return fetchJSON('/api/integrations/outlook/test-connection', { method: 'POST', body: JSON.stringify(payload), timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function connectOutlook( payload: OutlookConnectionPayload ): Promise { return fetchJSON('/api/integrations/outlook/connect', { method: 'POST', body: JSON.stringify(payload), timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function disconnectOutlook(): Promise<{ ok: boolean }> { return fetchJSON('/api/integrations/outlook/disconnect', { method: 'POST', timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function getOutlookOverview(): Promise { return fetchJSON('/api/integrations/outlook/overview', { timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function getOutlookMessages( folder: string, options?: { top?: number; skip?: number; unreadOnly?: boolean; } ): Promise { const params = new URLSearchParams({ folder, top: String(options?.top ?? 20), skip: String(options?.skip ?? 0), }); if (options?.unreadOnly) { params.set('unread_only', 'true'); } return fetchJSON(`/api/integrations/outlook/messages?${params.toString()}`, { timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function getOutlookEvents(options: { startTime: string; endTime: string; top?: number; skip?: number; }): Promise { const params = new URLSearchParams({ start_time: options.startTime, end_time: options.endTime, top: String(options.top ?? 20), skip: String(options.skip ?? 0), }); return fetchJSON(`/api/integrations/outlook/events?${params.toString()}`, { timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function getOutlookMessageDetail( messageId: string, changekey?: string | null ): Promise { const params = new URLSearchParams({ message_id: messageId }); if (changekey) { params.set('changekey', changekey); } return fetchJSON(`/api/integrations/outlook/message-detail?${params.toString()}`, { timeoutMs: OUTLOOK_REQUEST_TIMEOUT_MS, }); } export async function downloadSkill(name: string): Promise { const url = buildApiUrl(`/api/skills/${encodeURIComponent(name)}/download`); const res = await fetch(url, { headers: authHeaders(false) }); if (!res.ok) { const text = await res.text(); throw new Error(`下载失败:${text}`); } const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${name}.zip`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); } export async function deleteSkill(name: string): Promise { await fetchJSON(`/api/skills/${encodeURIComponent(name)}`, { method: 'DELETE' }); } export async function uploadSkill(file: File): Promise { const formData = new FormData(); formData.append('file', file); const token = getAccessToken(); const headers: Record = {}; if (token) headers.Authorization = `Bearer ${token}`; const url = buildApiUrl('/api/skills/upload'); const res = await fetch(url, { method: 'POST', headers, body: formData, }); if (!res.ok) { const text = await res.text(); throw new Error(`接口错误 ${res.status}: ${text}`); } return res.json(); } // --------------------------------------------------------------------------- // Marketplace (proxied) // --------------------------------------------------------------------------- export async function listMarketplaces(): Promise { return fetchJSON('/api/marketplaces'); } export async function addMarketplace(source: string): Promise { return fetchJSON('/api/marketplaces', { method: 'POST', body: JSON.stringify({ source }), }); } export async function removeMarketplace(name: string): Promise { await fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}`, { method: 'DELETE', }); } export async function updateMarketplace(name: string): Promise { return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/update`, { method: 'POST', }); } export async function listMarketplacePlugins(name: string): Promise { return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/plugins`); } export async function installMarketplacePlugin(marketplaceName: string, pluginName: string): Promise { await fetchJSON( `/api/marketplaces/${encodeURIComponent(marketplaceName)}/plugins/${encodeURIComponent(pluginName)}/install`, { method: 'POST' } ); } export async function uninstallPlugin(pluginName: string): Promise { await fetchJSON(`/api/plugins/${encodeURIComponent(pluginName)}`, { method: 'DELETE', }); } // --------------------------------------------------------------------------- // Files (proxied) // --------------------------------------------------------------------------- const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB export async function uploadFile( file: File, sessionId: string = 'web:default', onProgress?: (percent: number) => void ): Promise { if (file.size > MAX_FILE_SIZE) { throw new Error('文件过大(最大 50MB)'); } const formData = new FormData(); formData.append('file', file); formData.append('session_id', sessionId); const result = await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', buildApiUrl('/api/files/upload')); const token = getAccessToken(); if (token) { xhr.setRequestHeader('Authorization', `Bearer ${token}`); } xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) { onProgress(Math.round((e.loaded / e.total) * 100)); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { const data = JSON.parse(xhr.responseText); resolve(data); } else { reject(new Error(`上传失败:${xhr.status}`)); } }; xhr.onerror = () => reject(new Error('上传失败')); xhr.send(formData); }); return result; } export async function listFiles(sessionId?: string): Promise { const params = sessionId ? `?session_id=${encodeURIComponent(sessionId)}` : ''; return fetchJSON(`/api/files${params}`); } export async function deleteFile(fileId: string): Promise { await fetchJSON(`/api/files/${encodeURIComponent(fileId)}`, { method: 'DELETE' }); } export function getFileUrl(fileId: string): string { return buildApiUrl(`/api/files/${encodeURIComponent(fileId)}`); } // --------------------------------------------------------------------------- // Workspace Browser // --------------------------------------------------------------------------- export interface WorkspaceItem { name: string; path: string; type: 'file' | 'directory'; size: number | null; content_type?: string; modified: string; } export interface BrowseResult { path: string; items: WorkspaceItem[]; } export async function browseWorkspace(path: string = ''): Promise { const params = path ? `?path=${encodeURIComponent(path)}` : ''; return fetchJSON(`/api/workspace/browse${params}`); } export function getWorkspaceDownloadUrl(path: string): string { return buildApiUrl(`/api/workspace/download?path=${encodeURIComponent(path)}`); } export async function uploadToWorkspace( file: File, dirPath: string = '', onProgress?: (percent: number) => void ): Promise { if (file.size > MAX_FILE_SIZE) { throw new Error('文件过大(最大 50MB)'); } const formData = new FormData(); formData.append('file', file); formData.append('path', dirPath); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', buildApiUrl('/api/workspace/upload')); const token = getAccessToken(); if (token) { xhr.setRequestHeader('Authorization', `Bearer ${token}`); } xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) { onProgress(Math.round((e.loaded / e.total) * 100)); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error(`上传失败:${xhr.status}`)); } }; xhr.onerror = () => reject(new Error('上传失败')); xhr.send(formData); }); } export async function deleteWorkspacePath(path: string): Promise { await fetchJSON(`/api/workspace/delete?path=${encodeURIComponent(path)}`, { method: 'DELETE', }); } export async function createWorkspaceDir(path: string): Promise { return fetchJSON(`/api/workspace/mkdir?path=${encodeURIComponent(path)}`, { method: 'POST', }); }