Fix Codex image progress heartbeat merge regressions

This commit is contained in:
fawney19
2026-05-10 02:10:23 +08:00
156 changed files with 11741 additions and 943 deletions

View File

@@ -236,6 +236,28 @@ export interface EmailTemplateResetResponse {
}
}
export interface CleanupRunRecord {
id: string
kind: string
trigger: string
status: 'processing' | 'completed' | 'failed'
message: string
started_at_unix_secs: number
completed_at_unix_secs: number | null
duration_ms: number | null
summary: Record<string, unknown>
error: string | null
}
export interface CleanupRunListResponse {
items: CleanupRunRecord[]
}
export interface CleanupTaskResponse {
message: string
task: CleanupRunRecord
}
// 检查更新响应
export interface CheckUpdateResponse {
current_version: string
@@ -1051,12 +1073,29 @@ export const adminApi = {
},
// 数据清空
purgeConfig: () => purge<{ message: string; deleted: Record<string, number> }>('config'),
purgeUsers: () => purge<{ message: string; deleted: Record<string, number> }>('users'),
purgeUsage: () => purge<{ message: string; deleted: Record<string, number> }>('usage'),
purgeAuditLogs: () => purge<{ message: string; deleted: Record<string, number> }>('audit-logs'),
purgeRequestBodies: () => purge<{ message: string; cleaned: Record<string, number> }>('request-bodies'),
purgeStats: () => purge<{ message: string }>('stats'),
purgeConfig: () => purge<CleanupTaskResponse>('config'),
purgeUsers: () => purge<CleanupTaskResponse>('users'),
async purgeUsage(): Promise<CleanupTaskResponse> {
const response = await apiClient.post<CleanupTaskResponse>('/api/admin/system/purge/usage')
return response.data
},
async purgeAuditLogs(): Promise<CleanupTaskResponse> {
const response = await apiClient.post<CleanupTaskResponse>('/api/admin/system/purge/audit-logs')
return response.data
},
purgeRequestBodies: () => purge<CleanupTaskResponse>('request-bodies'),
async purgeRequestBodiesAsync(): Promise<CleanupTaskResponse> {
const response = await apiClient.post<CleanupTaskResponse>('/api/admin/system/purge/request-bodies/task')
return response.data
},
async purgeStats(): Promise<CleanupTaskResponse> {
const response = await apiClient.post<CleanupTaskResponse>('/api/admin/system/purge/stats')
return response.data
},
async getCleanupRuns(): Promise<CleanupRunListResponse> {
const response = await apiClient.get<CleanupRunListResponse>('/api/admin/system/cleanup/runs')
return response.data
},
async getTimeSeries(params?: {
start_date?: string

View File

@@ -1,39 +1,78 @@
import apiClient from './client'
// 异步任务状态
export type AsyncTaskStatus = 'pending' | 'submitted' | 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'
export type AsyncTaskStatus =
| 'queued'
| 'running'
| 'retrying'
| 'succeeded'
| 'failed'
| 'cancelled'
| 'skipped'
| 'pending'
| 'submitted'
| 'processing'
| 'completed'
// 异步任务类型
export type AsyncTaskType = 'video'
export type AsyncTaskKind = 'scheduled' | 'daemon' | 'on_demand' | 'fire_and_forget'
export type AsyncTaskType = AsyncTaskKind | 'video'
// 异步任务列表项
export interface AsyncTaskItem {
id: string
external_task_id: string
user_id: string
username: string
task_type: AsyncTaskType
model: string
prompt: string
status: AsyncTaskStatus
progress_percent: number
progress_message: string | null
provider_id: string
provider_name: string
duration_seconds: number
resolution: string
aspect_ratio: string
video_url: string | null
error_code: string | null
error_message: string | null
poll_count: number
max_poll_count: number
created_at: string
completed_at: string | null
submitted_at: string | null
export interface AsyncTaskDefinition {
task_key: string
kind: AsyncTaskKind
trigger: string
max_attempts: number
singleton: boolean
persist_history: boolean
}
export interface AsyncTaskItem {
id: string
task_key?: string
kind?: AsyncTaskKind
trigger?: string
task_type?: AsyncTaskType
external_task_id?: string
user_id?: string
username?: string
model?: string
prompt?: string
status: AsyncTaskStatus
attempt?: number
max_attempts?: number
owner_instance?: string | null
progress_percent: number
progress_message: string | null
payload?: unknown
result?: unknown
error_message: string | null
cancel_requested?: boolean
created_by?: string | null
provider_id?: string
provider_name?: string
duration_seconds?: number
resolution?: string
aspect_ratio?: string
video_url?: string | null
error_code?: string | null
poll_count?: number
max_poll_count?: number
created_at: string
started_at?: string | null
updated_at?: string | null
finished_at?: string | null
completed_at?: string | null
submitted_at?: string | null
}
export interface AsyncTaskEvent {
id: string
run_id: string
event_type: string
message: string
payload: unknown
created_at: string
}
// 候选 Key 信息
export interface CandidateKeyInfo {
index: number
provider_id: string
@@ -47,7 +86,6 @@ export interface CandidateKeyInfo {
selected?: boolean
}
// 请求元数据
export interface AsyncTaskRequestMetadata {
candidate_keys: CandidateKeyInfo[]
selected_key_id: string
@@ -56,111 +94,118 @@ export interface AsyncTaskRequestMetadata {
user_agent: string
request_id: string
request_headers?: Record<string, string>
poll_raw_response?: unknown // 轮询完成时的原始响应
billing_snapshot?: unknown // 计费快照
poll_raw_response?: unknown
billing_snapshot?: unknown
}
// 异步任务详情
export interface AsyncTaskDetail extends AsyncTaskItem {
api_key_id: string
endpoint_id: string
key_id: string
client_api_format: string
provider_api_format: string
format_converted: boolean
original_request_body: unknown
converted_request_body: unknown
size: string | null
video_urls: string[] | null
thumbnail_url: string | null
video_size_bytes: number | null
video_duration_seconds: number | null // 实际视频时长(秒)
video_expires_at: string | null
stored_video_path: string | null
storage_provider: string | null
retry_count: number
max_retries: number
poll_interval_seconds: number
next_poll_at: string | null
updated_at: string | null
endpoint: {
api_key_id?: string
endpoint_id?: string
key_id?: string
client_api_format?: string
provider_api_format?: string
format_converted?: boolean
original_request_body?: unknown
converted_request_body?: unknown
size?: string | null
video_urls?: string[] | null
thumbnail_url?: string | null
video_size_bytes?: number | null
video_duration_seconds?: number | null
video_expires_at?: string | null
stored_video_path?: string | null
storage_provider?: string | null
retry_count?: number
max_retries?: number
poll_interval_seconds?: number
next_poll_at?: string | null
endpoint?: {
id: string
base_url: string
api_format: string
} | null
request_metadata: AsyncTaskRequestMetadata | null
request_metadata?: AsyncTaskRequestMetadata | null
}
// 异步任务列表响应
export interface AsyncTaskListResponse {
items: AsyncTaskItem[]
total: number
page: number
page_size: number
pages: number
definitions?: AsyncTaskDefinition[]
}
// 异步任务统计响应
export interface AsyncTaskStatsResponse {
total: number
by_status: Record<AsyncTaskStatus, number>
by_model: Record<string, number>
today_count: number
active_users?: number // 仅管理员
processing_count?: number // 仅管理员
running_count?: number
registered_tasks?: number
by_status: Partial<Record<AsyncTaskStatus, number>>
by_kind?: Partial<Record<AsyncTaskKind, number>>
by_model?: Record<string, number>
today_count?: number
active_users?: number
processing_count?: number
}
// 异步任务查询参数
export interface AsyncTaskQueryParams {
status?: AsyncTaskStatus
kind?: AsyncTaskKind
task_type?: AsyncTaskType
task_key?: string
trigger?: string
user_id?: string
model?: string
page?: number
page_size?: number
}
function normalizeStatus(status: AsyncTaskStatus): AsyncTaskStatus {
if (status === 'processing') return 'running'
if (status === 'submitted' || status === 'pending') return 'queued'
if (status === 'completed') return 'succeeded'
return status
}
export const asyncTasksApi = {
/**
* 获取异步任务列表
*/
async list(params: AsyncTaskQueryParams = {}): Promise<AsyncTaskListResponse> {
const searchParams = new URLSearchParams()
if (params.status) searchParams.append('status', params.status)
if (params.task_type) searchParams.append('task_type', params.task_type)
if (params.user_id) searchParams.append('user_id', params.user_id)
if (params.model) searchParams.append('model', params.model)
if (params.status) searchParams.append('status', normalizeStatus(params.status))
if (params.kind) searchParams.append('kind', params.kind)
if (params.task_type && params.task_type !== 'video') searchParams.append('kind', params.task_type)
if (params.task_key) searchParams.append('task_key', params.task_key)
if (params.trigger) searchParams.append('trigger', params.trigger)
if (params.page) searchParams.append('page', params.page.toString())
if (params.page_size) searchParams.append('page_size', params.page_size.toString())
const query = searchParams.toString()
// 后端 API 路径保持不变,前端抽象为异步任务
const url = query ? `/api/admin/video-tasks?${query}` : '/api/admin/video-tasks'
const url = query ? `/api/admin/tasks?${query}` : '/api/admin/tasks'
const response = await apiClient.get(url)
return response.data
},
/**
* 获取异步任务统计
*/
async getStats(): Promise<AsyncTaskStatsResponse> {
const response = await apiClient.get('/api/admin/video-tasks/stats')
const response = await apiClient.get('/api/admin/tasks/stats')
return response.data
},
/**
* 获取异步任务详情
*/
async getDetail(taskId: string): Promise<AsyncTaskDetail> {
const response = await apiClient.get(`/api/admin/video-tasks/${taskId}`)
const response = await apiClient.get(`/api/admin/tasks/${taskId}`)
return response.data
},
async getEvents(taskId: string): Promise<{ items: AsyncTaskEvent[] }> {
const response = await apiClient.get(`/api/admin/tasks/${taskId}/events`)
return response.data
},
/**
* 取消异步任务
*/
async cancel(taskId: string): Promise<{ id: string; status: string; message: string }> {
const response = await apiClient.post(`/api/admin/video-tasks/${taskId}/cancel`)
const response = await apiClient.post(`/api/admin/tasks/${taskId}/cancel`)
return response.data
},
async trigger(taskKey: string, payload: Record<string, unknown> = {}): Promise<{ run_id: string; status: string }> {
const response = await apiClient.post(`/api/admin/tasks/${taskKey}/trigger`, payload)
return response.data
},
}

View File

@@ -171,6 +171,21 @@ export interface ApiKey {
force_capabilities?: Record<string, boolean> | null // 强制能力配置
}
export type InstallTargetCli = 'claude_code' | 'codex_cli' | 'gemini_cli'
export type InstallTargetSystem = 'macos' | 'linux' | 'windows' | 'auto'
export interface ApiKeyInstallSession {
install_code: string
expires_at_unix_secs: number
expires_in_seconds: number
target_cli: InstallTargetCli
target_cli_label: string
target_system: InstallTargetSystem
target_system_label: string
unix_command: string
powershell_command: string
}
// 不再需要 ProviderBinding 接口
export interface ChangePasswordRequest {
@@ -270,6 +285,17 @@ export const meApi = {
return response.data
},
async createApiKeyInstallSession(
keyId: string,
data: { target_cli: InstallTargetCli; target_system: InstallTargetSystem }
): Promise<ApiKeyInstallSession> {
const response = await apiClient.post<ApiKeyInstallSession>(
`/api/users/me/api-keys/${keyId}/install-sessions`,
data
)
return response.data
},
// 使用统计
async getUsage(params?: {
start_date?: string

View File

@@ -22,6 +22,15 @@ export interface ImageProgress {
downstream_heartbeat_interval_ms?: number | null
}
export interface CandidateResponseBoundary {
source?: string
status_code?: number | null
headers?: Record<string, unknown> | null
body?: unknown
body_ref?: string | null
body_state?: string | null
}
export interface CandidateRecord {
id: string
request_id: string
@@ -60,7 +69,10 @@ export interface CandidateRecord {
concurrent_requests?: number
ranking?: CandidateRankingMetadata | null
image_progress?: ImageProgress | null
extra_data?: Record<string, unknown>
extra_data?: Record<string, unknown> & {
upstream_response?: CandidateResponseBoundary
image_progress?: ImageProgress | null
}
created_at: string
started_at?: string
finished_at?: string
@@ -68,6 +80,9 @@ export interface CandidateRecord {
export interface RequestTrace {
request_id: string
request_path?: string
request_query_string?: string
request_path_and_query?: string
total_candidates: number
final_status: 'success' | 'failed' | 'streaming' | 'pending' | 'cancelled'
total_latency_ms: number