feat(v3): Web 应用化基线(异步任务/SSE/llama-server 管理/Vue SPA 四页 + 设置页整页滚动修复)

This commit is contained in:
tzt
2026-09-01 08:31:47 +08:00
parent 8d36eeec59
commit 3bbdcb7cc7
60 changed files with 7236 additions and 1160 deletions
+262
View File
@@ -0,0 +1,262 @@
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
}
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() },
}
}
// ── 辅助:轮询直到完成(用于不需要 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')
}