mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
Merge remote-tracking branch 'upstream/aether-rust-pioneer' into codex/user-groups-default-permissions
This commit is contained in:
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
<div class="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
||||
<Loader2
|
||||
class="w-5 h-5 text-blue-500"
|
||||
:class="{ 'animate-spin': (stats?.processing_count ?? 0) > 0 }"
|
||||
:class="{ 'animate-spin': runningCount > 0 }"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold">
|
||||
{{ stats?.processing_count ?? stats?.by_status?.processing ?? '-' }}
|
||||
{{ runningCount || '-' }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
处理中
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold">
|
||||
{{ stats?.by_status?.completed ?? '-' }}
|
||||
{{ stats?.by_status?.succeeded ?? '-' }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
已完成
|
||||
@@ -69,10 +69,10 @@
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold">
|
||||
{{ stats?.today_count ?? '-' }}
|
||||
{{ stats?.registered_tasks ?? '-' }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
今日任务
|
||||
已注册
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,14 +102,17 @@
|
||||
<SelectItem value="all">
|
||||
全部状态
|
||||
</SelectItem>
|
||||
<SelectItem value="submitted">
|
||||
已提交
|
||||
<SelectItem value="queued">
|
||||
排队中
|
||||
</SelectItem>
|
||||
<SelectItem value="processing">
|
||||
处理中
|
||||
<SelectItem value="running">
|
||||
运行中
|
||||
</SelectItem>
|
||||
<SelectItem value="completed">
|
||||
已完成
|
||||
<SelectItem value="retrying">
|
||||
重试中
|
||||
</SelectItem>
|
||||
<SelectItem value="succeeded">
|
||||
成功
|
||||
</SelectItem>
|
||||
<SelectItem value="failed">
|
||||
失败
|
||||
@@ -117,13 +120,16 @@
|
||||
<SelectItem value="cancelled">
|
||||
已取消
|
||||
</SelectItem>
|
||||
<SelectItem value="skipped">
|
||||
已跳过
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<!-- 模型筛选 -->
|
||||
<Input
|
||||
v-model="filterModel"
|
||||
type="text"
|
||||
placeholder="模型..."
|
||||
placeholder="任务 Key..."
|
||||
class="w-32 h-8 text-xs"
|
||||
/>
|
||||
<!-- 刷新按钮 -->
|
||||
@@ -207,13 +213,13 @@
|
||||
v-if="isVideoTask(task)"
|
||||
class="w-4 h-4 text-muted-foreground shrink-0"
|
||||
/>
|
||||
<span class="font-medium text-sm truncate">{{ task.model }}</span>
|
||||
<span class="font-medium text-sm truncate">{{ displayTaskName(task) }}</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-xs text-muted-foreground truncate max-w-[280px]"
|
||||
:title="task.prompt"
|
||||
:title="displayTaskDescription(task)"
|
||||
>
|
||||
{{ task.prompt }}
|
||||
{{ displayTaskDescription(task) }}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -229,7 +235,7 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Server class="w-3 h-3" />
|
||||
<span class="truncate max-w-[100px]">{{ task.provider_name }}</span>
|
||||
<span class="truncate max-w-[100px]">{{ displayTaskSource(task) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -243,7 +249,7 @@
|
||||
{{ getStatusLabel(task.status) }}
|
||||
</Badge>
|
||||
<div
|
||||
v-if="task.progress_percent > 0 && task.status === 'processing'"
|
||||
v-if="task.progress_percent > 0 && isRunningStatus(task.status)"
|
||||
class="w-full"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -262,11 +268,11 @@
|
||||
<TableCell>
|
||||
<div class="text-xs space-y-0.5 text-muted-foreground">
|
||||
<div
|
||||
v-if="task.duration_seconds"
|
||||
v-if="task.duration_seconds || task.attempt"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<Timer class="w-3 h-3" />
|
||||
<span>{{ task.duration_seconds }}s</span>
|
||||
<span>{{ task.duration_seconds ? `${task.duration_seconds}s` : `${task.attempt}/${task.max_attempts ?? 1}` }}</span>
|
||||
</div>
|
||||
<div v-if="task.resolution">
|
||||
{{ task.resolution }}
|
||||
@@ -284,11 +290,11 @@
|
||||
<span>{{ formatDate(task.created_at) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.completed_at"
|
||||
v-if="finishTime(task)"
|
||||
class="flex items-center gap-1.5 text-green-600 dark:text-green-400"
|
||||
>
|
||||
<CheckCircle class="w-3 h-3" />
|
||||
<span>{{ formatDate(task.completed_at) }}</span>
|
||||
<span>{{ formatDate(finishTime(task)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -305,6 +311,7 @@
|
||||
<Eye class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isVideoTask(task)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
@@ -337,7 +344,7 @@
|
||||
v-if="isVideoTask(task)"
|
||||
class="w-4 h-4 text-muted-foreground shrink-0"
|
||||
/>
|
||||
<span class="font-medium text-sm truncate">{{ task.model }}</span>
|
||||
<span class="font-medium text-sm truncate">{{ displayTaskName(task) }}</span>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="getStatusVariant(task.status)"
|
||||
@@ -349,7 +356,7 @@
|
||||
|
||||
<!-- 进度条(如果有) -->
|
||||
<div
|
||||
v-if="task.progress_percent > 0 && task.status === 'processing'"
|
||||
v-if="task.progress_percent > 0 && isRunningStatus(task.status)"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
@@ -365,7 +372,7 @@
|
||||
|
||||
<!-- Prompt -->
|
||||
<p class="text-sm text-muted-foreground line-clamp-2">
|
||||
{{ task.prompt }}
|
||||
{{ displayTaskDescription(task) }}
|
||||
</p>
|
||||
|
||||
<!-- 信息网格 -->
|
||||
@@ -379,24 +386,25 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Server class="w-3 h-3" />
|
||||
<span class="truncate">{{ task.provider_name }}</span>
|
||||
<span class="truncate">{{ displayTaskSource(task) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Clock class="w-3 h-3" />
|
||||
<span>{{ formatDate(task.created_at) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.duration_seconds"
|
||||
v-if="task.duration_seconds || task.attempt"
|
||||
class="flex items-center gap-1.5 text-muted-foreground"
|
||||
>
|
||||
<Timer class="w-3 h-3" />
|
||||
<span>{{ task.duration_seconds }}s</span>
|
||||
<span>{{ task.duration_seconds ? `${task.duration_seconds}s` : `${task.attempt}/${task.max_attempts ?? 1}` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
v-if="isVideoTask(task)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-7 text-xs"
|
||||
@@ -459,7 +467,7 @@
|
||||
v-if="isVideoTask(selectedTask)"
|
||||
class="w-3.5 h-3.5 mr-1"
|
||||
/>
|
||||
<span>{{ selectedTask.model }}</span>
|
||||
<span>{{ displayTaskName(selectedTask) }}</span>
|
||||
</div>
|
||||
<Badge :variant="getStatusVariant(selectedTask.status)">
|
||||
{{ getStatusLabel(selectedTask.status) }}
|
||||
@@ -503,11 +511,11 @@
|
||||
<span>用户: {{ selectedTask.username }}</span>
|
||||
</template>
|
||||
<span class="opacity-40">|</span>
|
||||
<span>Provider: {{ selectedTask.provider_name }}</span>
|
||||
<span>{{ displayTaskSource(selectedTask) }}</span>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div
|
||||
v-if="selectedTask.progress_percent > 0 && selectedTask.status === 'processing'"
|
||||
v-if="selectedTask.progress_percent > 0 && isRunningStatus(selectedTask.status)"
|
||||
class="mt-3"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -641,7 +649,7 @@
|
||||
|
||||
<!-- 任务完成但无视频 -->
|
||||
<div
|
||||
v-else-if="selectedTask.status === 'completed'"
|
||||
v-else-if="isSucceededStatus(selectedTask.status) && isVideoTask(selectedTask)"
|
||||
class="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg border border-amber-200 dark:border-amber-800 text-center"
|
||||
>
|
||||
<Video class="w-8 h-8 mx-auto mb-2 text-amber-500" />
|
||||
@@ -660,14 +668,14 @@
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-xs"
|
||||
@click="copyToClipboard(selectedTask.prompt)"
|
||||
@click="copyToClipboard(displayTaskDescription(selectedTask))"
|
||||
>
|
||||
<Copy class="w-3 h-3 mr-1" />
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-3 bg-muted/50 rounded-lg border border-border/60 text-sm whitespace-pre-wrap break-words max-h-32 overflow-y-auto leading-relaxed">
|
||||
{{ selectedTask.prompt }}
|
||||
{{ displayTaskDescription(selectedTask) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -738,7 +746,7 @@
|
||||
轮询
|
||||
</p>
|
||||
<p class="text-sm font-medium">
|
||||
{{ selectedTask.poll_count }} / {{ selectedTask.max_poll_count }}
|
||||
{{ selectedTask.poll_count ?? selectedTask.attempt ?? 0 }} / {{ selectedTask.max_poll_count ?? selectedTask.max_attempts ?? 1 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-muted/30 rounded-lg">
|
||||
@@ -746,7 +754,7 @@
|
||||
重试
|
||||
</p>
|
||||
<p class="text-sm font-medium">
|
||||
{{ selectedTask.retry_count }} / {{ selectedTask.max_retries }}
|
||||
{{ selectedTask.retry_count ?? 0 }} / {{ selectedTask.max_retries ?? selectedTask.max_attempts ?? 1 }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-muted/30 rounded-lg">
|
||||
@@ -754,7 +762,7 @@
|
||||
轮询间隔
|
||||
</p>
|
||||
<p class="text-sm font-medium">
|
||||
{{ selectedTask.poll_interval_seconds }}s
|
||||
{{ selectedTask.poll_interval_seconds ? `${selectedTask.poll_interval_seconds}s` : selectedTask.trigger ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
@@ -780,13 +788,13 @@
|
||||
<span>{{ formatTimeWithMs(selectedTask.created_at) }}</span>
|
||||
<span class="time-arrow-container">
|
||||
<span
|
||||
v-if="selectedTask.completed_at"
|
||||
v-if="finishTime(selectedTask)"
|
||||
class="time-duration"
|
||||
>+{{ calcDuration(selectedTask.created_at, selectedTask.completed_at) }}</span>
|
||||
>+{{ calcDuration(selectedTask.created_at, finishTime(selectedTask) || selectedTask.created_at) }}</span>
|
||||
<span class="time-arrow">→</span>
|
||||
</span>
|
||||
<template v-if="selectedTask.completed_at">
|
||||
<span>{{ formatTimeWithMs(selectedTask.completed_at) }}</span>
|
||||
<template v-if="finishTime(selectedTask)">
|
||||
<span>{{ formatTimeWithMs(finishTime(selectedTask)) }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
@@ -917,10 +925,47 @@ let overviewRefreshInFlight = false
|
||||
// 使用记录详情抽屉状态
|
||||
const usageDetailOpen = ref(false)
|
||||
const usageRequestId = ref<string | null>(null)
|
||||
const runningCount = computed(() => {
|
||||
return stats.value?.running_count
|
||||
?? stats.value?.processing_count
|
||||
?? stats.value?.by_status?.running
|
||||
?? stats.value?.by_status?.processing
|
||||
?? 0
|
||||
})
|
||||
|
||||
// 判断是否为视频任务
|
||||
function isVideoTask(task: AsyncTaskItem): boolean {
|
||||
return task.task_type === 'video' || !!task.video_url || !!task.duration_seconds
|
||||
return task.task_type === 'video' || !!task.video_url || !!task.duration_seconds || task.task_key === 'video.task.poller'
|
||||
}
|
||||
|
||||
function displayTaskName(task: AsyncTaskItem | AsyncTaskDetail): string {
|
||||
return task.model || task.task_key || task.id
|
||||
}
|
||||
|
||||
function displayTaskDescription(task: AsyncTaskItem | AsyncTaskDetail): string {
|
||||
if (task.prompt) return task.prompt
|
||||
if (task.progress_message) return task.progress_message
|
||||
if (task.error_message) return task.error_message
|
||||
if (task.payload) return formatJson(task.payload)
|
||||
return task.trigger || task.kind || '-'
|
||||
}
|
||||
|
||||
function displayTaskSource(task: AsyncTaskItem | AsyncTaskDetail): string {
|
||||
if (task.provider_name) return `Provider: ${task.provider_name}`
|
||||
if (task.kind || task.trigger) return `${task.kind ?? 'task'} / ${task.trigger ?? '-'}`
|
||||
return task.owner_instance || '-'
|
||||
}
|
||||
|
||||
function finishTime(task: AsyncTaskItem | AsyncTaskDetail): string | null {
|
||||
return task.finished_at || task.completed_at || null
|
||||
}
|
||||
|
||||
function isRunningStatus(status: string): boolean {
|
||||
return ['running', 'retrying', 'processing', 'submitted', 'pending', 'queued'].includes(status)
|
||||
}
|
||||
|
||||
function isSucceededStatus(status: string): boolean {
|
||||
return status === 'succeeded' || status === 'completed'
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
@@ -929,7 +974,7 @@ async function fetchTasks() {
|
||||
try {
|
||||
const response = await asyncTasksApi.list({
|
||||
status: filterStatus.value !== 'all' ? filterStatus.value as AsyncTaskStatus : undefined,
|
||||
model: filterModel.value || undefined,
|
||||
task_key: filterModel.value || undefined,
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
@@ -1085,6 +1130,7 @@ async function cancelTask(task: AsyncTaskItem | AsyncTaskDetail) {
|
||||
// 状态相关
|
||||
function getStatusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
switch (status) {
|
||||
case 'succeeded':
|
||||
case 'completed':
|
||||
return 'default'
|
||||
case 'failed':
|
||||
@@ -1101,16 +1147,20 @@ function getStatusLabel(status: string): string {
|
||||
pending: '待处理',
|
||||
submitted: '已提交',
|
||||
queued: '排队中',
|
||||
running: '运行中',
|
||||
retrying: '重试中',
|
||||
processing: '处理中',
|
||||
succeeded: '成功',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
skipped: '已跳过',
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
function canCancel(status: string): boolean {
|
||||
return ['pending', 'submitted', 'queued', 'processing'].includes(status)
|
||||
return ['pending', 'submitted', 'queued', 'processing', 'running', 'retrying'].includes(status)
|
||||
}
|
||||
|
||||
// 格式化日期(简短格式,用于表格列表)
|
||||
@@ -1229,7 +1279,7 @@ watch(filterModel, () => {
|
||||
// 检查是否有进行中的任务
|
||||
const hasProcessingTasks = computed(() => {
|
||||
return tasks.value.some(t =>
|
||||
['pending', 'submitted', 'queued', 'processing'].includes(t.status)
|
||||
['pending', 'submitted', 'queued', 'processing', 'running', 'retrying'].includes(t.status)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user