Skip to content

feat(generation): align validated SSE adapter with backend tasks - #110

Merged
xyh202131 merged 5 commits into
1024XEngineer:mainfrom
xyh202131:feat/generation-sse-adapter
Aug 11, 2026
Merged

feat(generation): align validated SSE adapter with backend tasks#110
xyh202131 merged 5 commits into
1024XEngineer:mainfrom
xyh202131:feat/generation-sse-adapter

Conversation

@xyh202131

@xyh202131 xyh202131 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

本次更新

  • Generation 类型严格对齐后端:character_imagecharacter_action;首帧和完整动画通过 num_frames(1 / 32)区分,不再另造任务类型。
  • 创建任务时携带当前认证用户的 user_id,并校验返回任务的用户、项目、任务 ID、类型、状态和终态结果。
  • 使用带鉴权的 fetch SSE 接收状态,兼容后端事件字段 task_idid,统一转换成前端 taskId
  • SSE 路由未部署时才回退任务查询;其他错误明确上报,并支持取消订阅和 401 会话恢复。
  • 动作结果保留后端帧序号、图片地址与时长,不在前端重新猜测。

依赖说明

当前 main 中已合并的旧 WorkflowController 仍使用旧 Generation 名称。本 PR 保持后端命名,不在适配器内重新引入旧概念;Controller 调用方需按新的 character_image / character_action 契约同步更新。

验证

  • Generation、SSE 与统一 API 相关测试:42 条通过。
  • TypeScript 类型检查、oxlint、格式检查通过。
  • 生产构建通过。

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
windup Ready Ready Preview Aug 11, 2026 8:11am

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary: two issues stand out.

Findings without inline locations

  • frontend/src/entities/generation/index.ts:102: The complete-animation result type only preserves URLs, but the backend action frame DTO already carries duration_ms and the frontend Frame model can store durationMs. Once this adapter maps the task into GenerationResult, that timing data is gone and downstream character writes will have to invent defaults.

Comment thread frontend/src/entities/generation/api.ts Outdated
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

实测反馈:契约对得上,但终态不关流会无限重连;另外目前零 UI 消费者

在集成分支(main + #117 + #119 + #111 + 本 PR)上验证,并对一台真实部署的后端订阅了一个已完成的任务。

1. 契约是对的,两处可以放心

  • 事件名:服务端实际只吐一种事件名 task_update,靠 payload 里的 status 区分状态,payload 五个键 task_id / task_type / status / result / error_message。本 PR 只监听 task_update 是对的。
    ⚠️ 注意另有一份设计文档写的是 status / progress / completed / failed 四个事件名,与线上实际行为不符,别照那份改。
  • URL 拼接带了 project_id,与后端强制要求的 query 参数一致。

2. 终态之后 EventSource 会每 3 秒重连一次,需要显式 close

服务端发完终态事件就关流,但响应里带了 retry: 3000。浏览器原生 EventSource 把「连接关闭」当成断线,于是按 retry 间隔重连——实测 45 秒内重连 15 次,每次都重新收到同一条 completed 事件,测试结束时强制 close,readyState 仍停在 CONNECTING。

也就是说:任务早就结束了,前端还在每 3 秒打一次请求,且业务回调会被重复触发。

修法二选一(前者更稳):

  • 前端在收到 status 为终态(completed / failed)时显式 es.close()
  • 或服务端在关流前不下发 retry,改用一个明确的终止事件。

3. 目前全仓没有调用方

createGenerationApis 的调用点,除测试外为 0。要接进界面还差组合根注入四样:baseUrluserIdtransport: { request, stream }(需要把 #117 的 client 与 shared/api/stream.tssubscribeToEventStream 适配进来)、resolveImageSize(projectId)。这一层目前没人认领,本 PR 合了之后界面上仍然摸不到。

@xyh202131
xyh202131 force-pushed the feat/generation-sse-adapter branch from b0bc0ce to 33b4863 Compare August 7, 2026 10:37
@xyh202131
xyh202131 force-pushed the feat/generation-sse-adapter branch from 33b4863 to d8d171b Compare August 8, 2026 08:11
@xyh202131 xyh202131 changed the title feat(generation): add validated SSE task adapter feat(generation): align validated SSE adapter with backend tasks Aug 8, 2026
@xyh202131
xyh202131 force-pushed the feat/generation-sse-adapter branch from f1042ab to 3fcdceb Compare August 11, 2026 06:57
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.95722% with 60 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
frontend/src/entities/generation/api.ts 84.27% 13 Missing and 26 partials ⚠️
frontend/src/shared/api/stream.ts 80.21% 8 Missing and 10 partials ⚠️
.../packages/app/src/windup_app/web/api/generation.py 91.17% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

@xyh202131

Copy link
Copy Markdown
Contributor Author

@fennoai 审核评价

@fennoai

fennoai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

审核完成

已针对固定提交范围 1f47667327e3ad91b152de2c91df1c88cc7d19fb...275b97e89a1ebec15bad33af66c2de509dab213c 完成代码质量、性能、安全与文档审查,并提交了一次非阻塞 PR 评审。

发现 4 个问题:

  • SSE 事件仅读取 id,未兼容要求中的 task_id
  • SSE 解析强制要求 project_id/user_id,与同 PR 声明的精简事件契约不一致。
  • 完整动画结果丢失后端帧 indexduration_ms,类型也无法暴露这些字段。
  • SSE 路由返回 404 等不可用错误时没有查询轮询兜底,运行中的任务可能永久卡住。

验证:尝试运行 Generation/SSE 测试与类型检查,但工作区未安装可执行的 vitest/tsc,命令均因 Permission denied 无法启动;未修改代码。

View job run

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

发现 4 个需要处理的问题,主要集中在 SSE 事件契约、结果字段保真和 SSE 不可用时的任务收敛。由于工作区未安装可执行的前端依赖,测试与类型检查无法启动。

Comment thread frontend/src/entities/generation/api.ts Outdated
Comment thread frontend/src/entities/generation/api.ts Outdated
Comment thread frontend/src/entities/generation/api.ts Outdated
Comment thread frontend/src/entities/generation/api.ts Outdated
@huyanxius

Copy link
Copy Markdown
Collaborator

LGTM👍

@xyh202131
xyh202131 merged commit 3d08f76 into 1024XEngineer:main Aug 11, 2026
7 checks passed
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 11, 2026
**先更正我上一版的方向。** 上一版删掉了 `project_id`、改用任务自己的 `user_id` 做归属
校验,并把 EventBus 改成单键。那是错的:主线 1024XEngineer#110 里 `project_id` 正是归属校验的依据
(`_get_project_or_raise`),且 EventBus 按 `(project_id, task_id)` 双键隔离同一 task_id
在不同项目下的流。删掉它会退化主线已有的能力。本版改为**在主线骨架上补齐它的 TODO**。

一、归属补成两道。主线已校验「项目属于当前用户」,缺「任务属于那个项目」。缺这一道,
   任意已认证用户拿**自己的** project_id 配上别人的 task_id 就能订阅到别人的流,而事件
   体带 result,即最终帧的对象存储 URL。两道都在 `subscribe` 之前 —— 放之后的话越权请求
   仍会在 EventBus 上挂一个订阅者(照样收事件、只是响应体被丢弃),订阅表还会因为没人
   unsubscribe 而增长。

二、终态预检落地(原先是一行 TODO,而 docstring 已经承诺了该行为)。实际表现是客户端要
   先挂满一次心跳超时才拿到终态。

三、跨线程投递:`publish` 改成**同 loop 直接入队、跨 loop 才 call_soon_threadsafe**。
   一律走 marshal 是错的 —— 那是异步调度,要等 loop 下一次迭代才真入队,于是
   「publish 完立刻 get_nowait」会拿到空队列,主线 1024XEngineer#110 的项目隔离用例正是这么写的。
   跨 loop 分支保留是因为 executor 在 daemon thread 里跑,而 asyncio.Queue 不是线程安全的。

四、`task.project_id` 为空时记 warning 并早退,不再 publish 到一个没人听的键上。
   静默发出去的现象是「任务确实在跑、状态也在落库,但前端进度条一动不动」,日志里一行
   异常都没有。

顺带:事件 payload 抽成公开的 `task_event_payload()`。终态预检是第二个发送点,在 API 层
再抄一份字段列表就是第二个真相源,加字段时漏一处会让客户端拿到两种形状的同名事件。

测试 10 条,7 条变异全部杀掉(去掉「任务属于项目」/ 去掉终态预检 / 校验挪到订阅之后 /
payload 少字段 / 终态映射把 failed 当 completed / project_id 为空时静默 publish /
publish 一律走异步 marshal)。

其中「payload 同形状」那条第一版是摆设:期望键集也用 task_event_payload 反算,两边同源、
删字段一起变、断言永远成立。已改成把 SSE 事件体键集写死为契约清单。
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 11, 2026
**先更正我上一版的方向。** 上一版删掉了 `project_id`、改用任务自己的 `user_id` 做归属
校验,并把 EventBus 改成单键。那是错的:主线 1024XEngineer#110 里 `project_id` 正是归属校验的依据
(`_get_project_or_raise`),且 EventBus 按 `(project_id, task_id)` 双键隔离同一 task_id
在不同项目下的流。删掉它会退化主线已有的能力。本版改为**在主线骨架上补齐它的 TODO**。

一、归属补成两道。主线已校验「项目属于当前用户」,缺「任务属于那个项目」。缺这一道,
   任意已认证用户拿**自己的** project_id 配上别人的 task_id 就能订阅到别人的流,而事件
   体带 result,即最终帧的对象存储 URL。两道都在 `subscribe` 之前 —— 放之后的话越权请求
   仍会在 EventBus 上挂一个订阅者(照样收事件、只是响应体被丢弃),订阅表还会因为没人
   unsubscribe 而增长。

二、终态预检落地(原先是一行 TODO,而 docstring 已经承诺了该行为)。实际表现是客户端要
   先挂满一次心跳超时才拿到终态。

三、跨线程投递:`publish` 改成**同 loop 直接入队、跨 loop 才 call_soon_threadsafe**。
   一律走 marshal 是错的 —— 那是异步调度,要等 loop 下一次迭代才真入队,于是
   「publish 完立刻 get_nowait」会拿到空队列,主线 1024XEngineer#110 的项目隔离用例正是这么写的。
   跨 loop 分支保留是因为 executor 在 daemon thread 里跑,而 asyncio.Queue 不是线程安全的。

四、`task.project_id` 为空时记 warning 并早退,不再 publish 到一个没人听的键上。
   静默发出去的现象是「任务确实在跑、状态也在落库,但前端进度条一动不动」,日志里一行
   异常都没有。

顺带:事件 payload 抽成公开的 `task_event_payload()`。终态预检是第二个发送点,在 API 层
再抄一份字段列表就是第二个真相源,加字段时漏一处会让客户端拿到两种形状的同名事件。

测试 10 条,7 条变异全部杀掉(去掉「任务属于项目」/ 去掉终态预检 / 校验挪到订阅之后 /
payload 少字段 / 终态映射把 failed 当 completed / project_id 为空时静默 publish /
publish 一律走异步 marshal)。

其中「payload 同形状」那条第一版是摆设:期望键集也用 task_event_payload 反算,两边同源、
删字段一起变、断言永远成立。已改成把 SSE 事件体键集写死为契约清单。
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 11, 2026
**先更正我上一版的方向。** 上一版删掉了 `project_id`、改用任务自己的 `user_id` 做归属
校验,并把 EventBus 改成单键。那是错的:主线 1024XEngineer#110 里 `project_id` 正是归属校验的依据
(`_get_project_or_raise`),且 EventBus 按 `(project_id, task_id)` 双键隔离同一 task_id
在不同项目下的流。删掉它会退化主线已有的能力。本版改为**在主线骨架上补齐它的 TODO**。

一、归属补成两道。主线已校验「项目属于当前用户」,缺「任务属于那个项目」。缺这一道,
   任意已认证用户拿**自己的** project_id 配上别人的 task_id 就能订阅到别人的流,而事件
   体带 result,即最终帧的对象存储 URL。两道都在 `subscribe` 之前 —— 放之后的话越权请求
   仍会在 EventBus 上挂一个订阅者(照样收事件、只是响应体被丢弃),订阅表还会因为没人
   unsubscribe 而增长。

二、终态预检落地(原先是一行 TODO,而 docstring 已经承诺了该行为)。实际表现是客户端要
   先挂满一次心跳超时才拿到终态。

三、跨线程投递:`publish` 改成**同 loop 直接入队、跨 loop 才 call_soon_threadsafe**。
   一律走 marshal 是错的 —— 那是异步调度,要等 loop 下一次迭代才真入队,于是
   「publish 完立刻 get_nowait」会拿到空队列,主线 1024XEngineer#110 的项目隔离用例正是这么写的。
   跨 loop 分支保留是因为 executor 在 daemon thread 里跑,而 asyncio.Queue 不是线程安全的。

四、`task.project_id` 为空时记 warning 并早退,不再 publish 到一个没人听的键上。
   静默发出去的现象是「任务确实在跑、状态也在落库,但前端进度条一动不动」,日志里一行
   异常都没有。

顺带:事件 payload 抽成公开的 `task_event_payload()`。终态预检是第二个发送点,在 API 层
再抄一份字段列表就是第二个真相源,加字段时漏一处会让客户端拿到两种形状的同名事件。

测试 10 条,7 条变异全部杀掉(去掉「任务属于项目」/ 去掉终态预检 / 校验挪到订阅之后 /
payload 少字段 / 终态映射把 failed 当 completed / project_id 为空时静默 publish /
publish 一律走异步 marshal)。

其中「payload 同形状」那条第一版是摆设:期望键集也用 task_event_payload 反算,两边同源、
删字段一起变、断言永远成立。已改成把 SSE 事件体键集写死为契约清单。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants