Skip to content

feat(generation): add validated SSE task adapter - #110

Open
xyh202131 wants to merge 2 commits into
1024XEngineer:mainfrom
xyh202131:feat/generation-sse-adapter
Open

feat(generation): add validated SSE task adapter#110
xyh202131 wants to merge 2 commits into
1024XEngineer:mainfrom
xyh202131:feat/generation-sse-adapter

Conversation

@xyh202131

@xyh202131 xyh202131 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Generation + SSE Adapter

Refs #78

Issue #78 已补 mile3 标签并关联 milestone/4。本变更只实现前端 Generation
实体适配器与 SSE 传输封装,并同步修正相关接口文档;不修改后端、页面、Controller
或构建产物。

变更范围

  • 将创建、按项目查询和状态订阅统一收口到 GenerationApis
  • createGenerationApis 必须由宿主注入 userIdtransport;模块不写死用户身份,
    也不直接持有 fetchEventSource
  • 新增业务无关的 shared/api/stream.ts。后端 SSE 需要 Bearer Token,因此使用
    fetch + ReadableStream,由宿主注入 token provider;支持命名事件、主动取消、
    终态关闭和网络断线重连,不恢复 2 秒业务轮询。
  • 查询与订阅要求调用方传入 WorkflowRun 已知的阶段期望;动作阶段还必须带上
    actionType,避免把其他动作的帧误接到当前任务。
  • 角色母版由宿主通过 resolveImageSize(projectId) 提供项目画布尺寸;Generation 不直接
    依赖 Project,也不会回退到可能冲突的 1024 默认值。

三阶段合同

前端阶段 后端请求 固定/可配置数量 结果映射
character_template POST /generation/image 固定 num_images: 4 严格校验并映射 4 个候选
first_frame POST /generation/action 固定 num_frames: 1 严格校验并映射 1 帧动作首帧
complete_animation POST /generation/action 当前固定 num_frames: 16 index 排序并保留 duration_ms

CompleteAnimationGenerationInput 当前没有 frameCount 字段,因此无法由调用输入表达
帧数。本次保守沿用后端合同默认值 16;后续若合同加入可配置字段,应改为透传输入并补充
边界校验,而不是继续保留常量。

SSE 行为

  • 端点:/generation/tasks/{taskId}/stream?project_id=...
  • 只监听 task_update 命名事件,事件 DTO 在 Generation 边界解析和校验。
  • 后端完整任务事件使用数字 id;适配器在边界转换为前端字符串 taskId
  • 每次建连前重新读取 token 并设置 Authorization: Bearer ...,token 刷新后重连会使用新值。
  • completedfailed 都视为终态;事件先交付调用方,再由传输层关闭连接。
  • onError 必传,非法事件关闭连接时不能静默留下永远等待的工作流。
  • 显式取消通过 AbortController 中止当前 fetch 请求。
  • 非法 JSON、非法 DTO 或调用方事件处理异常会报告错误并关闭连接。
  • 网络中断会报告错误并按配置重连;HTTP 错误、非法响应和业务 DTO 错误停止重连。
    没有定时 GET 或其他业务轮询。

DTO 校验

  • 校验响应 envelope 的 code/message/data,业务错误不会被当作成功数据。
  • 校验任务与事件的正整数 ID、项目归属、用户归属、输入、任务类型和四个合法状态。
  • 未知状态直接抛 GenerationApiError,绝不降级为 pending
  • 校验完成结果的判别字段、图片 URL、候选/首帧数量、动作类型、16 帧数量与连续索引、
    时长格式与状态/错误一致性;完整动画保留逐帧时长,非完成任务携带结果同样视为非法合同。

测试覆盖

  • 三阶段请求体映射与注入用户身份。
  • 角色母版四候选、动作一首帧、完整动画帧排序。
  • 未知状态与非法完成结果 DTO。
  • SSE URL、Bearer Token、重连时刷新 token、id → taskId、主动取消、终态关闭、
    事件解析错误和断线恢复语义。

验证结果

  • npx oxfmt --check 定向检查本次前端合同与 TypeScript 文件:通过。
  • npm run format:check 全量检查:已执行,但被基线中 41 个本次所有权外文件阻断;
    未批量重写这些文件,以免覆盖其他工作者的修改。
  • npm run lint:通过。
  • npm run typecheck:通过。
  • npm test:在最新 main 合并基线上通过,20 个测试文件、91 个测试。
  • npm run build:通过,Vite 8.1.5 共转换 105 个模块。

后端 /generation/tasks/{taskId}/stream 以 DireSoul PR #34 的已合并实现为准;本 PR
按其完整 GenerationTask 事件和鉴权要求对齐。测试覆盖前端传输与映射,真实部署环境的
端到端联调仍应在集成分支执行。

@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 Error Error Aug 7, 2026 10:37am

@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

实测反馈:契约对得上,但终态不关流会无限重连;另外目前零 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 合了之后界面上仍然摸不到。

Co-Authored-By: Claude <noreply@anthropic.com>
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.

2 participants