feat(v3): T19-T20 模型池设置 UI + 智能体页 + 指标分账卡
- 设置页「模型池」区块:三角色指派下拉、条目表格(档位徽标/单价/启用/测试/编辑/删除)、
内联添加表单(模型列表探测 datalist,api_key 留空保留原值)
- 新增「🤖 智能体」页 /agent:任务输入 + 池模型选择、工具调用时间轴
(round/tool_call/tool_result/usage/final)、左侧工作区文件浏览与预览
- 指标页「按模型分账」卡片(v2.by_model 表格)
- 修复:SSE final 后 EventSource 自动重连回放导致的事件重复渲染(终态即关闭+回放拦截)
This commit is contained in:
@@ -246,6 +246,161 @@ export function watchDownload(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 模型池(多价位异构模型) ─────────────────────────────────────────────────
|
||||
|
||||
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'
|
||||
ts?: number
|
||||
round?: number
|
||||
name?: string
|
||||
arguments?: Record<string, unknown>
|
||||
ok?: boolean
|
||||
preview?: string
|
||||
prompt_tokens?: number
|
||||
completion_tokens?: number
|
||||
reason?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** POST /agent:提交智能体任务 */
|
||||
export async function startAgent(task: string, poolId?: string) {
|
||||
const { data } = await http.post<{ request_id: string; status: string; model: string }>(
|
||||
'/agent', { task, pool_id: poolId || undefined })
|
||||
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=...:浏览工作区目录 */
|
||||
export async function listAgentWorkspace(path = '') {
|
||||
const { data } = await http.get<{
|
||||
ok: boolean
|
||||
entries?: { name: string; type: string; size?: number }[]
|
||||
error?: string
|
||||
}>('/agent/workspace', { params: path ? { path } : {} })
|
||||
return data
|
||||
}
|
||||
|
||||
/** GET /agent/file?path=...:读取工作区文件 */
|
||||
export async function readAgentFile(path: string) {
|
||||
const { data } = await http.get<{ ok: boolean; content: string; truncated: boolean; error?: string }>(
|
||||
'/agent/file', { params: { path } })
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user