feat(rust): 支持 image 同步转流式 SSE、free_team_first 调度预设及账户错误自动重试

- openai:image 同步响应桥接为流式 SSE(image_generation.completed / image_edit.completed 事件)
- image 请求解析与前置校验移除模型白名单,支持任意自定义模型名
- 调度器新增 free_team_first 预设(mode: both / free_only / team_only)
- pool config 解析重构:新增 POOL_ALLOWED_SCHEDULING_PRESETS 白名单,规范化 mode 字段
- 错误分类器新增账户/账单错误模式集,升级为 RetryUpstreamFailure 而非 StopSemanticClientError
- 修复 stream execution 中上游 headers 与输出 headers 混用导致 content-type 判断错误的问题
- codex image 工具始终写入 action 字段(generate/edit),仅 generate 操作填充默认 size/quality
- 前端:pool 节点组只展示实际执行过的候选节点,全部 skipped 时折叠为最后一个节点
- Redis 测试就绪检测从 TCP 连接改为 PING/PONG 协议验证
This commit is contained in:
fawney19
2026-04-24 10:07:24 +08:00
parent a9d10163af
commit 5d710d9d10
16 changed files with 1388 additions and 204 deletions
@@ -496,6 +496,7 @@ import { parseApiError } from '@/utils/errorParser'
import { formatApiFormat } from '@/api/endpoints/types/api-format'
import { resolveTimelineFinalStatus } from '../utils/status'
import {
buildPoolGroupVisibleAttempts,
buildPoolParticipatedCandidates,
extractPoolGroupId,
makeAttemptKey,
@@ -508,7 +509,7 @@ interface NodeGroup {
providerName: string
primary: CandidateRecord
primaryStatus: string
allAttempts: CandidateRecord[] // 所有尝试(包括首次和重试)
allAttempts: CandidateRecord[] // 当前展示的尝试(含主节点)
retryCount: number
totalLatency: number // 所有尝试的总延迟
startIndex: number
@@ -867,15 +868,19 @@ const groupedTimeline = computed<NodeGroup[]>(() => {
})
if (attempts.length === 0) continue
const poolPrimaryStatus = attempts.reduce((best, current) => {
const visibleAttempts = buildPoolGroupVisibleAttempts(attempts)
if (visibleAttempts.length === 0) continue
const poolPrimaryStatus = visibleAttempts.reduce((best, current) => {
const bestPriority = STATUS_PRIORITY[best] ?? 0
const currentStatus = getDisplayStatus(current)
const currentPriority = STATUS_PRIORITY[currentStatus] ?? 0
return currentPriority > bestPriority ? currentStatus : best
}, getDisplayStatus(attempts[0]))
}, getDisplayStatus(visibleAttempts[0]))
const successAttempt = attempts.find((item) => item.status === 'success')
const poolPrimary = successAttempt || attempts[attempts.length - 1] || attempts[0]
const successAttempt = visibleAttempts.find((item) => item.status === 'success')
const poolPrimary =
successAttempt || visibleAttempts[visibleAttempts.length - 1] || visibleAttempts[0]
const startIndex = Math.min(...attempts.map(item => item.candidate_index))
const endIndex = Math.max(...attempts.map(item => item.candidate_index))
@@ -884,12 +889,12 @@ const groupedTimeline = computed<NodeGroup[]>(() => {
providerName: getProviderDisplayName(poolPrimary, { allowAuthTypeFallback: false }),
primary: poolPrimary,
primaryStatus: poolPrimaryStatus,
allAttempts: attempts,
retryCount: Math.max(0, attempts.length - 1),
totalLatency: attempts.reduce((sum, item) => sum + (item.latency_ms || 0), 0),
allAttempts: visibleAttempts,
retryCount: Math.max(0, visibleAttempts.length - 1),
totalLatency: visibleAttempts.reduce((sum, item) => sum + (item.latency_ms || 0), 0),
startIndex,
endIndex,
hasConversion: attempts.some((item) => item.extra_data?.needs_conversion === true),
hasConversion: visibleAttempts.some((item) => item.extra_data?.needs_conversion === true),
providerApiFormat: null,
isPoolGroup: true,
})