From 367a8567fbd70147c967737a7e19c7c1e1e715d3 Mon Sep 17 00:00:00 2001 From: xyh202131 <246811510+xyh202131@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:59:27 +0800 Subject: [PATCH 1/2] feat(workflow-run): connect project run listing --- frontend/src/entities/workflow-run/README.md | 7 ++-- .../src/entities/workflow-run/api.test.ts | 36 +++++++++++++++++++ frontend/src/entities/workflow-run/api.ts | 12 ++++++- frontend/src/entities/workflow-run/index.ts | 5 ++- .../workflow-controller/controller.test.ts | 6 ++++ 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/frontend/src/entities/workflow-run/README.md b/frontend/src/entities/workflow-run/README.md index b0a05bc0..36ec65f8 100644 --- a/frontend/src/entities/workflow-run/README.md +++ b/frontend/src/entities/workflow-run/README.md @@ -19,10 +19,11 @@ ## 前后端边界 前端负责节点结构、依赖边、推进规则和状态变化;后端只把 `WorkflowRun.nodes` JSON 原样保存。 -HTTP 接口严格对应 `POST /workflow-runs`、`GET/PATCH/DELETE /workflow-runs/{id}`。 +HTTP 接口严格对应 `POST /workflow-runs`、`GET /workflow-runs?project_id=...` 与 +`GET/PATCH/DELETE /workflow-runs/{id}`。项目内列表使用后端统一的分页响应,并只返回未软删除记录。 -当前后端没有列表、按 Character 查询或订阅接口,因此前端也不虚构这些方法。所有持久化调用 -都是异步的。后端 CRUD service 尚未实现时,本模块只提供真实接口适配器,不宣称已经联通。 +当前后端没有按 Character 查询或订阅接口,因此前端也不虚构这些方法。所有持久化调用都是异步的。 +`version` 当前只是后端随 PATCH 递增的更新序号,不宣称具备后端尚未实现的并发冲突检测。 ## 文件 diff --git a/frontend/src/entities/workflow-run/api.test.ts b/frontend/src/entities/workflow-run/api.test.ts index 6be5c9fe..ea56c983 100644 --- a/frontend/src/entities/workflow-run/api.test.ts +++ b/frontend/src/entities/workflow-run/api.test.ts @@ -90,6 +90,42 @@ function jsonResponse(data: unknown) { } describe('workflowRunApis', () => { + it('lists active workflow runs by project using backend pagination', async () => { + let requestUrl = '' + const apis = await loadWorkflowRunApis(async (input) => { + requestUrl = String(input) + return new Response( + JSON.stringify({ + code: 200, + message: 'success', + data: [workflowRunDto], + total: 1, + page: 2, + page_size: 10, + }), + { headers: { 'content-type': 'application/json' } }, + ) + }) + + await expect(apis.listByProject('42', { page: 2, pageSize: 10 })).resolves.toEqual({ + items: [ + { + id: '17', + projectId: '42', + version: 3, + storageStatus: 'active', + nodes, + }, + ], + total: 1, + page: 2, + pageSize: 10, + }) + expect(requestUrl).toBe( + 'https://api.windup.test/workflow-runs?project_id=42&page=2&page_size=10', + ) + }) + it('hydrates the six visible workflow nodes with explicit dependency edges', async () => { const sixNodeDto = { ...workflowRunDto, diff --git a/frontend/src/entities/workflow-run/api.ts b/frontend/src/entities/workflow-run/api.ts index 944d6dc9..a6e2e228 100644 --- a/frontend/src/entities/workflow-run/api.ts +++ b/frontend/src/entities/workflow-run/api.ts @@ -267,7 +267,7 @@ function getApiClient() { return createApiClient({ getAccessToken: getApiAccessToken }) } -/** 精确对应后端已公开的 CRUD;不声明尚未提供的列表或按 Character 查询。 */ +/** 精确对应后端已公开的 CRUD 与项目内分页列表;不声明尚未提供的按 Character 查询。 */ export const workflowRunApis: WorkflowRunApis = { async create(input) { return mapWorkflowRun( @@ -277,6 +277,16 @@ export const workflowRunApis: WorkflowRunApis = { }), ) }, + async listByProject(projectId, query = {}) { + const result = await getApiClient().requestList('/workflow-runs', { + query: { + project_id: toBackendId(projectId, 'projectId'), + page: query.page, + page_size: query.pageSize, + }, + }) + return { ...result, items: result.items.map(mapWorkflowRun) } + }, async get(id) { return mapWorkflowRun( await getApiClient().request(`/workflow-runs/${encodeURIComponent(id)}`), diff --git a/frontend/src/entities/workflow-run/index.ts b/frontend/src/entities/workflow-run/index.ts index 68f72ba7..4c70805a 100644 --- a/frontend/src/entities/workflow-run/index.ts +++ b/frontend/src/entities/workflow-run/index.ts @@ -1,6 +1,7 @@ import type { ActionType } from '../character' import type { Generation } from '../generation' import type { MediaReference } from '../media' +import type { Paged, PageQuery } from '@/shared/pagination' import { WORKFLOW_GENERATION_ROLES, WORKFLOW_NODE_PHASES, @@ -113,7 +114,7 @@ export type WorkflowNode = export interface WorkflowRun { id: string projectId: string - /** 后端乐观版本号,每次 PATCH 后使用响应中的新值。 */ + /** 后端更新序号;当前仅随 PATCH 递增,不承担并发冲突检测。 */ version: number /** 后端资源状态,仅表示正常或软删除。 */ storageStatus: WorkflowRunStorageStatus @@ -128,6 +129,8 @@ export interface CreateWorkflowRunInput { export interface WorkflowRunApis { create(input: CreateWorkflowRunInput): Promise + /** 后端只返回未软删除的运行记录。 */ + listByProject(projectId: string, query?: PageQuery): Promise> get(id: WorkflowRun['id']): Promise update(run: WorkflowRun): Promise remove(id: WorkflowRun['id']): Promise diff --git a/frontend/src/features/workflow-controller/controller.test.ts b/frontend/src/features/workflow-controller/controller.test.ts index 713d76c2..a60e6461 100644 --- a/frontend/src/features/workflow-controller/controller.test.ts +++ b/frontend/src/features/workflow-controller/controller.test.ts @@ -163,6 +163,12 @@ function createWorkflowApis(initial: WorkflowRun = createRun()) { } return structuredClone(saved) }), + listByProject: vi.fn(async () => ({ + items: [structuredClone(saved)], + total: 1, + page: 1, + pageSize: 20, + })), get: vi.fn(async () => structuredClone(saved)), update: vi.fn(async (run) => { saved = { ...structuredClone(run), version: saved.version + 1 } From 7f7c5efbc388486a5553c385474aa271fbb6979f Mon Sep 17 00:00:00 2001 From: xyh202131 <246811510+xyh202131@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:21:46 +0800 Subject: [PATCH 2/2] fix(workflow-run): persist character run binding --- frontend/src/entities/workflow-run/README.md | 1 + frontend/src/entities/workflow-run/api.test.ts | 2 +- frontend/src/entities/workflow-run/api.ts | 1 + frontend/src/entities/workflow-run/index.ts | 5 +++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/entities/workflow-run/README.md b/frontend/src/entities/workflow-run/README.md index 36ec65f8..553be5c7 100644 --- a/frontend/src/entities/workflow-run/README.md +++ b/frontend/src/entities/workflow-run/README.md @@ -13,6 +13,7 @@ `character-template`,角色母版通过后即可并行,不互相阻塞。 - 资产生成方式当前可选 `video-cropping` 与 `3d-to-2d`。3D 转 2D 后端接口未提供前只保存选择并明确阻止提交,不伪装成视频路线。 - Quick Start 与 Workflow Editor 是两种独立界面,但推进同一张节点图,核心数据不区分 `ai/manual driver`。 +- 角色设定节点在 `input.characterId` 中保存所属角色。前端按项目列出 Run 后用该字段定位角色的唯一 Run;旧数据允许暂未绑定。 - 后端不提供 Revision 历史。重做时覆盖旧结果,并用 `nodeId + taskId` 防止旧请求串线。 - 已发布 Action 被删除后,对应四节点分支以 `deletedAt` 标记并继续留在图中,生成输入、任务引用和审核历史不会被擦除;角色母版及其他并行动作不受影响。 diff --git a/frontend/src/entities/workflow-run/api.test.ts b/frontend/src/entities/workflow-run/api.test.ts index ea56c983..b7011593 100644 --- a/frontend/src/entities/workflow-run/api.test.ts +++ b/frontend/src/entities/workflow-run/api.test.ts @@ -10,7 +10,7 @@ const nodes: WorkflowNode[] = [ dependsOnNodeIds: [], generations: [], error: null, - input: { prompt: '一个像素骑士', referenceMedia: [] }, + input: { characterId: 'character-7', prompt: '一个像素骑士', referenceMedia: [] }, }, { id: 'template-node', diff --git a/frontend/src/entities/workflow-run/api.ts b/frontend/src/entities/workflow-run/api.ts index a6e2e228..2d43ca90 100644 --- a/frontend/src/entities/workflow-run/api.ts +++ b/frontend/src/entities/workflow-run/api.ts @@ -88,6 +88,7 @@ function hasOnlyGenerationRole(value: Record, role: string | nu function hasValidCharacterInput(value: unknown): boolean { if (!isRecord(value)) return false return ( + (value.characterId === undefined || isNullableString(value.characterId)) && typeof value.prompt === 'string' && Array.isArray(value.referenceMedia) && value.referenceMedia.every((item) => typeof item === 'string') diff --git a/frontend/src/entities/workflow-run/index.ts b/frontend/src/entities/workflow-run/index.ts index 4c70805a..01b246f7 100644 --- a/frontend/src/entities/workflow-run/index.ts +++ b/frontend/src/entities/workflow-run/index.ts @@ -45,6 +45,11 @@ interface WorkflowNodeBase { } export interface WorkflowCharacterInput { + /** + * 当前节点图所属的 Character。后端只原样持久化 nodes,因此前端用它在项目列表中定位角色的唯一 Run。 + * 旧 Run 可能没有该字段;读取方必须兼容未绑定状态。 + */ + characterId?: string | null prompt: string referenceMedia: readonly MediaReference[] }