From a816bfa8993d4a5b278cca923ffc4f6074d250cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 17:08:58 +0800 Subject: [PATCH] =?UTF-8?q?task-104(=E4=BB=BB=E5=8A=A1=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=86=E6=9E=90=E4=B8=AD=E5=BF=83):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E8=A7=86=E9=A2=91=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image-video-model.ts 增加 ImageVideoDetail 与 parseImageVideoDetail(脱敏 request/ submit_response/result + error_message/archived_at),image-video-api.ts 增加 fetchImageVideoTaskDetail(GET /api/admin/image-video-tasks/{id})。 TDD: task-104.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/tasks/image-video-api.ts | 10 ++- .../src/pages/tasks/image-video-model.ts | 36 ++++++++ admin-frontend-vue/tests/task-104.test.ts | 84 +++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 admin-frontend-vue/tests/task-104.test.ts diff --git a/admin-frontend-vue/src/pages/tasks/image-video-api.ts b/admin-frontend-vue/src/pages/tasks/image-video-api.ts index 331c5793..42503903 100644 --- a/admin-frontend-vue/src/pages/tasks/image-video-api.ts +++ b/admin-frontend-vue/src/pages/tasks/image-video-api.ts @@ -1,6 +1,6 @@ -/** 视频任务列表加载适配(任务 103):GET /api/admin/image-video-tasks + 筛选归一与解析。 */ +/** 视频任务列表/详情适配(任务 103/104):GET /api/admin/image-video-tasks(/detail)。 */ import { http } from '@/api/http' -import { parseImageVideoPage, type ImageVideoPageResult } from './image-video-model.ts' +import { parseImageVideoDetail, parseImageVideoPage, type ImageVideoDetail, type ImageVideoPageResult } from './image-video-model.ts' import { toImageVideoQuery, type ImageVideoFilter } from './image-video-filter.ts' export const IMAGE_VIDEO_TASKS_ENDPOINT = '/api/admin/image-video-tasks' @@ -15,3 +15,9 @@ export async function fetchImageVideoTasks( }) return parseImageVideoPage(data) } + +/** 查询单任务详情(含脱敏 request/结果 JSON):GET /api/admin/image-video-tasks/{taskId}。 */ +export async function fetchImageVideoTaskDetail(taskId: string | number): Promise { + const { data } = await http.get(`${IMAGE_VIDEO_TASKS_ENDPOINT}/${taskId}`) + return parseImageVideoDetail(data) +} diff --git a/admin-frontend-vue/src/pages/tasks/image-video-model.ts b/admin-frontend-vue/src/pages/tasks/image-video-model.ts index 23aef0ab..2f755166 100644 --- a/admin-frontend-vue/src/pages/tasks/image-video-model.ts +++ b/admin-frontend-vue/src/pages/tasks/image-video-model.ts @@ -139,3 +139,39 @@ export function parseImageVideoPage(payload: unknown): ImageVideoPageResult { if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize) return out } + +/** 单任务详情(含脱敏 request/结果 JSON),见任务 104。 */ +export interface ImageVideoDetail extends ImageVideoRow { + errorMessage: string + archivedAt?: string + request: Record + submitResponse: Record + result: Record +} + +function objectOrEmpty(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} +} + +/** 解析单任务详情负载(data.item + includeJson 字段);缺 item/无效任务抛可操作错误。 */ +export function parseImageVideoDetail(payload: unknown): ImageVideoDetail { + const core = unwrap(payload) + const record = core && typeof core === 'object' ? (core as Record) : {} + const itemRaw = record.item + if (!itemRaw || typeof itemRaw !== 'object') { + throw new Error('视频任务详情响应异常:缺少 item') + } + const base = toImageVideoRow(itemRaw) + if (!base) { + throw new Error('视频任务详情响应异常:任务无效') + } + const detail = base as ImageVideoDetail + const r = itemRaw as Record + detail.errorMessage = text(r.error_message ?? r.errorMessage) + const archivedAt = text(r.archived_at ?? r.archivedAt) + if (archivedAt) detail.archivedAt = archivedAt + detail.request = objectOrEmpty(r.request) + detail.submitResponse = objectOrEmpty(r.submit_response ?? r.submitResponse) + detail.result = objectOrEmpty(r.result) + return detail +} diff --git a/admin-frontend-vue/tests/task-104.test.ts b/admin-frontend-vue/tests/task-104.test.ts new file mode 100644 index 00000000..5465f82d --- /dev/null +++ b/admin-frontend-vue/tests/task-104.test.ts @@ -0,0 +1,84 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseImageVideoDetail } from '../src/pages/tasks/image-video-model.ts' + +test('test_task_104_video_detail_normal_primary_path', () => { + // 正常主路径:详情负载(data.item + request/result JSON)解析为类型化详情。 + const detail = parseImageVideoDetail({ + success: true, + data: { + item: { + task_id: 21, + username: '张伟', + status: 'FAILED', + error_message: '上游生成失败', + archived_at: '2026-01-02 03:00:00', + request: { prompt: '下雨场景', api_key: '**masked**' }, + submit_response: { run_id: 'r-1' }, + result: { video_url: 'https://oss/x.mp4' }, + }, + }, + }) + assert.equal(detail.taskId, '21') + assert.equal(detail.status, 'FAILED') + assert.equal(detail.errorMessage, '上游生成失败') + assert.equal(detail.archivedAt, '2026-01-02 03:00:00') + assert.deepEqual(detail.request, { prompt: '下雨场景', api_key: '**masked**' }) + assert.deepEqual(detail.submitResponse, { run_id: 'r-1' }) + assert.deepEqual(detail.result, { video_url: 'https://oss/x.mp4' }) +}) + +test('test_task_104_video_detail_normal_variant_input', () => { + // 正常变体:已解包 data.item 直传;缺 includeJson 字段回空对象。 + const detail = parseImageVideoDetail({ item: { task_id: 8, status: 'SUCCESS' } }) + assert.equal(detail.taskId, '8') + assert.equal(detail.errorMessage, '') + assert.deepEqual(detail.request, {}) + assert.deepEqual(detail.result, {}) +}) + +test('test_task_104_video_detail_repeated_is_idempotent', () => { + // 正常重复:解析稳定、不改输入。 + const payload = { item: { task_id: 3, status: 'SUCCESS', request: { a: 1 } } } + assert.deepEqual(parseImageVideoDetail(payload), parseImageVideoDetail(payload)) + assert.equal((payload.item as { request: Record }).request.a, 1) +}) + +test('test_task_104_video_detail_boundary_empty_input', () => { + // 边界空值:无 item 详情抛可操作错误。 + assert.throws(() => parseImageVideoDetail({ data: {} }), /详情/) +}) + +test('test_task_104_video_detail_boundary_single_item', () => { + // 边界单元素:单任务详情含单视频对象。 + const detail = parseImageVideoDetail({ + item: { task_id: 5, status: 'SUCCESS', videos: [{ display_url: 'https://oss/5.mp4' }] }, + }) + assert.equal(detail.videos.length, 1) + assert.equal(detail.videos[0].displayUrl, 'https://oss/5.mp4') +}) + +test('test_task_104_video_detail_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:request/result 为非对象字符串时按空对象处理。 + const detail = parseImageVideoDetail({ item: { task_id: 2, status: 'RUNNING', request: 'raw-json' } }) + assert.equal(detail.taskId, '2') + assert.deepEqual(detail.request, {}) +}) + +test('test_task_104_video_detail_invalid_input_rejected', () => { + // 异常输入:success=false 抛后端 message;缺 task_id 视为无效详情。 + assert.throws(() => parseImageVideoDetail({ success: false, message: '视频任务不存在或无权访问' }), /无权访问/) + assert.throws(() => parseImageVideoDetail({ item: { status: 'SUCCESS' } }), /详情/) +}) + +test('test_task_104_video_detail_dependency_failure_returns_actionable_message', () => { + // 依赖失败/加载走 adapter:GET /api/admin/image-video-tasks/{id} + http.get + 解析。 + const api = readSource('src/pages/tasks/image-video-api.ts') + assert.match(api, /\/api\/admin\/image-video-tasks/) + assert.match(api, /http\.get/) + assert.match(api, /fetchImageVideoTaskDetail/) + assert.match(api, /parseImageVideoDetail/) + const model = readSource('src/pages/tasks/image-video-model.ts') + assert.equal(/axios|http\./.test(model), false, '详情解析保持纯逻辑') +})