Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions frontend/src/entities/workflow-run/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@
`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` 标记并继续留在图中,生成输入、任务引用和审核历史不会被擦除;角色母版及其他并行动作不受影响。

## 前后端边界

前端负责节点结构、依赖边、推进规则和状态变化;后端只把 `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 递增的更新序号,不宣称具备后端尚未实现的并发冲突检测

## 文件

Expand Down
38 changes: 37 additions & 1 deletion frontend/src/entities/workflow-run/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const nodes: WorkflowNode[] = [
dependsOnNodeIds: [],
generations: [],
error: null,
input: { prompt: '一个像素骑士', referenceMedia: [] },
input: { characterId: 'character-7', prompt: '一个像素骑士', referenceMedia: [] },
},
{
id: 'template-node',
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/entities/workflow-run/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function hasOnlyGenerationRole(value: Record<string, unknown>, 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')
Expand Down Expand Up @@ -267,7 +268,7 @@ function getApiClient() {
return createApiClient({ getAccessToken: getApiAccessToken })
}

/** 精确对应后端已公开的 CRUD;不声明尚未提供的列表或按 Character 查询。 */
/** 精确对应后端已公开的 CRUD 与项目内分页列表;不声明尚未提供的按 Character 查询。 */
export const workflowRunApis: WorkflowRunApis = {
async create(input) {
return mapWorkflowRun(
Expand All @@ -277,6 +278,16 @@ export const workflowRunApis: WorkflowRunApis = {
}),
)
},
async listByProject(projectId, query = {}) {
const result = await getApiClient().requestList<WorkflowRunDto>('/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<WorkflowRunDto>(`/workflow-runs/${encodeURIComponent(id)}`),
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/entities/workflow-run/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -44,6 +45,11 @@ interface WorkflowNodeBase {
}

export interface WorkflowCharacterInput {
/**
* 当前节点图所属的 Character。后端只原样持久化 nodes,因此前端用它在项目列表中定位角色的唯一 Run。
* 旧 Run 可能没有该字段;读取方必须兼容未绑定状态。
*/
characterId?: string | null
prompt: string
referenceMedia: readonly MediaReference[]
}
Expand Down Expand Up @@ -113,7 +119,7 @@ export type WorkflowNode =
export interface WorkflowRun {
id: string
projectId: string
/** 后端乐观版本号,每次 PATCH 后使用响应中的新值。 */
/** 后端更新序号;当前仅随 PATCH 递增,不承担并发冲突检测。 */
version: number
/** 后端资源状态,仅表示正常或软删除。 */
storageStatus: WorkflowRunStorageStatus
Expand All @@ -128,6 +134,8 @@ export interface CreateWorkflowRunInput {

export interface WorkflowRunApis {
create(input: CreateWorkflowRunInput): Promise<WorkflowRun>
/** 后端只返回未软删除的运行记录。 */
listByProject(projectId: string, query?: PageQuery): Promise<Paged<WorkflowRun>>
get(id: WorkflowRun['id']): Promise<WorkflowRun>
update(run: WorkflowRun): Promise<WorkflowRun>
remove(id: WorkflowRun['id']): Promise<void>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/features/workflow-controller/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down