// Beaver API client - single-user direct mode. import type { AuthzStatus, AuthUser, ActiveTask, AgentConfigPayload, ChatLogsResponse, BackendTask, ChatMessage, ChannelConfigDetail, ChannelConfigPayload, ChannelConnectorDescriptor, ConnectorSessionResponse, ConnectorSessionStartPayload, ChannelEventRecord, CronJob, FileAttachment, NotificationDetail, NotificationRun, ProviderConfigPayload, Session, SessionDetail, Skill, SkillDetailResponse, SkillDraft, SkillDraftEvalReport, SkillDraftSafetyReport, SkillFileContent, SkillHubInstallResponse, SkillHubSearchItem, SkillHubSearchResponse, SkillHubVersionResponse, SkillHubVersionsResponse, SkillLearningCandidate, SkillReviewRecord, SessionProcessProjection, SystemStatus, TokenResponse, OutlookConnectionPayload, OutlookConnectionTestResult, OutlookConnectResult, OutlookEventListResponse, OutlookMessageDetail, OutlookMessageListResponse, OutlookOverview, OutlookStatus, UiAgentDescriptor, UiSubagentDescriptor, UiMcpServerDescriptor, WsEvent, } from '@/types'; import { getCurrentAppLocale, pickAppText } from '@/lib/i18n/core'; 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 = 'beaver_access_token'; const REFRESH_TOKEN_KEY = 'beaver_refresh_token'; const REQUEST_TIMEOUT_MS = 8000; const OUTLOOK_REQUEST_TIMEOUT_MS = 45000; const SKILL_LEARNING_REQUEST_TIMEOUT_MS = 120000; 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 locale = getCurrentAppLocale(); const controller = new AbortController(); const timeoutId = globalThis.setTimeout(() => { controller.abort(new DOMException(pickAppText(locale, '请求超时', 'Request timed out'), '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 locale = getCurrentAppLocale(); 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(pickAppText(locale, '请求超时', 'Request timed out')); } 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(`${pickAppText(locale, '接口错误', 'API 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[], options?: { replyToScheduledRunId?: string; scheduledReplyIntent?: 'revise_once' | 'update_future' | 'continue_task'; thinkingEnabled?: boolean; } ): Promise<{ response?: string; status?: string; session_id: string; run_id?: string; task_id?: string | null; task_status?: string | null; evidence_status?: string | null; }> { const body: Record = { message, session_id: sessionId }; if (attachments && attachments.length > 0) { body.attachments = attachments; } if (options?.replyToScheduledRunId) { body.reply_to_scheduled_run_id = options.replyToScheduledRunId; body.scheduled_reply_intent = options.scheduledReplyIntent || 'revise_once'; } if (typeof options?.thinkingEnabled === 'boolean') { body.thinking_enabled = options.thinkingEnabled; } const result = await fetchJSON<{ response?: string; status?: string; session_id: string; run_id?: string; output_text?: string; finish_reason?: string; task_id?: string | null; task_status?: string | null; evidence_status?: string | null; }>('/api/chat', { method: 'POST', body: JSON.stringify(body), }); return { response: result.response ?? result.output_text, status: result.status ?? result.finish_reason, session_id: result.session_id, run_id: result.run_id, task_id: result.task_id, task_status: result.task_status, evidence_status: result.evidence_status, }; } export async function submitChatFeedback(params: { sessionId: string; runId: string; feedbackType: 'accept' | 'revise' | 'abandon'; comment?: string; }): Promise<{ session_id: string; run_id: string; task_id: string; task_status: string; acceptance_type: string; feedback_type: string; }> { return fetchJSON('/api/chat/acceptance', { method: 'POST', body: JSON.stringify({ session_id: params.sessionId, run_id: params.runId, acceptance_type: params.feedbackType, comment: params.comment, }), }); } export function streamMessage( message: string, sessionId: string, onChunk: (content: string) => void, onDone: () => void, onError: (error: string) => void ): () => void { const controller = new AbortController(); const locale = getCurrentAppLocale(); (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(`${pickAppText(locale, 'HTTP 错误', 'HTTP error')} ${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 || pickAppText(locale, '流式请求失败', 'Streaming request failed')); } } })(); 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 getSessionProcess(key: string): Promise { return fetchJSON(`/api/sessions/${encodeURIComponent(key)}/process`); } export async function getChatLogs(limit = 50): Promise { return fetchJSON(`/api/debug/chat-logs?limit=${encodeURIComponent(String(limit))}`, { timeoutMs: 30000, }); } export async function archiveSession(key: string): Promise { await fetchJSON(`/api/sessions/${encodeURIComponent(key)}/archive`, { method: 'POST' }); } // --------------------------------------------------------------------------- // Status (proxied) // --------------------------------------------------------------------------- export async function getStatus(): Promise { return fetchJSON('/api/status'); } export async function updateAgentConfig(payload: AgentConfigPayload): Promise<{ ok: boolean }> { return fetchJSON('/api/agent-config', { method: 'POST', body: JSON.stringify(payload), }); } export async function updateProviderConfig( providerId: string, payload: ProviderConfigPayload ): Promise<{ ok: boolean; provider: string; enabled: boolean }> { return fetchJSON(`/api/providers/${encodeURIComponent(providerId)}/config`, { method: 'POST', body: JSON.stringify(payload), }); } export async function getChannelConfig(channelId: string): Promise { return fetchJSON(`/api/channels/${encodeURIComponent(channelId)}/config`); } export async function updateChannelConfig( channelId: string, payload: ChannelConfigPayload ): Promise<{ ok: boolean; channel_id: string; restart_required: boolean; channel: ChannelConfigDetail }> { return fetchJSON(`/api/channels/${encodeURIComponent(channelId)}/config`, { method: 'POST', body: JSON.stringify(payload), }); } export async function listChannelEvents(channelId: string, limit: number = 100): Promise { return fetchJSON(`/api/channels/${encodeURIComponent(channelId)}/events?limit=${limit}`); } export async function listChannelConnectors(): Promise { return fetchJSON('/api/channel-connectors'); } export async function startChannelConnectorSession( payload: ConnectorSessionStartPayload ): Promise { return fetchJSON('/api/channel-connector-sessions', { method: 'POST', body: JSON.stringify({ kind: payload.kind, displayName: payload.displayName, ownerUserId: payload.ownerUserId, options: payload.options || {}, }), }); } export async function getChannelConnectorSession(sessionId: string): Promise { return fetchJSON(`/api/channel-connector-sessions/${encodeURIComponent(sessionId)}`); } export async function restartRuntime(): Promise<{ ok: boolean; restarting: boolean }> { return fetchJSON('/api/runtime/restart', { method: 'POST', timeoutMs: 5000, }); } // --------------------------------------------------------------------------- // 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; tz?: string; session_key?: string; mode?: 'notification' | 'task'; requires_followup?: boolean; }): 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 listNotifications(): Promise { return fetchJSON('/api/notifications'); } export async function getNotification(scheduledRunId: string): Promise { return fetchJSON(`/api/notifications/${encodeURIComponent(scheduledRunId)}`); } export async function engageNotification( scheduledRunId: string, intent: 'revise_once' | 'update_future' | 'continue_task' ): Promise<{ ok: boolean; task_id: string; intent: string }> { return fetchJSON(`/api/notifications/${encodeURIComponent(scheduledRunId)}/engage`, { method: 'POST', body: JSON.stringify({ intent }), }); } export async function listBackendTasks(): Promise { return fetchJSON('/api/tasks'); } export async function getBackendTask(taskId: string): Promise { return fetchJSON(`/api/tasks/${encodeURIComponent(taskId)}`); } export async function deleteBackendTask(taskId: string): Promise { await fetchJSON(`/api/tasks/${encodeURIComponent(taskId)}`, { method: 'DELETE' }); } export async function getActiveTask(sessionId: string): Promise { return fetchJSON(`/api/sessions/${encodeURIComponent(sessionId)}/active-task`); } 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 getSkillDetail(skillName: string): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/detail`); } export async function getSkillVersion(skillName: string, version: string): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/versions/${encodeURIComponent(version)}`); } export async function getSkillFile(skillName: string, version: string, filePath: string): Promise { const search = new URLSearchParams({ path: filePath }); return fetchJSON( `/api/skills/${encodeURIComponent(skillName)}/versions/${encodeURIComponent(version)}/file?${search.toString()}` ); } export async function listSkillCandidates(status?: string): Promise { const query = status ? `?status=${encodeURIComponent(status)}` : ''; return fetchJSON(`/api/skills/candidates${query}`); } export async function synthesizeSkillDraft(candidateId: string): Promise { return fetchJSON(`/api/skills/candidates/${encodeURIComponent(candidateId)}/draft`, { method: 'POST', body: JSON.stringify({}), timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS, }); } export async function regenerateSkillDraft(candidateId: string): Promise { return fetchJSON(`/api/skills/candidates/${encodeURIComponent(candidateId)}/regenerate`, { method: 'POST', body: JSON.stringify({}), timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS, }); } export async function runSkillLearningOnce(): Promise<{ processed: number; succeeded: number; failed: number; skipped: number; failures: Array>; }> { return fetchJSON('/api/skills/learning/run-once', { method: 'POST', body: JSON.stringify({}), timeoutMs: SKILL_LEARNING_REQUEST_TIMEOUT_MS, }); } export async function listSkillDrafts(): Promise { return fetchJSON('/api/skills/drafts'); } export async function getSkillDraft(skillName: string, draftId: string): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}`); } export async function getSkillDraftSafety(skillName: string, draftId: string): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/safety`); } export async function recheckSkillDraftSafety(skillName: string, draftId: string): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/safety`, { method: 'POST', body: JSON.stringify({}), }); } export async function getSkillDraftEval(skillName: string, draftId: string): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/eval`); } export async function submitSkillDraft( skillName: string, draftId: string, notes: string = '' ): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/submit`, { method: 'POST', body: JSON.stringify({ notes }), }); } export async function approveSkillDraft( skillName: string, draftId: string, notes: string = '' ): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/approve`, { method: 'POST', body: JSON.stringify({ notes }), }); } export async function rejectSkillDraft( skillName: string, draftId: string, notes: string = '' ): Promise { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/reject`, { method: 'POST', body: JSON.stringify({ notes }), }); } export async function publishSkillDraft( skillName: string, draftId: string, notes: string = '', confirmHighRisk: boolean = false ): Promise> { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/drafts/${encodeURIComponent(draftId)}/publish`, { method: 'POST', body: JSON.stringify({ notes, confirm_high_risk: confirmHighRisk }), }); } export async function disablePublishedSkill(skillName: string, reason: string = ''): Promise> { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/disable`, { method: 'POST', body: JSON.stringify({ reason }), }); } export async function rollbackPublishedSkill( skillName: string, targetVersion: string, reason: string = '' ): Promise> { return fetchJSON(`/api/skills/${encodeURIComponent(skillName)}/rollback`, { method: 'POST', body: JSON.stringify({ target_version: targetVersion, reason }), }); } 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 listSubagents(): Promise { return fetchJSON('/api/subagents'); } export async function createSubagent(payload: { id: string; name?: string; description?: string; system_prompt?: string; model?: string; enabled?: boolean; delegation_mode?: string; allow_mcp?: boolean; tags?: string[]; aliases?: string[]; mcp_servers?: Record>; metadata?: Record; }): Promise { return fetchJSON('/api/subagents', { method: 'POST', body: JSON.stringify(payload), }); } export async function updateSubagent( subagentId: string, payload: { id: string; name?: string; description?: string; system_prompt?: string; model?: string; enabled?: boolean; delegation_mode?: string; allow_mcp?: boolean; tags?: string[]; aliases?: string[]; mcp_servers?: Record>; metadata?: Record; } ): Promise { return fetchJSON(`/api/subagents/${encodeURIComponent(subagentId)}`, { method: 'PUT', body: JSON.stringify(payload), }); } export async function deleteSubagent(subagentId: string): Promise { await fetchJSON(`/api/subagents/${encodeURIComponent(subagentId)}`, { method: 'DELETE', }); } 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 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(); } // --------------------------------------------------------------------------- // SkillHub marketplace // --------------------------------------------------------------------------- export async function searchSkillHubSkills(params: { q?: string; sort?: 'relevance' | 'downloads' | 'newest'; page?: number; size?: number; namespace?: string; } = {}): Promise { const search = new URLSearchParams(); if (params.q) search.set('q', params.q); if (params.sort) search.set('sort', params.sort); if (typeof params.page === 'number') search.set('page', String(params.page)); if (typeof params.size === 'number') search.set('size', String(params.size)); if (params.namespace) search.set('namespace', params.namespace); const suffix = search.toString(); return fetchJSON(`/api/marketplaces/skills/search${suffix ? `?${suffix}` : ''}`); } export async function getSkillHubDetail(namespace: string, slug: string): Promise { return fetchJSON( `/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}` ); } export async function getSkillHubVersion( namespace: string, slug: string, version: string ): Promise { return fetchJSON( `/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}` ); } export async function getSkillHubVersions(namespace: string, slug: string): Promise { return fetchJSON( `/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/versions` ); } export async function getSkillHubFile( namespace: string, slug: string, version: string, filePath: string ): Promise { const search = new URLSearchParams({ path: filePath }); return fetchJSON( `/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/file?${search.toString()}` ); } export async function installSkillHubSkill( namespace: string, slug: string, version?: string ): Promise { return fetchJSON( `/api/marketplaces/skills/${encodeURIComponent(namespace.replace(/^@/, ''))}/${encodeURIComponent(slug)}/install`, { method: 'POST', body: JSON.stringify({ version }), timeoutMs: 45000, } ); } // --------------------------------------------------------------------------- // 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 { const locale = getCurrentAppLocale(); if (file.size > MAX_FILE_SIZE) { throw new Error(pickAppText(locale, '文件过大(最大 50MB)', 'File is too large (max 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(`${pickAppText(locale, '上传失败', 'Upload failed')}: ${xhr.status}`)); } }; xhr.onerror = () => reject(new Error(pickAppText(locale, '上传失败', 'Upload failed'))); 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 interface WorkspaceFileContent { name: string; path: string; size: number; content_type: string; modified: string; is_binary: boolean; is_truncated: boolean; content: string | null; } export async function browseWorkspace(path: string = ''): Promise { const params = path ? `?path=${encodeURIComponent(path)}` : ''; return fetchJSON(`/api/workspace/browse${params}`); } export async function getWorkspaceFile(path: string): Promise { return fetchJSON(`/api/workspace/file?path=${encodeURIComponent(path)}`); } 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 { const locale = getCurrentAppLocale(); if (file.size > MAX_FILE_SIZE) { throw new Error(pickAppText(locale, '文件过大(最大 50MB)', 'File is too large (max 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(`${pickAppText(locale, '上传失败', 'Upload failed')}: ${xhr.status}`)); } }; xhr.onerror = () => reject(new Error(pickAppText(locale, '上传失败', 'Upload failed'))); 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', }); }