feat(v3): Web 应用化基线(异步任务/SSE/llama-server 管理/Vue SPA 四页 + 设置页整页滚动修复)
This commit is contained in:
@@ -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')
|
||||
}
|
||||
Reference in New Issue
Block a user