import axios from 'axios' import type { ChatInitResponse, RunStatus, SSEEvent, ReviewItem, Metrics, } from '@/types' const http = axios.create({ timeout: 10_000 }) // ── Chat ───────────────────────────────────────────────────────────────────── /** POST /chat:异步提交,立即返回 request_id */ export async function chat(query: string, domain_group?: string) { const { data } = await http.post('/chat', { query, domain_group }) return data } /** GET /runs/{id}/status:查询任务状态 */ export async function getRunStatus(requestId: string) { const { data } = await http.get(`/runs/${requestId}/status`) return data } /** GET /runs/{id}/workspace:获取交流文本全文 */ export async function getWorkspace(requestId: string) { const { data } = await http.get(`/runs/${requestId}/workspace`) return data } /** SSE /runs/{id}/stream:订阅实时协作事件 */ export function watchRun(requestId: string) { const es = new EventSource(`/runs/${requestId}/stream`) return { /** 监听器会在组件卸载时自动断开,调用者只需提供 onXxx 回调 */ subscribe(opts: { onWorkspace: (ws: import('@/types').Workspace) => void onStatus: (s: string) => void onError: (detail: string) => void onDone: () => void }) { es.addEventListener('message', (ev) => { const d: SSEEvent = JSON.parse(ev.data) if (d.type === 'workspace') opts.onWorkspace(d.workspace) else if (d.type === 'status') { opts.onStatus(d.value) if (d.value === 'done' || d.value === 'failed') opts.onDone() } else if (d.type === 'error') opts.onError(d.detail) }) es.addEventListener('error', () => { // EventSource 自己会重连,这里只记录 }) }, close() { es.close() }, } } // ── 人工检验 ───────────────────────────────────────────────────────────────── export async function listReviews(status?: string) { const params = status ? { status } : {} const { data } = await http.get('/review/queue', { params }) return data } export async function submitReview(reviewId: number, verdict: string, correction?: string) { const { data } = await http.post(`/review/${reviewId}`, null, { params: { verdict, correction }, }) return data } // ── 指标 ───────────────────────────────────────────────────────────────────── export async function getMetrics() { const { data } = await http.get('/api/metrics') return data } // ── 模型设置 ───────────────────────────────────────────────────────────────── export interface ModelSettings { worker: { backend: string model: string base_url: string port: number temperature: number max_fix_attempts: number code_timeout_s: number } architect: { model: string base_url: string api_key: string /** 服务端打码标志:true 表示已存 key(返回值只含前 6 位,保存时留空即保留) */ api_key_set?: boolean } pipeline: { fast_path: boolean rounds_cap: number api_token_cap: number breach_policy: string } } /** GET /config:读取当前模型设置 */ export async function getConfig() { const { data } = await http.get('/config') return data } /** PUT /config:更新模型设置(部分更新) */ export async function updateConfig(patch: Partial) { const { data } = await http.put('/config', patch) return data } /** POST /config/reset:恢复默认设置 */ export async function resetConfig() { const { data } = await http.post('/config/reset') return data } // ── 模型发现 & 连接验证 ─────────────────────────────────────────────────────── export interface ModelInfo { id: string name: string } /** * GET /config/models?backend=llama_server&base_url=...&api_key=... * 返回 { models: ModelInfo[] } 或 { error: string } */ export async function listModels( backend: string, base_url: string, api_key: string, ) { const params: Record = { backend } if (base_url) params.base_url = base_url if (api_key) params.api_key = api_key const { data } = await http.get<{ models?: ModelInfo[]; error?: string }>( '/config/models', { params }, ) return data } /** * GET /config/ping?backend=...&base_url=...&api_key=... * 返回 { ok: boolean, detail?: string } */ export async function pingBackend( backend: string, base_url: string, api_key: string, ) { const params: Record = { backend } if (base_url) params.base_url = base_url if (api_key) params.api_key = api_key const { data } = await http.get<{ ok: boolean; detail?: string; status_code?: number }>( '/config/ping', { params }, ) return data } // ── llama-server 内置管理 ───────────────────────────────────────────────────── export interface LlamaStatus { running: boolean pid?: number | null model?: string | null port?: number | null base_url?: string | null started_at?: number | null error?: string | null } export interface LocalModel { id: string name: string size_mb: number path: string } export interface DownloadProgress { url: string dest: string downloaded_bytes: number total_bytes?: number | null progress_pct: number speed: string eta: string done: boolean error?: string | null } /** GET /llama/status */ export async function getLlamaStatus() { const { data } = await http.get('/llama/status') return data } /** GET /llama/models */ export async function listLocalModels() { const { data } = await http.get<{ models: LocalModel[] }>('/llama/models') return data.models } /** POST /llama/start */ export async function startLlama(model: string, port = 8901, ngl = 99, ctx = 4096) { const { data } = await http.post('/llama/start', null, { params: { model, port, ngl, ctx }, }) return data } /** POST /llama/stop */ export async function stopLlama() { const { data } = await http.post<{ running: boolean }>('/llama/stop') return data } /** POST /llama/download */ export async function downloadModel(url: string, dest?: string) { const params: Record = { url } if (dest) params.dest = dest const { data } = await http.post('/llama/download', null, { params }) return data } /** SSE /llama/download/stream?url=... */ export function watchDownload(url: string) { const es = new EventSource(`/llama/download/stream?url=${encodeURIComponent(url)}`) return { subscribe(opts: { onProgress: (p: DownloadProgress) => void; onDone: (p: DownloadProgress) => void }) { es.addEventListener('message', (ev) => { const p: DownloadProgress = JSON.parse(ev.data) opts.onProgress(p) if (p.done) opts.onDone(p) }) es.addEventListener('error', () => { es.close() }) }, close() { es.close() }, } } // ── 模型池(多价位异构模型) ───────────────────────────────────────────────── export type PoolTier = 'local' | 'budget' | 'premium' export type PoolRole = 'architect' | 'worker' | 'agent' export interface PoolEntry { id: string name: string tier: PoolTier backend: 'mock' | 'llama_server' | 'openai' base_url: string model: string api_key: string api_key_set: boolean price_in: number price_out: number temperature: number max_tokens: number enabled: boolean } export interface PoolData { roles: Record entries: PoolEntry[] } /** GET /pool:读取模型池(api_key 打码) */ export async function getPool() { const { data } = await http.get('/pool') return data } /** POST /pool:新增或更新条目(api_key 留空 = 保留原值) */ export async function upsertPoolEntry(entry: Partial) { const { data } = await http.post('/pool', entry) return data } /** DELETE /pool/{id} */ export async function deletePoolEntry(id: string) { const { data } = await http.delete(`/pool/${id}`) return data } /** PUT /pool/roles:指派角色(空串 = 经典设置) */ export async function setPoolRoles(roles: Partial>) { const { data } = await http.put('/pool/roles', roles) return data } /** POST /pool/{id}/test:条目连通性测试 */ export async function testPoolEntry(id: string) { const { data } = await http.post<{ ok: boolean; detail?: string }>(`/pool/${id}/test`) return data } /** GET /pool/{id}/models:条目端点下的可用模型列表 */ export async function listPoolModels(id: string) { const { data } = await http.get<{ models?: ModelInfo[]; error?: string }>(`/pool/${id}/models`) return data } // ── 智能体(工具调用) ─────────────────────────────────────────────────────── export interface AgentEvent { type: 'round' | 'tool_call' | 'tool_result' | 'usage' | 'final' | 'phase' | 'message' | 'approval_request' | 'approval_decided' | 'delta' ts?: number round?: number name?: string arguments?: Record ok?: boolean preview?: string prompt_tokens?: number completion_tokens?: number reason?: string error?: string // 两级模式(D7) phase?: 'plan' | 'execute' | 'review' handoff?: number model?: string role?: 'planner' | 'executor' content?: string // 审批(D9)/ 流式(D10) id?: string policy?: string allowed?: boolean note?: string text?: string } /** POST /agent/{id}/approve:裁决待审批操作(dsh 式 allow-once / deny) */ export async function approveAgent(requestId: string, approvalId: string, allowed: boolean) { const { data } = await http.post<{ ok: boolean }>(`/agent/${requestId}/approve`, { approval_id: approvalId, allowed }) return data } export interface AgentStatus { request_id: string task: string model: string state: 'running' | 'done' | 'failed' response: string rounds: number prompt_tokens: number completion_tokens: number error?: string | null started_at?: number finished_at?: number workspace?: string executor_model?: string mode?: 'single' | 'dual' tool_calls?: number } /** POST /agent:提交智能体任务(sessionId 可选:会话式多轮) */ export async function startAgent( task: string, poolId?: string, workspace?: string, executorPoolId?: string, sessionId?: string, ) { const { data } = await http.post<{ request_id: string; status: string; model: string workspace?: string; mode?: 'single' | 'dual'; executor_model?: string session_id?: string | null }>('/agent', { task, pool_id: poolId || undefined, workspace: workspace || undefined, executor_pool_id: executorPoolId || undefined, session_id: sessionId || undefined, }) return data } /** POST /agent/{id}/cancel:停止运行中的智能体任务 */ export async function cancelAgent(requestId: string) { const { data } = await http.post<{ ok: boolean; detail?: string }>( `/agent/${requestId}/cancel`) return data } // ── 智能体会话(dsh 式多轮) ───────────────────────────────────────────────── export interface AgentSessionBrief { id: string title: string workspace?: string created_at?: number updated_at?: number busy?: boolean pool_id?: string executor_pool_id?: string turns: number | unknown[] } export interface AgentSessionDetail extends AgentSessionBrief { turns: { request_id: string task: string response: string state: string tool_calls: number tokens: number error?: string | null ts?: number }[] } /** POST /agent/sessions:创建会话 */ export async function createAgentSession(workspace?: string, executorPoolId?: string, title?: string) { const { data } = await http.post('/agent/sessions', { title: title || undefined, workspace: workspace || undefined, executor_pool_id: executorPoolId || undefined, }) return data } /** GET /agent/sessions:会话列表 */ export async function listAgentSessions() { const { data } = await http.get('/agent/sessions') return data } /** GET /agent/sessions/{sid}:会话详情(含轮次) */ export async function getAgentSession(sid: string) { const { data } = await http.get(`/agent/sessions/${sid}`) return data } /** DELETE /agent/sessions/{sid}:删除会话 */ export async function deleteAgentSession(sid: string) { const { data } = await http.delete<{ ok: boolean }>(`/agent/sessions/${sid}`) return data } /** PATCH /agent/sessions/{sid}:重命名会话 */ export async function renameAgentSession(sid: string, title: string) { const { data } = await http.patch(`/agent/sessions/${sid}`, { title, }) return data } /** GET /agent/{id}/status */ export async function getAgentStatus(requestId: string) { const { data } = await http.get(`/agent/${requestId}/status`) return data } /** GET /agent/{id}/events:完整事件列表(刷新恢复用) */ export async function getAgentEvents(requestId: string) { const { data } = await http.get(`/agent/${requestId}/events`) return data } /** GET /agent/workspace?path=&root=:浏览工作区目录(root 可指定选中工作区) */ export async function listAgentWorkspace(path = '', root = '') { const { data } = await http.get<{ ok: boolean entries?: { name: string; type: string; size?: number }[] error?: string }>('/agent/workspace', { params: { ...(path ? { path } : {}), ...(root ? { root } : {}) } }) return data } /** GET /agent/file?path=&root=:读取工作区文件 */ export async function readAgentFile(path: string, root = '') { const { data } = await http.get<{ ok: boolean; content: string; truncated: boolean; error?: string }>( '/agent/file', { params: { path, ...(root ? { root } : {}) } }) return data } // ── 智能体:工作区选择(参考 deepseek-harness 的打开文件夹体验) ───────────── export interface FsBrowse { ok: boolean path: string parent: string dirs: string[] error?: string } export interface WorkspacesInfo { current: string recent: string[] } /** GET /agent/fs?path=:浏览本地目录(目录选择器,只列子目录) */ export async function browseFs(path = '') { const { data } = await http.get('/agent/fs', { params: path ? { path } : {} }) return data } /** GET /agent/workspaces:当前工作区 + 最近列表 */ export async function getAgentWorkspaces() { const { data } = await http.get('/agent/workspaces') return data } /** POST /agent/workspaces:打开(或新建)工作目录并设为当前 */ export async function openWorkspace(path: string, create = false) { const { data } = await http.post<{ ok: boolean; current: string; recent: string[] }>( '/agent/workspaces', { path, create }) return data } /** SSE /agent/{id}/stream:订阅智能体过程事件 */ export function watchAgent(requestId: string) { const es = new EventSource(`/agent/${requestId}/stream`) let finalSeen = false return { subscribe(opts: { onEvent: (ev: AgentEvent) => void onDone: () => void }) { es.addEventListener('message', (ev) => { if (finalSeen) return // final 后的自动重连回放,忽略 const d: AgentEvent = JSON.parse(ev.data) opts.onEvent(d) if (d.type === 'final') { finalSeen = true es.close() // 终态即关闭,防止重连回放重复渲染 opts.onDone() } }) es.addEventListener('error', () => { // 连接中断由浏览器自动重连;final 已 seen 时回放会被上方拦截 }) }, close() { es.close() }, } } // ── 辅助:轮询直到完成(用于不需要 SSE 的场景)────────────────────────────────── export async function pollUntilDone( requestId: string, { timeout = 60_000, interval = 500 }: { timeout?: number; interval?: number } = {}, ) { const t0 = Date.now() while (Date.now() - t0 < timeout) { const s = await getRunStatus(requestId) if (s.status === 'done' || s.status === 'failed') return s await new Promise((r) => setTimeout(r, interval)) } throw new Error('poll timeout') }