Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0e7e107
feat(workflow-run): add versioned history store
xyh202131 Aug 3, 2026
29001a1
chore(workflow-run): narrow pull request scope
xyh202131 Aug 3, 2026
8453962
fix(workflow-run): validate add-action targets
xyh202131 Aug 3, 2026
1b1cd82
feat(workflow-run): implement generation orchestration
xyh202131 Aug 3, 2026
c49e696
feat(workflow-run): expose action review frames
xyh202131 Aug 4, 2026
e4a1f01
fix(workflow-run): support interrupted task recovery
xyh202131 Aug 4, 2026
8762df0
feat(workflow-controller): add workflow coordination boundary
xyh202131 Aug 4, 2026
030f668
feat(workflow-controller): return action review frames
xyh202131 Aug 4, 2026
5ec5ba3
fix(workflow-controller): make task recovery idempotent
xyh202131 Aug 4, 2026
600120f
fix(workflow-run): close async state races
xyh202131 Aug 4, 2026
2f7c04d
fix(workflow-run): close async state races
xyh202131 Aug 4, 2026
f1f7f10
refactor(workflow-run): align run model with editor cards
xyh202131 Aug 5, 2026
8a8c943
Merge remote-tracking branch 'upstream/main' into feat/workflow-run-c…
xyh202131 Aug 5, 2026
014e83a
feat(workflow-run): restore editor revisions
xyh202131 Aug 5, 2026
249de20
docs(workflow-run): define backend agent boundary
xyh202131 Aug 6, 2026
c2e231d
Merge remote-tracking branch 'upstream/main' into feat/workflow-run-c…
xyh202131 Aug 6, 2026
e8ee4aa
fix(workflow-run): align character asset contracts
xyh202131 Aug 6, 2026
dbcea48
docs(workflow-run): keep generation implementation neutral
xyh202131 Aug 6, 2026
450a895
Merge branch 'feat/workflow-run-core-clean' into feat/workflow-contro…
xyh202131 Aug 6, 2026
5be316d
docs(workflow-run): keep execution strategy undecided
xyh202131 Aug 6, 2026
6955c4e
docs(workflow-run): keep execution strategy undecided
xyh202131 Aug 6, 2026
d47578e
fix(workflow-controller): reuse bound workflow runs
xyh202131 Aug 6, 2026
0cd2744
Merge remote-tracking branch 'upstream/main' into feat/workflow-contr…
xyh202131 Aug 6, 2026
989947d
feat(workflow-run): group character actions in one run
xyh202131 Aug 6, 2026
71559f6
Merge branch 'feat/workflow-run-core-clean' into feat/workflow-contro…
xyh202131 Aug 6, 2026
4e14085
feat(workflow-controller): append actions to character run
xyh202131 Aug 6, 2026
689c9a3
feat(workflow-run): sync versioned persistence for controller
xyh202131 Aug 6, 2026
80ebae3
feat(workflow-controller): centralize phase progression
xyh202131 Aug 6, 2026
db31dd3
feat(workflow-controller): align character run commands
xyh202131 Aug 6, 2026
a2da43d
refactor(frontend): make WorkflowRun the aggregate root
xyh202131 Aug 6, 2026
09e0241
refactor(frontend): align controller with current workflow run
xyh202131 Aug 6, 2026
f51530d
Merge remote-tracking branch 'upstream/main' into feat/workflow-contr…
xyh202131 Aug 6, 2026
134bf92
style(frontend): format action generation task
xyh202131 Aug 6, 2026
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
129 changes: 96 additions & 33 deletions frontend/src/entities/generation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,17 @@ import type { MediaReference } from '../media'
/**
* Generation 是业务数据,不是「调用图片生成能力」。
* 前端只创建 generation 并订阅它的状态;真正调用模型的是后端,前端不接触那一层。
*
* 后端只有 GenerationTask 一个实体,generation 与 task 指同一条记录;
* `/generation/tasks/{task_id}` 里的 tasks 只是路径段,前端不为它另立实体。
*/

/**
* 后端 GenerationTask.status,与 WorkflowRevision.generationStatus 不是一回事:
* 这里是单次生成任务的状态,那里是一个版本在生成阶段的汇总状态。
* pending 表示已提交但尚未执行。
*/
export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed'

/**
* 生成对应的三个前端可见异步步骤。
* 它是前端工作流粒度,不等于后端 task_type——后端只有 character_image 与
* character_action 两种,character_template 和 first_frame 都落在 character_image 上。
* 完整动画内部可含视频生成、截帧和多次图像处理,但对前端仍是一次 Generation。
*/
/** 生成对应的三个前端可见异步步骤。 */
export type GenerationType = 'character_template' | 'first_frame' | 'complete_animation'

/** 完整动作默认生成帧数;首帧生成仍固定为 1 帧。 */
export const COMPLETE_ANIMATION_FRAME_COUNT = 32

/** 后端单次生成任务的生命周期。 */
export type GenerationTaskStatus = 'pending' | 'running' | 'completed' | 'failed'

interface GenerationInputBase {
projectId: string
/** 可选参考媒体;没有参考图时传空数组。 */
Expand All @@ -35,6 +26,10 @@ export interface CharacterTemplateGenerationInput extends GenerationInputBase {
type: 'character_template'
/** 已由手动输入或 Quick Start 整理好的角色提示词。 */
prompt: string
/** 项目约束的精灵图宽度,提交生成时传给后端做尺寸校验。 */
spriteWidth: number
/** 项目约束的精灵图高度,提交生成时传给后端做尺寸校验。 */
spriteHeight: number
}

/** 指定角色造型下的动作首帧生成;不能只绑定 Character。 */
Expand Down Expand Up @@ -74,15 +69,84 @@ export interface CharacterTemplateGenerationResult {
images: readonly GeneratedImage[]
}

/**
* Generation.result 来自运行时边界,写回 WorkflowRun 前必须按生成类型收窄。
*
* 兼容后端两种返回格式:
* - 旧版单图:`{ type, image_url: "..." }`
* - 新版多图:`{ type, image_urls: ["...", "..."] }`
*/
export function parseCharacterTemplateGenerationResult(
value: unknown,
): CharacterTemplateGenerationResult | null {
if (
!isRecord(value) ||
(value.type !== 'character_template' && value.type !== 'character_image')
) {
return null
}

// 优先使用 image_urls(多图),兼容 image_url(单图)
const rawUrls: string[] = []
if (Array.isArray(value.image_urls)) {
for (const item of value.image_urls) {
if (typeof item === 'string' && item.length > 0) rawUrls.push(item)
}
} else if (typeof value.image_url === 'string' && value.image_url.length > 0) {
rawUrls.push(value.image_url)
}

// 兼容旧版 images 数组格式
if (rawUrls.length === 0 && Array.isArray(value.images)) {
for (const image of value.images) {
if (isRecord(image) && typeof image.url === 'string' && image.url.length > 0) {
rawUrls.push(image.url)
}
}
}

if (rawUrls.length === 0) return null

const images: GeneratedImage[] = rawUrls.map((url) => ({ url }))
return { type: 'character_template', images }
}

export interface FirstFrameGenerationResult {
type: 'first_frame'
image: GeneratedImage
}

export interface GeneratedAnimationFrame extends GeneratedImage {
durationMs: number | null
}

/** 帧顺序由数组位置表达。 */
export interface CompleteAnimationGenerationResult {
type: 'complete_animation'
frames: readonly GeneratedImage[]
actionType: ActionType
frames: readonly GeneratedAnimationFrame[]
}

/** 校验已经过适配层归一化的完整动画结果,供本地持久化恢复使用。 */
export function parseCompleteAnimationGenerationResult(
value: unknown,
): CompleteAnimationGenerationResult | null {
if (!isRecord(value) || value.type !== 'complete_animation') return null
if (!['walk', 'idle', 'attack', 'jump', 'custom'].includes(String(value.actionType))) return null
if (!Array.isArray(value.frames) || value.frames.length === 0) return null
const frames: GeneratedAnimationFrame[] = []
for (const frame of value.frames) {
if (
!isRecord(frame) ||
typeof frame.url !== 'string' ||
frame.url.length === 0 ||
(frame.durationMs !== null && typeof frame.durationMs !== 'number')
) {
return null
}
frames.push({ url: frame.url, durationMs: frame.durationMs as number | null })
}
return { type: 'complete_animation', actionType: value.actionType as ActionType, frames }
}

export type GenerationResult =
Expand All @@ -98,50 +162,49 @@ export type GenerationResultFor<T extends GenerationInput> =
: CompleteAnimationGenerationResult

/**
* 一次生成任务的完整快照,创建、查询和断线恢复都用它
* 一次生成任务的完整快照。
* 它是服务端的资源,不是一次「调用能力」——前端创建它,然后订阅或轮询它的状态。
*
* TType 在调用边界已知时保留精确类型;按 ID 恢复时用默认值,等运行时解析后再收窄。
* 完成不代表工作流节点已通过,节点状态由 WorkflowStep 自己判定。
*/
export interface Generation<TType extends GenerationType = GenerationType> {
/** 创建接口返回的后端任务 ID。 */
id: string
projectId: string
/** 与创建时的输入判别字段保持同一字面量类型。 */
type: TType
status: TaskStatus
status: GenerationTaskStatus
/** 完成前为 null;完成后形状由 type 决定。 */
result: GenerationResult | null
/** status 为 failed 时有值。 */
error: string | null
}

/**
* 一条状态变更事件。
* 不含 projectId:后端事件 payload 只有 task_id、task_type、status,
* 以及完成时的 result 和失败时的 error_message。
*/
/** 后端任务状态变化映射成同一份 Generation 快照。 */
export interface GenerationEvent<TType extends GenerationType = GenerationType> extends Omit<
Generation<TType>,
'id' | 'projectId'
> {
/** 对应 Generation.id,字段名沿用后端事件里的 task_id。 */
/** 字段名对应后端事件中的 task_id,但语义上仍是 Generation.id。 */
taskId: Generation['id']
}

/** Generation 对应的一组后端接口。服务端没有取消能力,因此这里不声明 cancel。 */
/** Generation 对应的一组后端接口。 */
export interface GenerationApis {
/** 创建一次生成任务。 */
create<T extends GenerationInput>(input: T): Promise<Generation<T['type']>>
/** 按所属项目和任务 ID 读取生成任务的最新快照。 */
get(projectId: Generation['projectId'], id: Generation['id']): Promise<Generation>
/**
* 按所属项目和任务 ID 读取最新快照
* projectId 不能从 id 推导,后端查询接口要求两者同时传入
* 订阅任务状态。当前后端没有 SSE 时,实现可以封装轮询;调用方不感知传输方式
* 返回取消订阅函数
*/
get(projectId: Generation['projectId'], id: Generation['id']): Promise<Generation>
/** 订阅状态变化,返回取消订阅函数。 */
subscribe(
projectId: Generation['projectId'],
id: Generation['id'],
onEvent: (event: GenerationEvent) => void,
onError?: (error: Error) => void,
): () => void
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
17 changes: 14 additions & 3 deletions frontend/src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ export { characterApis } from './character'
/* 动作模板 —— 能跨角色复用的配方 */
export type { ActionTemplate, ActionTemplateApis } from './action-template'

/* 生成 —— 业务数据,不是「调用生成能力」;后端的 task 就是它,不另立实体 */
/* 生成 —— 业务数据,不是「调用生成能力」 */
export {
COMPLETE_ANIMATION_FRAME_COUNT,
parseCharacterTemplateGenerationResult,
} from './generation'
export type {
CharacterTemplateGenerationInput,
CharacterTemplateGenerationResult,
CompleteAnimationGenerationInput,
CompleteAnimationGenerationResult,
FirstFrameGenerationInput,
FirstFrameGenerationResult,
GeneratedAnimationFrame,
GeneratedImage,
Generation,
GenerationApis,
Expand All @@ -43,16 +48,21 @@ export type {
GenerationResult,
GenerationResultFor,
GenerationType,
TaskStatus,
GenerationTaskStatus,
} from './generation'

/* 媒体引用 —— 不承诺 URL 或后端 Media ID 的具体表示 */
export type { MediaReference } from './media'

/* 工作流 —— 节点与运行状态都由前端管理 */
export { WORKFLOW_STEP_ORDER } from './workflow-run'
export { createWorkflowRunStore, WORKFLOW_STEP_ORDER } from './workflow-run'
export type {
ActionGenerationWorkflowStep,
CharacterSetupStepInput,
CharacterSetupWorkflowStep,
CharacterTemplateWorkflowStep,
CreateWorkflowRunInput,
CreateWorkflowRunStoreOptions,
ExportStatus,
GenerationStatus,
WorkflowDriver,
Expand All @@ -62,6 +72,7 @@ export type {
WorkflowRevision,
WorkflowRevisionStatus,
WorkflowRun,
WorkflowRunStore,
WorkflowRunPurpose,
WorkflowRunStatus,
} from './workflow-run'
11 changes: 11 additions & 0 deletions frontend/src/entities/workflow-run/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# WorkflowRun

本目录保存 Controller 使用的 WorkflowRun 数据模型和本地存储。这里沿用当前工作区的最小概念:
`WorkflowRun + WorkflowRunStore`,不增加 Snapshot 或 Handle。

- `WorkflowRun` 是一条角色制作流程,持有 Revision、Step、角色关联和当前状态。
- `WorkflowRevision` 只表达从历史步骤重新执行形成的新版本。
- `WorkflowRunStore` 直接读写 WorkflowRun,并保证同一 Character 只绑定一条 Run。
- 新增动作在原 Run 当前 Revision 末尾追加动作生成和审核步骤。

后端 GenerationTask 是步骤引用的异步任务,不等于 WorkflowRun。
16 changes: 16 additions & 0 deletions frontend/src/entities/workflow-run/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const WORKFLOW_DRIVERS = ['ai', 'manual'] as const
export const WORKFLOW_PURPOSES = ['create_character', 'add_action'] as const
export const WORKFLOW_RUN_STATUSES = ['active', 'interrupted', 'completed', 'failed'] as const
export const WORKFLOW_REVISION_STATUSES = ['active', 'completed', 'failed', 'abandoned'] as const
export const GENERATION_STATUSES = ['not_started', 'in_progress', 'completed', 'failed'] as const
export const EXPORT_STATUSES = ['not_exported', 'exporting', 'exported', 'failed'] as const
export const WORKFLOW_STEP_STATUSES = ['locked', 'available', 'active', 'passed', 'failed'] as const

/** 新建角色时的基础五步;完成后可继续追加 action-generation / review 成对步骤。 */
export const WORKFLOW_STEP_ORDER = [
'character-setup',
'character-template',
'template-candidate',
'action-generation',
'review',
] as const
Loading