Files
projectAIpopular/webapp/src/api/index.ts
T
tzt 7caab52bf1 feat(proxy): T-P7 管理面前端(ProxyView 三卡片 + 路由/NAV 注册)
- webapp/api:getProxyStats/getProxyLedger/listProxyStudents/createStudent/
  topup/issueKey/revokeKey 封装
- ProxyView.vue 三卡片:① 命中率-毛利看板(h_g/h_p/综合/收入/成本/毛利/分桶)
  ② 学生与 key 管理(建学生/签发(明文仅显一次警示)/充值/选中签发)
  ③ 用量流水表(时间/模型/状态徽标/收入/成本/毛利)
- App.vue NAV + router 注册 /proxy;npm run build 产物更新
- 全量 423 passed
2026-09-05 16:19:26 +08:00

641 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ChatInitResponse>('/chat', { query, domain_group })
return data
}
/** GET /runs/{id}/status:查询任务状态 */
export async function getRunStatus(requestId: string) {
const { data } = await http.get<RunStatus>(`/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<ReviewItem[]>('/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<Metrics>('/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<ModelSettings>('/config')
return data
}
/** PUT /config:更新模型设置(部分更新) */
export async function updateConfig(patch: Partial<ModelSettings>) {
const { data } = await http.put<ModelSettings>('/config', patch)
return data
}
/** POST /config/reset:恢复默认设置 */
export async function resetConfig() {
const { data } = await http.post<ModelSettings>('/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<string, string> = { 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<string, string> = { 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<LlamaStatus>('/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<LlamaStatus>('/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<string, string> = { url }
if (dest) params.dest = dest
const { data } = await http.post<DownloadProgress>('/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<PoolRole, string>
entries: PoolEntry[]
}
/** GET /pool:读取模型池(api_key 打码) */
export async function getPool() {
const { data } = await http.get<PoolData>('/pool')
return data
}
/** POST /pool:新增或更新条目(api_key 留空 = 保留原值) */
export async function upsertPoolEntry(entry: Partial<PoolEntry>) {
const { data } = await http.post<PoolData>('/pool', entry)
return data
}
/** DELETE /pool/{id} */
export async function deletePoolEntry(id: string) {
const { data } = await http.delete<PoolData>(`/pool/${id}`)
return data
}
/** PUT /pool/roles:指派角色(空串 = 经典设置) */
export async function setPoolRoles(roles: Partial<Record<PoolRole, string>>) {
const { data } = await http.put<PoolData>('/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<string, unknown>
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<AgentSessionDetail>('/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<AgentSessionBrief[]>('/agent/sessions')
return data
}
/** GET /agent/sessions/{sid}:会话详情(含轮次) */
export async function getAgentSession(sid: string) {
const { data } = await http.get<AgentSessionDetail>(`/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<AgentSessionDetail>(`/agent/sessions/${sid}`, {
title,
})
return data
}
/** GET /agent/{id}/status */
export async function getAgentStatus(requestId: string) {
const { data } = await http.get<AgentStatus>(`/agent/${requestId}/status`)
return data
}
/** GET /agent/{id}/events:完整事件列表(刷新恢复用) */
export async function getAgentEvents(requestId: string) {
const { data } = await http.get<AgentEvent[]>(`/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<FsBrowse>('/agent/fs', { params: path ? { path } : {} })
return data
}
/** GET /agent/workspaces:当前工作区 + 最近列表 */
export async function getAgentWorkspaces() {
const { data } = await http.get<WorkspacesInfo>('/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() },
}
}
// ── 代理层管理面(T-P7) ─────────────────────────────────────────────────────
export interface ProxyStats {
requests: number
h_g: number
h_p: number
revenue_milli: number
cost_milli: number
margin_milli: number
by_bucket?: Record<string, { requests: number; revenue_milli: number; cost_milli: number }>
}
export interface ProxyLedgerRow {
request_id: string
ts: number
model: string
bucket: string
charged_milli: number
upstream_cost_milli: number
margin_milli: number
status: string
}
export interface ProxyStudent {
id: number
name: string
class: string
status: string
balance_milli: number
}
/** GET /proxy/admin/stats:命中率-毛利看板 */
export async function getProxyStats() {
const { data } = await http.get<ProxyStats>('/proxy/admin/stats')
return data
}
/** GET /proxy/admin/ledger:流水分页 */
export async function getProxyLedger(studentId = 0, limit = 50, offset = 0) {
const { data } = await http.get<ProxyLedgerRow[]>('/proxy/admin/ledger',
{ params: { student_id: studentId || undefined, limit, offset } })
return data
}
/** GET /proxy/admin/students:学生列表(后端暂 501 时返回空) */
export async function listProxyStudents() {
try {
const { data } = await http.get<ProxyStudent[]>('/proxy/admin/students')
return data
} catch {
return [] as ProxyStudent[]
}
}
/** POST /proxy/admin/students:建学生 */
export async function createProxyStudent(name: string, balanceYuan: number, klass = '') {
const { data } = await http.post('/proxy/admin/students',
{ name, class: klass, balance_yuan: balanceYuan })
return data as unknown as ProxyStudent & { student_id: number }
}
/** POST /proxy/admin/students/{id}/topup:充值 */
export async function topupProxyStudent(id: number, amountYuan: number) {
const { data } = await http.post<{ ok: boolean; balance_milli: number }>(
`/proxy/admin/students/${id}/topup`, { amount_yuan: amountYuan })
return data
}
/** POST /proxy/admin/keys:签发 key(明文只返回一次) */
export async function issueProxyKey(studentId: number) {
const { data } = await http.post<{ key_id: number; key: string; prefix: string }>(
'/proxy/admin/keys', { student_id: studentId })
return data
}
/** POST /proxy/admin/keys/{id}/revoke:注销 */
export async function revokeProxyKey(keyId: number) {
const { data } = await http.post<{ ok: boolean }>(`/proxy/admin/keys/${keyId}/revoke`)
return data
}
// ── 辅助:轮询直到完成(用于不需要 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')
}