diff --git a/src/api.ts b/src/api.ts index 557792a..c93377f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,13 +1,13 @@ -import axios from 'axios'; - -const API_BASE_URL = 'https://api.hoyidata.com/aura/v1'; -const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, ''); -const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`; -const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`; +import axios from 'axios'; + +const API_BASE_URL = 'https://api.hoyidata.com/aura/v1'; +const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, ''); +const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`; +const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`; const isMockAuth = () => typeof localStorage !== 'undefined' && localStorage.getItem('mock-auth') === '1'; export const api = axios.create({ - baseURL: API_BASE_URL, + baseURL: API_BASE_URL, timeout: 90000, withCredentials: true // 关键:跨域请求带 cookie }); @@ -16,10 +16,10 @@ export const api = axios.create({ api.interceptors.response.use( (r) => r, (err) => { - const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login'); - if (err?.response?.status === 401 && !isLoginPage && !isMockAuth()) { + const isLoginPage = location.pathname === '/login' || location.pathname === withAppBase('/login'); + if (err?.response?.status === 401 && !isLoginPage && !isMockAuth()) { const next = encodeURIComponent(location.pathname + location.search); - location.href = `${withAppBase('/login')}?next=${next}`; + location.href = `${withAppBase('/login')}?next=${next}`; } return Promise.reject(err); } @@ -91,20 +91,20 @@ export interface ToolCallTrace { result: any; } -export interface ChatMessage { - id: string; - role: 'user' | 'assistant'; - content: string; - reasoning?: string | null; - parentId?: string | null; - createdAt: number; - meta?: { - retrieved?: RetrievedSnippet[]; - toolCalls?: ToolCallTrace[]; - aborted?: boolean; - reasoning?: string; - error?: string; - } | null; +export interface ChatMessage { + id: string; + role: 'user' | 'assistant'; + content: string; + reasoning?: string | null; + parentId?: string | null; + createdAt: number; + meta?: { + retrieved?: RetrievedSnippet[]; + toolCalls?: ToolCallTrace[]; + aborted?: boolean; + reasoning?: string; + error?: string; + } | null; } export interface BranchInfo { @@ -118,18 +118,18 @@ export interface ChatHistoryResp { branches: Record; } -export interface AiModel { - model_id: string; - model_name: string; - icon: string; - model_ratio: number; - completion_ratio: number; -} - -export const ModelAPI = { - list: () => api.get<{ data: AiModel[] }>('/models').then((r) => r.data.data), -}; - +export interface AiModel { + id: string; + model_name: string; + icon: string; + model_ratio: number; + completion_ratio: number; +} + +export const ModelAPI = { + list: () => api.get<{ data: AiModel[] }>('/models').then((r) => r.data.data), +}; + export const AgentAPI = { list: () => api.get('/agents').then((r) => r.data), detail: (id: string) => api.get(`/agents/${id}`).then((r) => r.data), @@ -176,14 +176,14 @@ export const ChatAPI = { agentId: string, content: string, sessionId?: string, - model?: string, + model?: string, imageUrls?: string[] ) => api .post<{ user: ChatMessage; assistant: ChatMessage }>(`/chat/${agentId}/messages`, { content, sessionId, - model, + model, imageUrls }) .then((r) => r.data), @@ -320,7 +320,7 @@ export const SessionAPI = { .then((r) => r.data), /** 导出会话为文件并触发浏览器下载 */ exportSession: (agentId: string, sessionId: string, format: 'md' | 'json' = 'md') => { - const url = withApiBase(`/agents/${agentId}/sessions/${sessionId}/export?format=${format}`); + const url = withApiBase(`/agents/${agentId}/sessions/${sessionId}/export?format=${format}`); const a = document.createElement('a'); a.href = url; a.rel = 'noopener'; @@ -473,114 +473,114 @@ export const PromptTemplateAPI = { use: (id: string) => api.post(`/prompt-templates/${id}/use`).then((r) => r.data) }; -// ============== 积分商城 (v1) ============== - -export interface PointsMallMe { - points: number; - level?: string; - totalSpentUSD?: number; -} - -export interface PointsExchangeRequest { - productId: string; - recipientName: string; - phone: string; - province: string; - city: string; - district: string; - address: string; - zipCode?: string; -} - -export interface PointsExchangeResponse { - orderId: string; - pointsDeducted: number; - remainingPoints: number; -} - -export interface PointsMallCategory { - id: string; - name: string; - sort: number; -} - -export interface PointsMallAnnouncement { - id: string; - title: string; - content: string; - linkUrl?: string; -} - -export interface PointsMallBanner { - id: string; - title: string; - subtitle?: string; - imageUrl: string; - linkUrl?: string; -} - -export interface PointsMallPromoEntry { - id: string; - title: string; - subtitle?: string; - iconUrl?: string; - linkUrl?: string; -} - -export interface PointsMallProduct { - id: string; - categoryId: string; - name: string; - subtitle?: string; - coverUrl: string; - pointsPrice: number; - stock: number; - sold: number; - tags?: string[]; -} - -export interface PointsMallOverview { - me: PointsMallMe; - categories: PointsMallCategory[]; - announcements: PointsMallAnnouncement[]; - banners: PointsMallBanner[]; - promoEntries: PointsMallPromoEntry[]; -} - -export interface PointsMallProductsResponse { - page: number; - pageSize: number; - total: number; - items: PointsMallProduct[]; -} - -export const PointsMallAPI = { - overview: () => api.get('/points-mall/overview').then((r) => r.data), - products: (opts: { - categoryId?: string; - q?: string; - sort?: string; - page?: number; - pageSize?: number; - } = {}) => - api - .get('/points-mall/products', { - params: { - categoryId: opts.categoryId, - q: opts.q ?? '', - sort: opts.sort ?? 'popular', - page: opts.page ?? 1, - pageSize: opts.pageSize ?? 24 - } - }) - .then((r) => r.data), - exchange: (productId: string, shippingInfo: Omit) => - api.post('/points-mall/exchange', { - productId, - ...shippingInfo - }).then((r) => r.data) -}; - +// ============== 积分商城 (v1) ============== + +export interface PointsMallMe { + points: number; + level?: string; + totalSpentUSD?: number; +} + +export interface PointsExchangeRequest { + productId: string; + recipientName: string; + phone: string; + province: string; + city: string; + district: string; + address: string; + zipCode?: string; +} + +export interface PointsExchangeResponse { + orderId: string; + pointsDeducted: number; + remainingPoints: number; +} + +export interface PointsMallCategory { + id: string; + name: string; + sort: number; +} + +export interface PointsMallAnnouncement { + id: string; + title: string; + content: string; + linkUrl?: string; +} + +export interface PointsMallBanner { + id: string; + title: string; + subtitle?: string; + imageUrl: string; + linkUrl?: string; +} + +export interface PointsMallPromoEntry { + id: string; + title: string; + subtitle?: string; + iconUrl?: string; + linkUrl?: string; +} + +export interface PointsMallProduct { + id: string; + categoryId: string; + name: string; + subtitle?: string; + coverUrl: string; + pointsPrice: number; + stock: number; + sold: number; + tags?: string[]; +} + +export interface PointsMallOverview { + me: PointsMallMe; + categories: PointsMallCategory[]; + announcements: PointsMallAnnouncement[]; + banners: PointsMallBanner[]; + promoEntries: PointsMallPromoEntry[]; +} + +export interface PointsMallProductsResponse { + page: number; + pageSize: number; + total: number; + items: PointsMallProduct[]; +} + +export const PointsMallAPI = { + overview: () => api.get('/points-mall/overview').then((r) => r.data), + products: (opts: { + categoryId?: string; + q?: string; + sort?: string; + page?: number; + pageSize?: number; + } = {}) => + api + .get('/points-mall/products', { + params: { + categoryId: opts.categoryId, + q: opts.q ?? '', + sort: opts.sort ?? 'popular', + page: opts.page ?? 1, + pageSize: opts.pageSize ?? 24 + } + }) + .then((r) => r.data), + exchange: (productId: string, shippingInfo: Omit) => + api.post('/points-mall/exchange', { + productId, + ...shippingInfo + }).then((r) => r.data) +}; + // ============== 调用统计 (v0.8 P1) ============== export interface StatsOverview { @@ -601,45 +601,45 @@ export interface AgentStats { daily: { day: string; count: number }[]; } -export interface AgentTokenStatsByModel { - providerKind: string; - model: string; - calls: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; - costUSD: number; -} - -export interface AgentTokenStatsDaily { - day: string; - calls: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; - costUSD: number; -} - -export interface AgentTokenStats { - agentId: string; - days: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; - costUSD: number; - byModel: AgentTokenStatsByModel[]; - daily: AgentTokenStatsDaily[]; -} - +export interface AgentTokenStatsByModel { + providerKind: string; + model: string; + calls: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + costUSD: number; +} + +export interface AgentTokenStatsDaily { + day: string; + calls: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + costUSD: number; +} + +export interface AgentTokenStats { + agentId: string; + days: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + costUSD: number; + byModel: AgentTokenStatsByModel[]; + daily: AgentTokenStatsDaily[]; +} + export const StatsAPI = { overview: () => api.get('/stats/overview').then((r) => r.data), - agent: (id: string) => api.get(`/stats/agents/${id}`).then((r) => r.data), - agentTokens: (id: string, opts: { days?: number; limit?: number } = {}) => - api - .get(`/stats/agents/${id}/tokens`, { - params: { days: opts.days ?? 30, limit: opts.limit ?? 10 } - }) - .then((r) => r.data) + agent: (id: string) => api.get(`/stats/agents/${id}`).then((r) => r.data), + agentTokens: (id: string, opts: { days?: number; limit?: number } = {}) => + api + .get(`/stats/agents/${id}/tokens`, { + params: { days: opts.days ?? 30, limit: opts.limit ?? 10 } + }) + .then((r) => r.data) }; // ============== 流式(手写 SSE,不用 EventSource,因为它不能 POST) ============== @@ -663,17 +663,17 @@ export interface LLMProvider { updatedAt: number; } -export interface AgentAllowedLLMResponse { - agentId: string; - model: string; -} - +export interface AgentAllowedLLMResponse { + agentId: string; + model: string; +} + export const LLMProviderAPI = { list: () => api.get('/llm-providers').then((r) => r.data), - allowedModels: (agentId: string) => - api - .get('/llm-providers', { params: { agent_id: agentId } }) - .then((r) => r.data), + allowedModels: (agentId: string) => + api + .get('/llm-providers', { params: { agent_id: agentId } }) + .then((r) => r.data), create: (payload: Partial & { apiKey?: string }) => api.post<{ id: string }>('/llm-providers', payload).then((r) => r.data), update: (id: string, payload: Partial & { apiKey?: string }) => @@ -689,10 +689,10 @@ export const LLMProviderAPI = { export interface StreamEvents { - onMeta?: (data: any) => void; - onReasoningDelta?: (text: string) => void; - onDelta?: (text: string) => void; - onRetry?: (data: any) => void; + onMeta?: (data: any) => void; + onReasoningDelta?: (text: string) => void; + onDelta?: (text: string) => void; + onRetry?: (data: any) => void; onToolCall?: (data: { id: string; name: string; args: any }) => void; onToolResult?: (data: { id: string; name: string; result: any }) => void; onDone?: (data: { user: ChatMessage; assistant: ChatMessage }) => void; @@ -708,30 +708,30 @@ export interface ModelOverrides { maxTokens?: number; } -export async function streamChat( - agentId: string, - content: string, - handlers: StreamEvents, - signal?: AbortSignal, - sessionId?: string, - model?: string, - modelId?: string, - imageUrls?: string[] -) { - const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/stream`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, - body: JSON.stringify({ - content, - sessionId, - model, - model_id: modelId, - imageUrls: imageUrls ?? [] - }), - signal, - credentials: 'include' - }); - return await consumeSSE(resp, handlers, signal); +export async function streamChat( + agentId: string, + content: string, + handlers: StreamEvents, + signal?: AbortSignal, + sessionId?: string, + model?: string, + modelId?: string, + imageUrls?: string[] +) { + const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/stream`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, + body: JSON.stringify({ + content, + sessionId, + model, + model_id: modelId, + imageUrls: imageUrls ?? [] + }), + signal, + credentials: 'include' + }); + return await consumeSSE(resp, handlers, signal); } /** 重新生成(开新分支);行为同 streamChat */ @@ -745,7 +745,7 @@ export async function regenerateMessage( ) { const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/${messageId}/regenerate`, { method: 'POST', - headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, + headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: JSON.stringify({ overrides, attachmentsText }), signal, credentials: 'include' @@ -753,94 +753,94 @@ export async function regenerateMessage( return await consumeSSE(resp, handlers, signal); } -async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) { - if (!resp.ok || !resp.body) { - const txt = await resp.text().catch(() => ''); - h.onError?.(`HTTP ${resp.status}: ${txt}`); - return; - } - const reader = resp.body.getReader(); - const decoder = new TextDecoder('utf-8'); - let buf = ''; - let hasError = false; // 标记是否已发生错误,避免后续事件继续处理 - - try { - while (true) { - const { value, done } = await reader.read(); - if (done || hasError) break; - - buf += decoder.decode(value, { stream: true }); - buf = buf.replace(/\r\n/g, '\n'); - - let idx; - while ((idx = buf.indexOf('\n\n')) !== -1 && !hasError) { - const raw = buf.slice(0, idx); - buf = buf.slice(idx + 2); - if (!raw.trim() || raw.startsWith(':')) continue; - - let event = 'message'; - let dataStr = ''; - for (const line of raw.split('\n')) { - if (line.startsWith('event:')) event = line.slice(6).trim(); - else if (line.startsWith('data:')) { - let part = line.slice(5); - if (part.startsWith(' ')) part = part.slice(1); - dataStr += (dataStr ? '\n' : '') + part; - } - } - if (!dataStr) continue; - let data: any; - try { - data = JSON.parse(dataStr); - } catch { - continue; - } - switch (event) { - case 'meta': - h.onMeta?.(data); - break; - case 'retry': - h.onRetry?.(data); - break; - case 'reasoning_delta': - h.onReasoningDelta?.(data.content || ''); - break; - case 'delta': - h.onDelta?.(data.content || ''); - break; - case 'tool_call': - h.onToolCall?.(data); - break; - case 'tool_result': - h.onToolResult?.(data); - break; - case 'done': - // 只有在没有错误的情况下才处理 done 事件 - if (!hasError) { - h.onDone?.(data); - } - break; - case 'aborted': - h.onAborted?.(data); - break; - case 'error': - hasError = true; - h.onError?.(data.message || 'stream error'); - // 发生错误后立即停止读取 - break; - } - } - } - } catch (e: any) { - if (signal?.aborted || e?.name === 'AbortError') { - // 静默:本地已 abort - return; - } - h.onError?.(e?.message ?? String(e)); - } finally { - // 确保 reader 被释放 - reader.cancel().catch(() => {}); - } +async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) { + if (!resp.ok || !resp.body) { + const txt = await resp.text().catch(() => ''); + h.onError?.(`HTTP ${resp.status}: ${txt}`); + return; + } + const reader = resp.body.getReader(); + const decoder = new TextDecoder('utf-8'); + let buf = ''; + let hasError = false; // 标记是否已发生错误,避免后续事件继续处理 + + try { + while (true) { + const { value, done } = await reader.read(); + if (done || hasError) break; + + buf += decoder.decode(value, { stream: true }); + buf = buf.replace(/\r\n/g, '\n'); + + let idx; + while ((idx = buf.indexOf('\n\n')) !== -1 && !hasError) { + const raw = buf.slice(0, idx); + buf = buf.slice(idx + 2); + if (!raw.trim() || raw.startsWith(':')) continue; + + let event = 'message'; + let dataStr = ''; + for (const line of raw.split('\n')) { + if (line.startsWith('event:')) event = line.slice(6).trim(); + else if (line.startsWith('data:')) { + let part = line.slice(5); + if (part.startsWith(' ')) part = part.slice(1); + dataStr += (dataStr ? '\n' : '') + part; + } + } + if (!dataStr) continue; + let data: any; + try { + data = JSON.parse(dataStr); + } catch { + continue; + } + switch (event) { + case 'meta': + h.onMeta?.(data); + break; + case 'retry': + h.onRetry?.(data); + break; + case 'reasoning_delta': + h.onReasoningDelta?.(data.content || ''); + break; + case 'delta': + h.onDelta?.(data.content || ''); + break; + case 'tool_call': + h.onToolCall?.(data); + break; + case 'tool_result': + h.onToolResult?.(data); + break; + case 'done': + // 只有在没有错误的情况下才处理 done 事件 + if (!hasError) { + h.onDone?.(data); + } + break; + case 'aborted': + h.onAborted?.(data); + break; + case 'error': + hasError = true; + h.onError?.(data.message || 'stream error'); + // 发生错误后立即停止读取 + break; + } + } + } + } catch (e: any) { + if (signal?.aborted || e?.name === 'AbortError') { + // 静默:本地已 abort + return; + } + h.onError?.(e?.message ?? String(e)); + } finally { + // 确保 reader 被释放 + reader.cancel().catch(() => {}); + } } // ============== Workflow (v1.1) ============== @@ -936,7 +936,7 @@ export function streamWorkflowRun( } ): () => void { const qs = input ? `?input=${encodeURIComponent(JSON.stringify(input))}` : ''; - const url = withApiBase(`/workflows/${workflowId}/run-stream${qs}`); + const url = withApiBase(`/workflows/${workflowId}/run-stream${qs}`); const es = new EventSource(url, { withCredentials: true } as any); es.addEventListener('ready', (e: any) => handlers.onReady?.(JSON.parse(e.data))); es.addEventListener('step_start', (e: any) => handlers.onStepStart?.(JSON.parse(e.data))); diff --git a/src/pages/AgentEditor/components/ModelCheckboxDropdown.tsx b/src/pages/AgentEditor/components/ModelCheckboxDropdown.tsx index 7b0a320..66d6ef2 100644 --- a/src/pages/AgentEditor/components/ModelCheckboxDropdown.tsx +++ b/src/pages/AgentEditor/components/ModelCheckboxDropdown.tsx @@ -30,7 +30,7 @@ export default function ModelCheckboxDropdown({ value = [], onChange, models }: const inputPrice = 2 * m.model_ratio; const outputPrice = inputPrice * m.completion_ratio; return ( - +