feat: async task pipeline for media and llm workflows
这个提交包含在:
@@ -59,6 +59,7 @@ type MockAppState = {
|
||||
user: MockUser;
|
||||
videos: any[];
|
||||
analyses: any[];
|
||||
tasks: any[];
|
||||
activePlan: {
|
||||
id: number;
|
||||
title: string;
|
||||
@@ -79,6 +80,7 @@ type MockAppState = {
|
||||
} | null;
|
||||
mediaSession: MockMediaSession | null;
|
||||
nextVideoId: number;
|
||||
nextTaskId: number;
|
||||
authMeNullResponsesAfterLogin: number;
|
||||
};
|
||||
|
||||
@@ -159,6 +161,32 @@ function buildMediaSession(user: MockUser, title: string): MockMediaSession {
|
||||
};
|
||||
}
|
||||
|
||||
function createTask(state: MockAppState, input: {
|
||||
type: string;
|
||||
title: string;
|
||||
status?: string;
|
||||
progress?: number;
|
||||
message?: string;
|
||||
result?: any;
|
||||
error?: string | null;
|
||||
}) {
|
||||
const task = {
|
||||
id: `task-${state.nextTaskId++}`,
|
||||
userId: state.user.id,
|
||||
type: input.type,
|
||||
status: input.status ?? "succeeded",
|
||||
title: input.title,
|
||||
message: input.message ?? "任务执行完成",
|
||||
progress: input.progress ?? 100,
|
||||
result: input.result ?? null,
|
||||
error: input.error ?? null,
|
||||
createdAt: nowIso(),
|
||||
updatedAt: nowIso(),
|
||||
};
|
||||
state.tasks = [task, ...state.tasks];
|
||||
return task;
|
||||
}
|
||||
|
||||
async function fulfillJson(route: Route, body: unknown) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@@ -218,11 +246,112 @@ async function handleTrpc(route: Route, state: MockAppState) {
|
||||
},
|
||||
],
|
||||
};
|
||||
return trpcResult({ planId: state.activePlan.id, plan: state.activePlan });
|
||||
return trpcResult({
|
||||
taskId: createTask(state, {
|
||||
type: "training_plan_generate",
|
||||
title: "7天训练计划生成",
|
||||
result: {
|
||||
kind: "training_plan_generate",
|
||||
planId: state.activePlan.id,
|
||||
plan: state.activePlan,
|
||||
},
|
||||
}).id,
|
||||
});
|
||||
case "plan.adjust":
|
||||
return trpcResult({
|
||||
taskId: createTask(state, {
|
||||
type: "training_plan_adjust",
|
||||
title: "训练计划调整",
|
||||
result: {
|
||||
kind: "training_plan_adjust",
|
||||
adjustmentNotes: "已根据最近分析结果调整训练重点。",
|
||||
},
|
||||
}).id,
|
||||
});
|
||||
case "video.list":
|
||||
return trpcResult(state.videos);
|
||||
case "analysis.list":
|
||||
return trpcResult(state.analyses);
|
||||
case "task.list":
|
||||
return trpcResult(state.tasks);
|
||||
case "task.get": {
|
||||
const rawInput = url.searchParams.get("input");
|
||||
const parsedInput = rawInput ? JSON.parse(rawInput) : {};
|
||||
const taskId = parsedInput.json?.taskId || parsedInput[0]?.json?.taskId;
|
||||
return trpcResult(state.tasks.find((task) => task.id === taskId) || null);
|
||||
}
|
||||
case "task.retry": {
|
||||
const rawInput = url.searchParams.get("input");
|
||||
const parsedInput = rawInput ? JSON.parse(rawInput) : {};
|
||||
const taskId = parsedInput.json?.taskId || parsedInput[0]?.json?.taskId;
|
||||
const task = state.tasks.find((item) => item.id === taskId);
|
||||
if (task) {
|
||||
task.status = "succeeded";
|
||||
task.progress = 100;
|
||||
task.error = null;
|
||||
task.message = "任务执行完成";
|
||||
}
|
||||
return trpcResult({ task });
|
||||
}
|
||||
case "task.createMediaFinalize": {
|
||||
if (state.mediaSession) {
|
||||
state.mediaSession.status = "archived";
|
||||
state.mediaSession.archiveStatus = "completed";
|
||||
state.mediaSession.playback = {
|
||||
ready: true,
|
||||
webmUrl: "/media/assets/sessions/session-e2e/recording.webm",
|
||||
mp4Url: "/media/assets/sessions/session-e2e/recording.mp4",
|
||||
webmSize: 2_400_000,
|
||||
mp4Size: 1_800_000,
|
||||
previewUrl: "/media/assets/sessions/session-e2e/recording.webm",
|
||||
};
|
||||
state.videos = [
|
||||
{
|
||||
id: state.nextVideoId++,
|
||||
title: state.mediaSession.title,
|
||||
url: state.mediaSession.playback.webmUrl,
|
||||
format: "webm",
|
||||
fileSize: state.mediaSession.playback.webmSize,
|
||||
exerciseType: "recording",
|
||||
analysisStatus: "completed",
|
||||
createdAt: nowIso(),
|
||||
},
|
||||
...state.videos,
|
||||
];
|
||||
}
|
||||
return trpcResult({
|
||||
taskId: createTask(state, {
|
||||
type: "media_finalize",
|
||||
title: "录制归档",
|
||||
result: {
|
||||
kind: "media_finalize",
|
||||
sessionId: state.mediaSession?.id,
|
||||
videoId: state.videos[0]?.id,
|
||||
url: state.videos[0]?.url,
|
||||
},
|
||||
}).id,
|
||||
});
|
||||
}
|
||||
case "analysis.getCorrections":
|
||||
return trpcResult({
|
||||
taskId: createTask(state, {
|
||||
type: "pose_correction_multimodal",
|
||||
title: "动作纠正",
|
||||
result: {
|
||||
corrections: "## 动作概览\n整体节奏稳定,建议继续优化击球点前置。",
|
||||
report: {
|
||||
priorityFixes: [
|
||||
{
|
||||
title: "击球点前置",
|
||||
why: "击球点略靠后会影响挥拍连贯性。",
|
||||
howToPractice: "每组 8 次影子挥拍,刻意在身体前侧完成触球动作。",
|
||||
successMetric: "连续 3 组都能稳定在身体前侧完成挥拍。",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).id,
|
||||
});
|
||||
case "video.registerExternal":
|
||||
if (state.mediaSession?.playback.webmUrl || state.mediaSession?.playback.mp4Url) {
|
||||
state.videos = [
|
||||
@@ -366,9 +495,11 @@ export async function installAppMocks(
|
||||
createdAt: nowIso(),
|
||||
},
|
||||
],
|
||||
tasks: [],
|
||||
activePlan: null,
|
||||
mediaSession: null,
|
||||
nextVideoId: 100,
|
||||
nextTaskId: 1,
|
||||
authMeNullResponsesAfterLogin: options?.authMeNullResponsesAfterLogin ?? 0,
|
||||
};
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户