Files
ele-HeiXiu/src/services/modules/task.ts
YoungestSongMo 4237dcbaf6 feat: 数据获取架构重构 + Mac指纹采集 + Token刷新容错 + 账单页面 (0.0.19)
## 架构重构
  - useAsyncData 集成 AbortController:cleanup 时真正中断 HTTP 请求,
    不再仅丢弃响应(解决 StrictMode 双重请求问题)
  - API 模块统一 signal?: AbortSignal 参数,透传 axios config
  - use-api.ts 成为数据 Hook 集中管理中心,新增 useAccountInfo、
    useBillingBalance、useBillingLedger
  - 页面组件改为纯渲染:const {data,loading,error,refetch} = useXxx()

  ## 新功能
  - Mac 硬件指纹采集(electron/preload/device.ts):
    ioreg(UUID) + sysctl(CPU) + system_profiler(HDD三级回退)
  - 账单页面 BillingInfo(余额卡片 + 账单表格 + 分页)
  - PageDispatcher 注册 /billing 页面(FloatingPanel 960×680)
  - 共享工具函数 centToYuan / formatDate(src/utils/display.ts)

  ## Bug 修复
  - Token 刷新容错:网络错误时保留 refresh_token,等网络恢复后重试;
    仅 RefreshError(token 过期)时清除(auth-token.ts)
  - AppProvider 自动登录时 nullable 字段默认值(email→''、admin_permissions→[])
  - AccountInfo 重构消除 ~40 行手写状态管理代码

  ## 代码质量
  - AccountInfo 消除 centToYuan/formatDate 重复定义
  - BillingInfo 标题层级精简(移除冗余页面/Card标题)
  - antd Table align 字面量类型修正('left' as const)
  - billing.ts signal 参数位置修复(params→config)
  - modelFirstFetchDone 模块级变量移除(StrictMode 不安全)
2026-06-11 20:08:44 +08:00

237 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 任务模块 — 任务记录 CRUD API
// GET /api/v1/tasks → 任务列表(分页/游标)
// POST /api/v1/tasks → 提交任务
// GET /api/v1/tasks/:id → 任务详情
// GET /api/v1/tasks/:id/status → 轻量轮询状态
// GET /api/v1/tasks/export → 导出 CSV
//
// TaskAPI 的静态方法直接返回 get/post 的 Promise<T>
// 不做额外的 async/await 包装,由调用方决定是否 await。
// ============================================================
import {get, post} from '../request';
import {getDeviceId} from '@/utils/device.ts';
// ============================================================
// 基础枚举 / 字面量类型
// ============================================================
/** 输出类型映射 */
export enum OutputTypeMap {
image = '图片',
video = '视频',
audio = '音频',
}
export type OutputTypeString = keyof typeof OutputTypeMap;
/** 任务状态 */
export enum TaskStatusMap {
pending = '待处理',
submitted = '已提交',
processing = '处理中',
success = '成功',
failed = '失败',
}
export type TaskStatusString = keyof typeof TaskStatusMap;
// ============================================================
// 请求体 / 参数类型
// ============================================================
/** 任务列表请求参数 */
export interface TaskListRequestParams {
status?: TaskStatusString;
user_ids?: string[];
model_ids?: string[];
/** 游标分页:上次最后一条的 created_atISO 8601 */
cursor?: string;
/** 页码OFFSET 分页模式default: 1 */
page?: number;
/** 每页条数default: 20 */
page_size?: number;
}
/** 创建任务请求体 */
export interface CreatTaskRequestBody {
model_id: string;
params: Record<string, string>;
device_id: string;
tags?: string[];
}
/** 导出 CSV 参数 */
export interface ExportTaskCSVParams {
start_date: Date;
end_date: Date;
status?: TaskStatusString;
user_ids?: string[];
model_ids?: string[];
}
// ============================================================
// 内容项 — 带判别标签的联合类型
// ============================================================
interface BaseContentItem {
type: string;
}
interface TextContent extends BaseContentItem {
type: 'text';
text: string;
}
interface ImageUrlContent extends BaseContentItem {
type: 'image_url';
role: 'reference_image';
image_url: { url: string };
}
interface VideoUrlContent extends BaseContentItem {
type: 'video_url';
role: 'reference_video';
video_url: { url: string };
}
interface AudioContent extends BaseContentItem {
type: 'audio_url';
role: 'reference_audio';
audio_url: { url: string };
}
export type ContentItem =
| TextContent
| ImageUrlContent
| VideoUrlContent
| AudioContent;
// ============================================================
// 各输出类型的 UI 参数
// ============================================================
export interface ImageUiParams {
imageUrls?: string[];
prompt: string;
aspectRatio: string;
resolution?: string;
}
export interface VideoUiParams {
resolution?: string;
ratio?: string;
model?: string;
generate_audio: boolean;
duration: number;
content: ContentItem[];
}
export interface AudioUiParams {
format?: string;
sampleRate?: number;
bitrate?: number;
}
// ============================================================
// 响应体 — 用判别联合按 output_type 分发
// ============================================================
/** 任务条目基类(不含 output_type 特有字段) */
interface BaseTaskItemResponseBody {
id: string;
user_id: string;
model_id: string;
model_name: string;
provider_name: string;
status: TaskStatusString;
external_task_id: string;
cost_cent: number;
estimated_cost_cent: number;
error_code: string;
poll_attempts: number;
is_favorite: boolean;
submitted_at: Date;
finished_at: Date;
created_at: Date;
updated_at: Date;
prompt?: string;
output_assets?: Array<{ url: string }>;
tags: Array<string | null>;
error_message: string;
user_display_name: string | null;
}
/** 判别联合:根据 output_type 收窄 ui_params */
export type TaskInfoItemResponseBody =
| (BaseTaskItemResponseBody & { output_type: 'image'; ui_params: ImageUiParams })
| (BaseTaskItemResponseBody & { output_type: 'video'; ui_params: VideoUiParams })
| (BaseTaskItemResponseBody & { output_type: 'audio'; ui_params: AudioUiParams });
/** 任务列表分页响应 */
export interface TaskInfoDataResponseBody {
has_more: boolean;
next_cursor: string;
page: number;
page_size: number;
total: number;
items: TaskInfoItemResponseBody[];
}
/** 轻量轮询状态响应 */
export interface TaskStatusResponseBody {
task_id: string;
status: TaskStatusString;
progress: number;
output_assets?: string[];
error_code?: string;
error_message?: string;
cost_cent?: number;
}
// ============================================================
// 路由常量
// ============================================================
const TaskUrlObj = {
task: '/api/v1/tasks',
taskInfo: (task_id: string) => `/api/v1/tasks/${task_id}`,
taskStatus: (task_id: string) => `/api/v1/tasks/${task_id}/status`,
tasksCSV: '/api/v1/tasks/export',
} as const;
// ============================================================
// TaskAPI — 静态方法直接返回 Promise<T>,不做二次 async 包装
// ============================================================
export class TaskAPI {
/** 获取任务列表(分页/游标) */
static getTaskList(params: TaskListRequestParams, signal?: AbortSignal): Promise<TaskInfoDataResponseBody> {
return get<TaskInfoDataResponseBody>(TaskUrlObj.task, params as Record<string, unknown>, { signal });
}
/** 提交新任务device_id 由 API 层自动注入 getDeviceId(),调用方无需传入) */
static submitTask(body: Omit<CreatTaskRequestBody, 'device_id'>): Promise<TaskInfoItemResponseBody> {
return post<TaskInfoItemResponseBody>(TaskUrlObj.task, {
...body,
device_id: getDeviceId(),
});
}
/** 获取任务详情 */
static getTaskInfo(task_id: string, signal?: AbortSignal): Promise<TaskInfoItemResponseBody> {
return get<TaskInfoItemResponseBody>(TaskUrlObj.taskInfo(task_id), undefined, { signal });
}
/** 轻量轮询任务状态 */
static getTaskStatus(task_id: string): Promise<TaskStatusResponseBody> {
return get<TaskStatusResponseBody>(TaskUrlObj.taskStatus(task_id));
}
/** 导出任务 CSV */
static exportTaskCSV(params: ExportTaskCSVParams): Promise<File> {
return get<File>(TaskUrlObj.tasksCSV, params as unknown as Record<string, unknown>);
}
}