Compare commits

..

3 Commits

Author SHA1 Message Date
CI Bot 57d45c3219 fix: 恢复被ruff误删的向后兼容re-export常量
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 19s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 9s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m24s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m38s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m53s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 50m4s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 28s
- 9个engine模块的re-export常量被ruff --fix误删(F401)
- 这些是测试文件依赖的向后兼容导出
- 全部加回并标记 # noqa: F401
2026-07-27 12:05:09 +08:00
CI Bot fbf3c5288e fix: 全量修复ruff/black/isort代码质量问题
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 23s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 58s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m56s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m2s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 3m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m59s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 2m58s
- 修复50个ruff错误(未使用import、重复定义、命名歧义等)
- 解决black与isort在长import行上的死循环问题
- 2个文件使用isort:off保护向后兼容re-export块
- 全量657个文件通过ruff+black+isort三重检查
2026-07-27 11:49:17 +08:00
CI Bot c8163cbadc fix: 修复全量ruff/black/isort代码质量问题 (50个错误)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 54s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 7s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m37s
AI Code Review / AI Code Review (pull_request) Successful in 1m43s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 3m35s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m44s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 7m16s
2026-07-27 11:25:58 +08:00
26 changed files with 421 additions and 612 deletions
-1
View File
@@ -12,7 +12,6 @@
from __future__ import annotations
import json
import logging
from typing import Any, Dict, List, Optional
+386 -39
View File
@@ -2,32 +2,333 @@
* 任务中心页面
* 展示用户的所有任务(生成任务、素材导入等),支持状态筛选、类型筛选、分页、重试
*/
import { Button } from "antd"
import { CloseCircleOutlined } from "@ant-design/icons"
import { TaskFilterBar } from "./components/TaskFilterBar"
import { TaskTable } from "./components/TaskTable"
import { useTaskList } from "./hooks/useTaskList"
import { useState } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Table, Tabs, Select, Tag, Button, message, Popconfirm, Tooltip } from "antd"
import {
CheckCircleOutlined,
ClockCircleOutlined,
SyncOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
MinusCircleOutlined,
RedoOutlined,
InfoCircleOutlined,
} from "@ant-design/icons"
import type { ColumnsType } from "antd/es/table"
import {
getTasks,
retryTask,
type TaskItem,
type TaskStatus,
type TaskListParams,
} from "@/api/tasks"
import "./tasks.css"
/* ──────────── 常量 ──────────── */
/** 状态 Tab 配置 */
const STATUS_TABS: { key: TaskStatus | "all"; label: string }[] = [
{ key: "all", label: "全部" },
{ key: "waiting", label: "等待中" },
{ key: "running", label: "进行中" },
{ key: "completed", label: "已完成" },
{ key: "failed", label: "失败" },
{ key: "cancelled", label: "已取消" },
]
/** 类型筛选选项 */
const TYPE_OPTIONS = [
{ value: "all", label: "全部类型" },
{ value: "generation", label: "生成任务" },
{ value: "ingest", label: "素材导入" },
]
/** 状态标签配置 */
const STATUS_CONFIG: Record<TaskStatus, { label: string; color: string; icon: React.ReactNode }> = {
pending: {
label: "等待中",
color: "default",
icon: <ClockCircleOutlined />,
},
waiting: {
label: "排队中",
color: "processing",
icon: <SyncOutlined spin />,
},
running: {
label: "进行中",
color: "processing",
icon: <SyncOutlined spin />,
},
completed: {
label: "已完成",
color: "success",
icon: <CheckCircleOutlined />,
},
failed: {
label: "失败",
color: "error",
icon: <CloseCircleOutlined />,
},
cancelled: {
label: "已取消",
color: "default",
icon: <MinusCircleOutlined />,
},
}
/** 任务类型标签 */
const TYPE_LABELS: Record<string, { label: string; color: string }> = {
generation: { label: "生成任务", color: "blue" },
ingest: { label: "素材导入", color: "green" },
}
/* ──────────── 工具函数 ──────────── */
/** 格式化耗时 */
const formatDuration = (seconds?: number): string => {
if (!seconds) return "-"
if (seconds < 60) return `${Math.round(seconds)}`
const minutes = Math.floor(seconds / 60)
const secs = Math.round(seconds % 60)
if (minutes < 60) return `${minutes}${secs}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}小时${mins}`
}
/** 格式化时间 */
const formatTime = (dateStr?: string | null): string => {
if (!dateStr) return "-"
const date = new Date(dateStr)
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
}
/* ──────────── 主组件 ──────────── */
export default function TaskCenter() {
const {
statusFilter,
typeFilter,
const queryClient = useQueryClient()
// 筛选状态
const [statusFilter, setStatusFilter] = useState<TaskStatus | "all">("all")
const [typeFilter, setTypeFilter] = useState<string>("all")
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null)
const [expandedTaskDetail, setExpandedTaskDetail] = useState<TaskItem | null>(null)
// 查询参数
const queryParams: TaskListParams = {
page,
pageSize,
expandedTaskId,
expandedTaskDetail,
data,
isLoading,
error,
retryLoading,
handleStatusChange,
handleTypeChange,
handlePageChange,
handleExpand,
handleViewDetail,
handleRetry,
} = useTaskList()
page_size: pageSize,
...(statusFilter !== "all" && { status: statusFilter }),
...(typeFilter !== "all" && { task_type: typeFilter }),
}
// 获取任务列表
const { data, isLoading, error } = useQuery({
queryKey: ["tasks", queryParams],
queryFn: () => getTasks(queryParams),
refetchInterval: (query) => {
// 有进行中的任务时自动刷新
const tasks = query.state.data?.items ?? []
const hasRunning = tasks.some((t) => t.status === "running" || t.status === "waiting")
return hasRunning ? 5000 : false
},
})
// 重试任务
const retryMutation = useMutation({
mutationFn: retryTask,
onSuccess: () => {
message.success("任务已重新提交")
queryClient.invalidateQueries({ queryKey: ["tasks"] })
},
onError: () => {
message.error("重试失败,请检查任务状态")
},
})
// 展开查看详情
const handleExpand = async (expanded: boolean, record: TaskItem) => {
if (!expanded) {
setExpandedTaskId(null)
setExpandedTaskDetail(null)
return
}
setExpandedTaskId(record.id)
// 如果是失败任务,获取详情(含 error_info
if (record.status === "failed" && record.error_info) {
setExpandedTaskDetail(record)
}
}
// 表格列定义
const columns: ColumnsType<TaskItem> = [
{
title: "任务ID",
dataIndex: "id",
key: "id",
width: 120,
ellipsis: true,
render: (id: string) => (
<Tooltip title={id}>
<span className="task-id">{id.slice(0, 8)}...</span>
</Tooltip>
),
},
{
title: "类型",
dataIndex: "task_type",
key: "task_type",
width: 100,
render: (type: string) => {
const config = TYPE_LABELS[type] || { label: type, color: "default" }
return <Tag color={config.color}>{config.label}</Tag>
},
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status: TaskStatus, record: TaskItem) => {
const config = STATUS_CONFIG[status] || {
label: status,
color: "default",
icon: null,
}
return (
<Tag color={config.color} icon={config.icon} className="task-status-tag">
{config.label}
{status === "running" && record.progress > 0 && (
<span className="task-progress"> {record.progress}%</span>
)}
</Tag>
)
},
},
{
title: "当前步骤",
dataIndex: "current_step",
key: "current_step",
width: 150,
ellipsis: true,
render: (step: string) => <span className="task-step">{step || "-"}</span>,
},
{
title: "耗时",
dataIndex: "duration_seconds",
key: "duration_seconds",
width: 100,
render: (seconds: number) => <span className="task-duration">{formatDuration(seconds)}</span>,
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
width: 120,
render: (time: string) => <span className="task-time">{formatTime(time)}</span>,
},
{
title: "操作",
key: "action",
width: 100,
fixed: "right",
render: (_: unknown, record: TaskItem) => {
if (record.status === "failed" && record.retryable) {
return (
<Popconfirm
title="确认重试"
description="确定要重试这个失败的任务吗?"
onConfirm={() => retryMutation.mutate(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
size="small"
icon={<RedoOutlined />}
loading={retryMutation.isPending}
className="task-retry-btn"
>
</Button>
</Popconfirm>
)
}
if (record.status === "failed") {
return (
<Button
type="link"
size="small"
icon={<InfoCircleOutlined />}
onClick={() => {
setExpandedTaskId(record.id)
setExpandedTaskDetail(record)
}}
>
</Button>
)
}
return <span className="task-action-placeholder">-</span>
},
},
]
// 展开行渲染(错误详情)
const expandedRowRender = (record: TaskItem) => {
const detail = expandedTaskDetail || record
const errorInfo = detail.error_info
if (!errorInfo && !detail.error_message) {
return <div className="task-expand-empty"></div>
}
return (
<div className="task-error-detail">
<div className="task-error-header">
<ExclamationCircleOutlined className="task-error-icon" />
<span></span>
</div>
<div className="task-error-body">
{errorInfo?.error_type && (
<div className="task-error-row">
<span className="task-error-label"></span>
<Tag color="error">{errorInfo.error_type}</Tag>
</div>
)}
{(errorInfo?.error_message || detail.error_message) && (
<div className="task-error-row">
<span className="task-error-label"></span>
<span className="task-error-message">
{errorInfo?.error_message || detail.error_message}
</span>
</div>
)}
{errorInfo?.failed_step && (
<div className="task-error-row">
<span className="task-error-label"></span>
<span>{errorInfo.failed_step}</span>
</div>
)}
{errorInfo?.stack_trace && (
<div className="task-error-row task-error-stack">
<span className="task-error-label"></span>
<pre>{errorInfo.stack_trace}</pre>
</div>
)}
</div>
</div>
)
}
// 错误处理
if (error) {
@@ -50,26 +351,72 @@ export default function TaskCenter() {
<p className="task-subtitle"></p>
</div>
<TaskFilterBar
statusFilter={statusFilter}
typeFilter={typeFilter}
onStatusChange={handleStatusChange}
onTypeChange={handleTypeChange}
/>
{/* 筛选栏 */}
<div className="task-filters">
{/* 状态 Tab */}
<Tabs
activeKey={statusFilter}
onChange={(key) => {
setStatusFilter(key as TaskStatus | "all")
setPage(1)
}}
items={STATUS_TABS.map((tab) => ({
key: tab.key,
label: tab.label,
}))}
className="task-status-tabs"
/>
<TaskTable
{/* 类型筛选 */}
<div className="task-type-filter">
<Select
value={typeFilter}
onChange={(value) => {
setTypeFilter(value)
setPage(1)
}}
options={TYPE_OPTIONS}
style={{ width: 140 }}
placeholder="选择类型"
/>
</div>
</div>
{/* 任务表格 */}
<Table
columns={columns}
dataSource={data?.items || []}
rowKey="id"
loading={isLoading}
total={data?.total || 0}
page={page}
pageSize={pageSize}
expandedTaskId={expandedTaskId}
expandedTaskDetail={expandedTaskDetail}
retryLoading={retryLoading}
onPageChange={handlePageChange}
onExpand={handleExpand}
onRetry={handleRetry}
onViewDetail={handleViewDetail}
pagination={{
current: page,
pageSize,
total: data?.total || 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
onChange: (p, ps) => {
setPage(p)
setPageSize(ps)
},
}}
expandable={{
expandedRowRender,
expandedRowKeys: expandedTaskId ? [expandedTaskId] : [],
onExpand: handleExpand,
rowExpandable: (record) =>
record.status === "failed" && (!!record.error_info || !!record.error_message),
}}
scroll={{ x: 800 }}
className="task-table"
locale={{
emptyText: (
<div className="task-empty">
<ClockCircleOutlined />
<p></p>
</div>
),
}}
/>
</div>
)
@@ -1,58 +0,0 @@
import React from "react"
import { Tag } from "antd"
import { ExclamationCircleOutlined } from "@ant-design/icons"
import type { TaskItem } from "@/api/tasks"
interface TaskErrorDetailProps {
record: TaskItem
detail?: TaskItem | null
}
/**
* 任务错误详情展开行
*/
export const TaskErrorDetail: React.FC<TaskErrorDetailProps> = ({ record, detail }) => {
const d = detail || record
const errorInfo = d.error_info
if (!errorInfo && !d.error_message) {
return <div className="task-expand-empty"></div>
}
return (
<div className="task-error-detail">
<div className="task-error-header">
<ExclamationCircleOutlined className="task-error-icon" />
<span></span>
</div>
<div className="task-error-body">
{errorInfo?.error_type && (
<div className="task-error-row">
<span className="task-error-label"></span>
<Tag color="error">{errorInfo.error_type}</Tag>
</div>
)}
{(errorInfo?.error_message || d.error_message) && (
<div className="task-error-row">
<span className="task-error-label"></span>
<span className="task-error-message">
{errorInfo?.error_message || d.error_message}
</span>
</div>
)}
{errorInfo?.failed_step && (
<div className="task-error-row">
<span className="task-error-label"></span>
<span>{errorInfo.failed_step}</span>
</div>
)}
{errorInfo?.stack_trace && (
<div className="task-error-row task-error-stack">
<span className="task-error-label"></span>
<pre>{errorInfo.stack_trace}</pre>
</div>
)}
</div>
</div>
)
}
@@ -1,48 +0,0 @@
import React from "react"
import { Tabs, Select } from "antd"
import { STATUS_TABS, TYPE_OPTIONS } from "../constants"
import type { TaskStatus } from "@/api/tasks"
interface TaskFilterBarProps {
statusFilter: TaskStatus | "all"
typeFilter: string
onStatusChange: (key: string) => void
onTypeChange: (value: string) => void
}
/**
* 任务筛选栏
* 状态 Tab + 类型下拉筛选
*/
export const TaskFilterBar: React.FC<TaskFilterBarProps> = ({
statusFilter,
typeFilter,
onStatusChange,
onTypeChange,
}) => {
return (
<div className="task-filters">
{/* 状态 Tab */}
<Tabs
activeKey={statusFilter}
onChange={onStatusChange}
items={STATUS_TABS.map((tab) => ({
key: tab.key,
label: tab.label,
}))}
className="task-status-tabs"
/>
{/* 类型筛选 */}
<div className="task-type-filter">
<Select
value={typeFilter}
onChange={onTypeChange}
options={TYPE_OPTIONS}
style={{ width: 140 }}
placeholder="选择类型"
/>
</div>
</div>
)
}
@@ -1,190 +0,0 @@
import React from "react"
import { Table, Tag, Button, Popconfirm, Tooltip } from "antd"
import { RedoOutlined, InfoCircleOutlined, ClockCircleOutlined } from "@ant-design/icons"
import type { ColumnsType } from "antd/es/table"
import type { TaskItem, TaskStatus } from "@/api/tasks"
import { STATUS_CONFIG, TYPE_LABELS } from "../constants"
import { formatDuration, formatTime } from "../utils"
import { TaskErrorDetail } from "./TaskErrorDetail"
interface TaskTableProps {
dataSource: TaskItem[]
loading: boolean
total: number
page: number
pageSize: number
expandedTaskId: string | null
expandedTaskDetail: TaskItem | null
retryLoading: boolean
onPageChange: (page: number, pageSize: number) => void
onExpand: (expanded: boolean, record: TaskItem) => void
onRetry: (id: string) => void
onViewDetail: (record: TaskItem) => void
}
/**
* 任务列表表格
* 含列定义、分页、展开行
*/
export const TaskTable: React.FC<TaskTableProps> = ({
dataSource,
loading,
total,
page,
pageSize,
expandedTaskId,
expandedTaskDetail,
retryLoading,
onPageChange,
onExpand,
onRetry,
onViewDetail,
}) => {
// 表格列定义
const columns: ColumnsType<TaskItem> = [
{
title: "任务ID",
dataIndex: "id",
key: "id",
width: 120,
ellipsis: true,
render: (id: string) => (
<Tooltip title={id}>
<span className="task-id">{id.slice(0, 8)}...</span>
</Tooltip>
),
},
{
title: "类型",
dataIndex: "task_type",
key: "task_type",
width: 100,
render: (type: string) => {
const config = TYPE_LABELS[type] || { label: type, color: "default" }
return <Tag color={config.color}>{config.label}</Tag>
},
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status: TaskStatus, record: TaskItem) => {
const config = STATUS_CONFIG[status] || {
label: status,
color: "default",
icon: null,
}
return (
<Tag color={config.color} icon={config.icon} className="task-status-tag">
{config.label}
{status === "running" && record.progress > 0 && (
<span className="task-progress"> {record.progress}%</span>
)}
</Tag>
)
},
},
{
title: "当前步骤",
dataIndex: "current_step",
key: "current_step",
width: 150,
ellipsis: true,
render: (step: string) => <span className="task-step">{step || "-"}</span>,
},
{
title: "耗时",
dataIndex: "duration_seconds",
key: "duration_seconds",
width: 100,
render: (seconds: number) => <span className="task-duration">{formatDuration(seconds)}</span>,
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
width: 120,
render: (time: string) => <span className="task-time">{formatTime(time)}</span>,
},
{
title: "操作",
key: "action",
width: 100,
fixed: "right",
render: (_: unknown, record: TaskItem) => {
if (record.status === "failed" && record.retryable) {
return (
<Popconfirm
title="确认重试"
description="确定要重试这个失败的任务吗?"
onConfirm={() => onRetry(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
size="small"
icon={<RedoOutlined />}
loading={retryLoading}
className="task-retry-btn"
>
</Button>
</Popconfirm>
)
}
if (record.status === "failed") {
return (
<Button
type="link"
size="small"
icon={<InfoCircleOutlined />}
onClick={() => onViewDetail(record)}
>
</Button>
)
}
return <span className="task-action-placeholder">-</span>
},
},
]
return (
<Table
columns={columns}
dataSource={dataSource}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
onChange: onPageChange,
}}
expandable={{
expandedRowRender: (record) => (
<TaskErrorDetail record={record} detail={expandedTaskDetail} />
),
expandedRowKeys: expandedTaskId ? [expandedTaskId] : [],
onExpand,
rowExpandable: (record) =>
record.status === "failed" && (!!record.error_info || !!record.error_message),
}}
scroll={{ x: 800 }}
className="task-table"
locale={{
emptyText: (
<div className="task-empty">
<ClockCircleOutlined />
<p></p>
</div>
),
}}
/>
)
}
-69
View File
@@ -1,69 +0,0 @@
import React from "react"
import {
CheckCircleOutlined,
ClockCircleOutlined,
SyncOutlined,
CloseCircleOutlined,
MinusCircleOutlined,
} from "@ant-design/icons"
import type { TaskStatus } from "@/api/tasks"
/** 状态 Tab 配置 */
export const STATUS_TABS: { key: TaskStatus | "all"; label: string }[] = [
{ key: "all", label: "全部" },
{ key: "waiting", label: "等待中" },
{ key: "running", label: "进行中" },
{ key: "completed", label: "已完成" },
{ key: "failed", label: "失败" },
{ key: "cancelled", label: "已取消" },
]
/** 类型筛选选项 */
export const TYPE_OPTIONS = [
{ value: "all", label: "全部类型" },
{ value: "generation", label: "生成任务" },
{ value: "ingest", label: "素材导入" },
]
/** 状态标签配置 */
export const STATUS_CONFIG: Record<
TaskStatus,
{ label: string; color: string; icon: React.ReactNode }
> = {
pending: {
label: "等待中",
color: "default",
icon: <ClockCircleOutlined />,
},
waiting: {
label: "排队中",
color: "processing",
icon: <SyncOutlined spin />,
},
running: {
label: "进行中",
color: "processing",
icon: <SyncOutlined spin />,
},
completed: {
label: "已完成",
color: "success",
icon: <CheckCircleOutlined />,
},
failed: {
label: "失败",
color: "error",
icon: <CloseCircleOutlined />,
},
cancelled: {
label: "已取消",
color: "default",
icon: <MinusCircleOutlined />,
},
}
/** 任务类型标签 */
export const TYPE_LABELS: Record<string, { label: string; color: string }> = {
generation: { label: "生成任务", color: "blue" },
ingest: { label: "素材导入", color: "green" },
}
@@ -1,119 +0,0 @@
import { useState, useCallback } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import {
getTasks,
retryTask,
type TaskItem,
type TaskStatus,
type TaskListParams,
} from "@/api/tasks"
/**
* 任务列表业务 Hook
* 封装筛选状态、数据获取、重试操作
*/
export const useTaskList = () => {
const queryClient = useQueryClient()
// 筛选状态
const [statusFilter, setStatusFilter] = useState<TaskStatus | "all">("all")
const [typeFilter, setTypeFilter] = useState<string>("all")
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null)
const [expandedTaskDetail, setExpandedTaskDetail] = useState<TaskItem | null>(null)
// 查询参数
const queryParams: TaskListParams = {
page,
page_size: pageSize,
...(statusFilter !== "all" && { status: statusFilter }),
...(typeFilter !== "all" && { task_type: typeFilter }),
}
// 获取任务列表
const { data, isLoading, error } = useQuery({
queryKey: ["tasks", queryParams],
queryFn: () => getTasks(queryParams),
refetchInterval: (query) => {
// 有进行中的任务时自动刷新
const tasks = query.state.data?.items ?? []
const hasRunning = tasks.some((t) => t.status === "running" || t.status === "waiting")
return hasRunning ? 5000 : false
},
})
// 重试任务
const retryMutation = useMutation({
mutationFn: retryTask,
onSuccess: () => {
message.success("任务已重新提交")
queryClient.invalidateQueries({ queryKey: ["tasks"] })
},
onError: () => {
message.error("重试失败,请检查任务状态")
},
})
// 状态筛选变化
const handleStatusChange = useCallback((key: string) => {
setStatusFilter(key as TaskStatus | "all")
setPage(1)
}, [])
// 类型筛选变化
const handleTypeChange = useCallback((value: string) => {
setTypeFilter(value)
setPage(1)
}, [])
// 分页变化
const handlePageChange = useCallback((p: number, ps: number) => {
setPage(p)
setPageSize(ps)
}, [])
// 展开查看详情
const handleExpand = useCallback((expanded: boolean, record: TaskItem) => {
if (!expanded) {
setExpandedTaskId(null)
setExpandedTaskDetail(null)
return
}
setExpandedTaskId(record.id)
// 如果是失败任务,获取详情(含 error_info
if (record.status === "failed" && record.error_info) {
setExpandedTaskDetail(record)
}
}, [])
// 查看错误详情
const handleViewDetail = useCallback((record: TaskItem) => {
setExpandedTaskId(record.id)
setExpandedTaskDetail(record)
}, [])
return {
// 状态
statusFilter,
typeFilter,
page,
pageSize,
expandedTaskId,
expandedTaskDetail,
// 数据
data,
isLoading,
error,
// Mutation
retryLoading: retryMutation.isPending,
// 操作
handleStatusChange,
handleTypeChange,
handlePageChange,
handleExpand,
handleViewDetail,
handleRetry: retryMutation.mutate,
}
}
-23
View File
@@ -1,23 +0,0 @@
/** 格式化耗时 */
export const formatDuration = (seconds?: number): string => {
if (!seconds) return "-"
if (seconds < 60) return `${Math.round(seconds)}`
const minutes = Math.floor(seconds / 60)
const secs = Math.round(seconds % 60)
if (minutes < 60) return `${minutes}${secs}`
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}小时${mins}`
}
/** 格式化时间 */
export const formatTime = (dateStr?: string | null): string => {
if (!dateStr) return "-"
const date = new Date(dateStr)
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
}
@@ -9,12 +9,11 @@
from __future__ import annotations
import logging
from typing import Optional
from packages.domain.chroma_key_config import CHROMA_KEY_PRESETS # noqa: F401
from packages.domain.chroma_key_config import apply_chroma_key_if_needed # noqa: F401
from packages.domain.chroma_key_config import (
CHROMA_KEY_PRESETS,
ChromaKeyConfig,
apply_chroma_key_if_needed,
)
from packages.domain.chroma_key_config import ( # noqa: F401 — 向后兼容
build_chromakey_filter as _build_chromakey_filter_base,
@@ -195,17 +195,3 @@ class ColorGradeEngine:
# ── 便捷函数 ──────────────────────────────────────────────────────────────────
def get_preset_names() -> list[tuple[str, str]]:
"""获取所有预设名称列表.
Returns:
[(preset_key, display_name), ...]
"""
return [(key, PRESET_DISPLAY_NAMES.get(key, key)) for key in PRESET_PARAMS.keys()]
def get_preset_params(preset: str) -> dict[str, float] | None:
"""获取指定预设的参数."""
return PRESET_PARAMS.get(preset)
+3 -5
View File
@@ -22,11 +22,9 @@ from shared.ffmpeg_utils import ( # noqa: F401
# xfade 转场纯逻辑已抽离到 domain 层,这里 re-export 保持向后兼容
from packages.domain.xfade_builder import DEFAULT_TRANSITION_DURATION as _default_transition_duration_base # noqa: F401
from packages.domain.xfade_builder import (
SUPPORTED_TRANSITIONS,
XFADE_TRANSITION_MAP,
XFade_TRANSITION_NAMES,
)
from packages.domain.xfade_builder import SUPPORTED_TRANSITIONS # noqa: F401
from packages.domain.xfade_builder import XFADE_TRANSITION_MAP # noqa: F401
from packages.domain.xfade_builder import XFade_TRANSITION_NAMES # noqa: F401
from packages.domain.xfade_builder import build_xfade_filter_chain as _build_xfade_filter_chain_base
from packages.domain.xfade_builder import chain_filters as _chain_filters_base
from packages.domain.xfade_builder import resolve_xfade_transition as _resolve_xfade_transition_base
@@ -9,16 +9,21 @@ from __future__ import annotations
import logging
# isort: off
from packages.domain.noise_reduction_config import (
NoiseReductionConfig,
NoiseReductionLevel,
NoiseReductionLevel, # noqa: F401
)
from packages.domain.noise_reduction_config import (
apply_noise_reduction_if_needed as _apply_noise_reduction_if_needed_base,
)
from packages.domain.noise_reduction_config import build_afftdn_filter as _build_afftdn_filter_base # noqa: F401 — 向后兼容
from packages.domain.noise_reduction_config import (
build_afftdn_filter as _build_afftdn_filter_base,
) # noqa: F401 — 向后兼容
from packages.domain.noise_reduction_config import build_arnndn_filter as _build_arnndn_filter_base
# isort: on
logger = logging.getLogger(__name__)
+1 -10
View File
@@ -18,22 +18,13 @@ from pathlib import Path
# 向后兼容:POSITION_BOTTOM_CENTER 也从 pip_config 再导出
from packages.domain.pip_config import POSITION_BOTTOM_CENTER # noqa: E402, F401
from packages.domain.pip_config import PiPConfig # noqa: F401
from packages.domain.pip_config import (
ANIMATION_FADE,
ANIMATION_SCALE,
ANIMATION_SLIDE_BOTTOM,
ANIMATION_SLIDE_LEFT,
ANIMATION_SLIDE_RIGHT,
ANIMATION_SLIDE_TOP,
POSITION_BOTTOM_LEFT,
POSITION_BOTTOM_RIGHT,
POSITION_CENTER,
POSITION_CENTER_LEFT,
POSITION_CENTER_RIGHT,
POSITION_TOP_CENTER,
POSITION_TOP_LEFT,
POSITION_TOP_RIGHT,
PiPConfig,
PiPLayerConfig,
)
from packages.domain.pip_config import ( # noqa: F401 — 向后兼容:保留模块级导出
@@ -13,9 +13,6 @@ from pathlib import Path
from typing import Any
from packages.domain.ass_subtitle_builder import (
TITLE_MARGIN_BOTTOM,
TITLE_MARGIN_SIDE,
TITLE_MARGIN_TOP,
build_ass_content,
)
from packages.domain.ass_subtitle_builder import build_ass_style as _build_ass_style_base # noqa: F401 — 向后兼容
@@ -14,16 +14,20 @@ import logging
from pathlib import Path
from typing import Any
# isort: off
from packages.domain.sticker_config import (
POSITION_PRESETS,
STICKER_CATEGORIES,
POSITION_PRESETS, # noqa: F401
STICKER_CATEGORIES, # noqa: F401
ImageStickerConfig,
StickerOverlayResult,
TextStickerConfig,
)
from packages.domain.sticker_config import get_sticker_categories as _get_sticker_categories_base # noqa: F401 向后兼容导出
from packages.domain.sticker_config import (
get_sticker_categories as _get_sticker_categories_base,
) # noqa: F401 向后兼容导出
from packages.domain.sticker_config import parse_stickers_from_config as _parse_stickers_base
from packages.domain.sticker_config import (
# isort: on
resolve_sticker_position,
)
@@ -25,28 +25,22 @@ from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from video_processing.path_security import PathSecurityError, is_in_allowed_dirs, safe_resolve_path
from packages.domain.subtitle_style import (
ALLOWED_SUBTITLE_EXTENSIONS,
DEFAULT_COLOR,
DEFAULT_FONT,
DEFAULT_FONT_SIZE,
DEFAULT_MAX_CHARS_PER_LINE,
DEFAULT_POSITION,
DEFAULT_STROKE_COLOR,
DEFAULT_STROKE_WIDTH,
POSITION_ALIASES,
POSITION_ALIGNMENT,
SubtitleSegment,
SubtitleStyle,
)
from packages.domain.subtitle_style import escape_ass_text as _escape_ass_text # noqa: F401 向后兼容导出
from packages.domain.subtitle_style import format_ass_time as _format_ass_time
from packages.domain.subtitle_style import hex_to_ass_bgr as _hex_to_ass_bgr
from packages.domain.subtitle_style import hex_to_ass_color as _hex_to_ass_color
from packages.domain.subtitle_style import opacity_to_ass_alpha as _opacity_to_ass_alpha
from packages.domain.subtitle_style import hex_to_ass_bgr as _hex_to_ass_bgr # noqa: F401
from packages.domain.subtitle_style import hex_to_ass_color as _hex_to_ass_color # noqa: F401
from packages.domain.subtitle_style import opacity_to_ass_alpha as _opacity_to_ass_alpha # noqa: F401
from packages.domain.subtitle_style import wrap_text as _wrap_text
logger = logging.getLogger(__name__)
+2 -4
View File
@@ -15,16 +15,14 @@ from __future__ import annotations
import logging
from typing import Any
from packages.domain.trim_config import MIN_TRIM_DURATION # noqa: F401
from packages.domain.trim_config import extract_trim_from_clip_config # noqa: F401
from packages.domain.trim_config import (
MIN_TRIM_DURATION,
TrimConfig,
TrimSegment,
)
from packages.domain.trim_config import build_audio_trim_filter as _build_audio_trim_filter # noqa: F401 — 向后兼容
from packages.domain.trim_config import build_video_trim_filter as _build_video_trim_filter
from packages.domain.trim_config import (
extract_trim_from_clip_config,
)
from packages.domain.trim_config import parse_segments_from_config as _parse_segments_from_config
from packages.domain.trim_config import resolve_segments as _resolve_segments
@@ -53,7 +53,6 @@ from video_processing.tts_engine import TtsEngine
from video_processing.watermark_engine import WatermarkConfig, WatermarkEngine
from packages.domain.render_layer_utils import LAYER_Z_INDEX as _IMPORTED_LAYER_Z_INDEX
from packages.domain.render_layer_utils import can_pass_through as _can_pass_through_pure
from packages.domain.render_layer_utils import clip_adjusted_duration as _clip_adjusted_duration_pure
from packages.domain.render_layer_utils import clip_effective_duration as _clip_effective_duration_pure
from packages.domain.render_layer_utils import clip_playback_speed as _clip_playback_speed_pure
@@ -14,10 +14,9 @@
from __future__ import annotations
import logging
from typing import Any
from packages.domain.watermark_config import WATERMARK_POSITIONS # noqa: F401
from packages.domain.watermark_config import (
WATERMARK_POSITIONS,
WatermarkConfig,
)
from packages.domain.watermark_config import ( # noqa: F401 — 向后兼容
+1 -1
View File
@@ -186,7 +186,7 @@ class PiPConfig:
"""最大 z_index."""
if not self.layers:
return 0
return max(l.z_index for l in self.layers)
return max(layer.z_index for layer in self.layers)
# ── 纯逻辑工具函数 ───────────────────────────────────────────────────────────
+1 -1
View File
@@ -6,7 +6,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
# ── 常量 ──────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -11,7 +11,7 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -10,7 +10,7 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -39,7 +39,7 @@ class TestConstants:
def test_preset_params_complete(self):
assert set(PRESET_PARAMS.keys()) == VALID_PRESETS
for preset, params in PRESET_PARAMS.items():
for _preset, params in PRESET_PARAMS.items():
assert set(params.keys()) == set(ALL_PARAM_KEYS)
def test_default_params_keys(self):
@@ -54,7 +54,7 @@ class TestConstants:
assert min_val <= DEFAULT_PARAMS[key] <= max_val
def test_all_presets_within_ranges(self):
for preset, params in PRESET_PARAMS.items():
for _preset, params in PRESET_PARAMS.items():
for key in ALL_PARAM_KEYS:
min_val, max_val = PARAM_RANGES[key]
assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]} out of range"
+1 -1
View File
@@ -208,7 +208,7 @@ class TestPiPConfigFromDict:
}
)
assert cfg.layer_count == 3
assert [l.source for l in cfg.layers] == ["bottom", "mid", "top"]
assert [layer.source for layer in cfg.layers] == ["bottom", "mid", "top"]
def test_invalid_layer_skipped(self):
cfg = PiPConfig.from_dict(
+1 -1
View File
@@ -23,7 +23,7 @@ class TestConstants:
assert len(POSITION_PRESETS) == 9
def test_position_presets_normalized(self):
for name, (x, y) in POSITION_PRESETS.items():
for _name, (x, y) in POSITION_PRESETS.items():
assert 0.0 <= x <= 1.0
assert 0.0 <= y <= 1.0