Skip to content

feat(generation): 生成任务编排——图片/动作生成 + 视频下载重试 - #151

Closed
xiaocheny214 wants to merge 8 commits into
1024XEngineer:mainfrom
xiaocheny214:feat/generation
Closed

feat(generation): 生成任务编排——图片/动作生成 + 视频下载重试#151
xiaocheny214 wants to merge 8 commits into
1024XEngineer:mainfrom
xiaocheny214:feat/generation

Conversation

@xiaocheny214

Copy link
Copy Markdown
Contributor

概述

生成任务编排模块:图片生成、动作生成、任务轮询、后台执行,以及视频下载重试修复。

包含内容

生成编排(server/orchestrator)

  • generation 包重命名为 orchestrator
  • interface、model、service、task_repo、executor
  • after_commit 回调修复任务行未提交竞态

生成 API(web/api/generation)

  • POST /generation/image:角色图片生成
  • POST /generation/action:角色动作生成
  • GET /generation/tasks/{task_id}:任务状态查询
  • JWT 归属校验 + project 归属校验

视频下载重试(framework/providers/sufy)

  • 下载完成后校验文件长度
  • 失败自动重试

测试

  • test_sufy_video_download.py:视频下载重试用例

关联

xiaocheny214 and others added 8 commits August 6, 2026 18:22
- Add backend/Dockerfile with multi-stage build (uv + Python 3.12)
- Add docker-compose.yml with backend and PostgreSQL services
- Add db/init.sql for automatic database table initialization
- Add .env.example with configuration template
- PostgreSQL configured with port 7856 and secure password
…browser

三处让部署跑不起来的问题,都在这台服务器上实测定位:

1. 构建阶段 uv sync 超时。宿主机访问 pypi.org 需 8s,构建容器内默认超时会在
   下载大包(uvloop)时 "operation timed out" 直接失败。改走国内镜像源并把
   UV_HTTP_TIMEOUT 拉到 180s。

2. 容器起来即反复重启,报 "exec /app/.venv/bin/uvicorn: no such file or directory"。
   文件其实存在,报的是它 shebang 指向的解释器——uv 装出来的 venv 里 shebang 与
   .pth 都是绝对路径,builder 在 /build、runtime 在 /app,跨路径拷贝后解释器与
   workspace 包全部失效。把 builder 的 WORKDIR 也改成 /app 即可。

3. 七牛上传 TLS 握手超时、媒体上传请求挂死。宿主机网卡 MTU 1480,而 compose
   自建网络不继承 daemon 的 mtu 设置、默认仍是 1500,大包被丢。显式给网络设
   1450 后,up-z0.qiniup.com 从握手超时 14s 变为 1.0s,上传恢复正常。

4. 浏览器跨域被全部拦下:OPTIONS 预检返回 405、响应无 access-control-* 头,
   后端日志里连请求都看不到。挂上 CORSMiddleware,允许来源用
   WINDUP_CORS_ORIGINS 覆盖,并放行 Vercel 预览域名。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
后端验证码/refresh_token 依赖 Redis,原 compose 只有 Postgres。
新增 redis:7-alpine 服务(含健康检查),backend depends_on 等待就绪,
环境变量 REDIS_URL=redis://redis:6379/0。
ORM 声明了 UniqueConstraint(user_id, project_name),但 init.sql 建表时遗漏。
生产 Postgres 并发创建同名项目不会触发 IntegrityError,API 兜底失效。
补上 CONSTRAINT uq_windup_project_user_name UNIQUE (user_id, project_name)。
…imiting

- 注册/登录(邮箱+验证码+密码)、免密登录、刷新 token、登出、改密
- JWT 鉴权中间件(白名单放行 + request.state.current_user 注入)
- 邮箱验证码(Redis 存储 + 冷却计时)
- 接口限流中间件(Redis 滑动窗口 + 降级策略)
- Redis 连接配置与客户端单例
- 22 个集成测试覆盖完整认证链路
- Add auth_client fixture with valid JWT token
- Update test_project_api.py to use auth_client
- Fix CI failures caused by auth middleware blocking unauthenticated requests
- workflow_run 模块:接口、ORM 模型、JSONB 节点树 schema
- agent 模块:SSE 会话管理骨架
- project/character 模块:接口、ORM 模型、service 实现
- 统一异常处理器(BizException 继承体系)
- media 上传 API
- server/generation → server/orchestrator(生成任务编排/调度)
- orchestrator 模块:interface、model、service、task_repo、executor
- generation API:图片生成 + 动作生成 + 任务轮询
- after_commit 回调修复任务行未提交竞态
- generation 端点从 JWT 取 user_id,加项目归属校验
@xiaocheny214 xiaocheny214 added the enhancement New feature or request label Aug 6, 2026
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 6, 2026 12:42pm

@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: found three security issues around tenant isolation and rate limiting.

# ── 端点 ─────────────────────────────────────────────────────────────────────


@router.post("", response_model=Response[CharacterOut])

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.

high: None of the /characters handlers verify that body.project_id or character_id belongs to request.state.current_user. Any authenticated user can create, list, read, update, or delete characters in another user's project by guessing IDs.

return Response.success(_task_to_out(task), message="任务已提交")


@router.post("/action", response_model=Response[GenerationTaskOut])

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.

high: project_id is optional here, and the handler never loads the character_id record. An authenticated caller can omit project_id or point it at a different project and still enqueue action generation for any guessed character ID.

RATELIMIT_USER_KEY = "ratelimit:api:{user_id}"


def _get_client_ip(request: Request) -> str:

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.

medium: _get_client_ip trusts X-Forwarded-For from the client. Unless a trusted proxy overwrites that header, callers can rotate the value and bypass both the global and sensitive-route limits.

johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 10, 2026
1024XEngineer#151 / 1024XEngineer#152 的内容迁到当前 main(efd230e)之上重提。主线上 orchestrator 只有
interface + model,缺 service / executor / task_repo;generation API 三个端点是
「接口待实现」。

## 迁移时按主线现状做的调整

**接口签名统一到 session-per-call。** 主线 orchestrator/interface.py 的三个方法没有
session 参数,而 1024XEngineer#176 刚合入的 workflow_run/service.py 用的是「session 首参 + 关键字
入参 + 只 flush 不 commit」。两处不一致会让同一个仓里出现两种事务写法,故 orchestrator
跟上已确立的那套;事务边界仍归 windup_framework.db.get_session。

**保留主线的 SSE 文档与终态关流。** interface.py 的 SSE 契约说明、generation.py 的
_TERMINAL_EVENTS 与终态后 break,都是主线后来加的,比源分支的「前端轮询」更准确,
原样保留。终态关流治的是:服务端发完 task_update 就关流但带 retry: 3000,浏览器原生
EventSource 每 3 秒重连、45 秒内 15 次,每次重收同一条 completed。

**user_id 从 JWT 取,不信客户端。** 源分支的请求体里有 user_id: int = Field(gt=0),
客户端可以填别人的 id。改为 request.state.current_user.id,并加项目归属校验
(项目不存在或不属于当前用户一律 404,不区分两者以免泄露他人项目是否存在)。

## 后台执行的两处竞态与分层

**after_commit 再起线程。** create_task 只 flush,session 要等 handler 返回后才 commit。
直接起线程的话,后台 session 可能读不到未提交的任务行,update 静默跳过——任务永远停在
PENDING 且无任何报错。改为注册 after_commit 回调,提交成功后才派发。

**executor 挂 app.state,不 import 进 web 层。** import-linter 的分层契约禁止
app.web 直连 ai_engine,而 executor 要调 ai_engine.impl。放进 state 由 bootstrap
(唯一装配点)注入,契约与实现两边都成立。

## 依赖链

orchestrator/executor.py import 了 windup_ai_engine.{impl,ports,strategy.concrete} 与
windup_common.models,四层里主线只有 windup_framework.providers。故本分支 stack 在
ai_engine 那条线之上,那条合入后 rebase。

Refs 1024XEngineer#171
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 10, 2026
1024XEngineer#151 / 1024XEngineer#152 的内容迁到当前 main(efd230e)之上重提。主线上 orchestrator 只有
interface + model,缺 service / executor / task_repo;generation API 三个端点是
「接口待实现」。

## 迁移时按主线现状做的调整

**接口签名统一到 session-per-call。** 主线 orchestrator/interface.py 的三个方法没有
session 参数,而 1024XEngineer#176 刚合入的 workflow_run/service.py 用的是「session 首参 + 关键字
入参 + 只 flush 不 commit」。两处不一致会让同一个仓里出现两种事务写法,故 orchestrator
跟上已确立的那套;事务边界仍归 windup_framework.db.get_session。

**保留主线的 SSE 文档与终态关流。** interface.py 的 SSE 契约说明、generation.py 的
_TERMINAL_EVENTS 与终态后 break,都是主线后来加的,比源分支的「前端轮询」更准确,
原样保留。终态关流治的是:服务端发完 task_update 就关流但带 retry: 3000,浏览器原生
EventSource 每 3 秒重连、45 秒内 15 次,每次重收同一条 completed。

**user_id 从 JWT 取,不信客户端。** 源分支的请求体里有 user_id: int = Field(gt=0),
客户端可以填别人的 id。改为 request.state.current_user.id,并加项目归属校验
(项目不存在或不属于当前用户一律 404,不区分两者以免泄露他人项目是否存在)。

## 后台执行的两处竞态与分层

**after_commit 再起线程。** create_task 只 flush,session 要等 handler 返回后才 commit。
直接起线程的话,后台 session 可能读不到未提交的任务行,update 静默跳过——任务永远停在
PENDING 且无任何报错。改为注册 after_commit 回调,提交成功后才派发。

**executor 挂 app.state,不 import 进 web 层。** import-linter 的分层契约禁止
app.web 直连 ai_engine,而 executor 要调 ai_engine.impl。放进 state 由 bootstrap
(唯一装配点)注入,契约与实现两边都成立。

## 依赖链

orchestrator/executor.py import 了 windup_ai_engine.{impl,ports,strategy.concrete} 与
windup_common.models,四层里主线只有 windup_framework.providers。故本分支 stack 在
ai_engine 那条线之上,那条合入后 rebase。

Refs 1024XEngineer#171
@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

orchestrator 部分已按最新 main 迁移至 #182(含 service / executor / task_repo + generation 三个端点真实现)。完整的合并顺序与迁移说明见 #152 下的回复。

johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 10, 2026
1024XEngineer#151 / 1024XEngineer#152 的内容迁到当前 main(efd230e)之上重提。主线上 orchestrator 只有
interface + model,缺 service / executor / task_repo;generation API 三个端点是
「接口待实现」。

## 迁移时按主线现状做的调整

**接口签名统一到 session-per-call。** 主线 orchestrator/interface.py 的三个方法没有
session 参数,而 1024XEngineer#176 刚合入的 workflow_run/service.py 用的是「session 首参 + 关键字
入参 + 只 flush 不 commit」。两处不一致会让同一个仓里出现两种事务写法,故 orchestrator
跟上已确立的那套;事务边界仍归 windup_framework.db.get_session。

**保留主线的 SSE 文档与终态关流。** interface.py 的 SSE 契约说明、generation.py 的
_TERMINAL_EVENTS 与终态后 break,都是主线后来加的,比源分支的「前端轮询」更准确,
原样保留。终态关流治的是:服务端发完 task_update 就关流但带 retry: 3000,浏览器原生
EventSource 每 3 秒重连、45 秒内 15 次,每次重收同一条 completed。

**user_id 从 JWT 取,不信客户端。** 源分支的请求体里有 user_id: int = Field(gt=0),
客户端可以填别人的 id。改为 request.state.current_user.id,并加项目归属校验
(项目不存在或不属于当前用户一律 404,不区分两者以免泄露他人项目是否存在)。

## 后台执行的两处竞态与分层

**after_commit 再起线程。** create_task 只 flush,session 要等 handler 返回后才 commit。
直接起线程的话,后台 session 可能读不到未提交的任务行,update 静默跳过——任务永远停在
PENDING 且无任何报错。改为注册 after_commit 回调,提交成功后才派发。

**executor 挂 app.state,不 import 进 web 层。** import-linter 的分层契约禁止
app.web 直连 ai_engine,而 executor 要调 ai_engine.impl。放进 state 由 bootstrap
(唯一装配点)注入,契约与实现两边都成立。

## 依赖链

orchestrator/executor.py import 了 windup_ai_engine.{impl,ports,strategy.concrete} 与
windup_common.models,四层里主线只有 windup_framework.providers。故本分支 stack 在
ai_engine 那条线之上,那条合入后 rebase。

Refs 1024XEngineer#171
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 11, 2026
1024XEngineer#151 / 1024XEngineer#152 的内容迁到当前 main(efd230e)之上重提。主线上 orchestrator 只有
interface + model,缺 service / executor / task_repo;generation API 三个端点是
「接口待实现」。

## 迁移时按主线现状做的调整

**接口签名统一到 session-per-call。** 主线 orchestrator/interface.py 的三个方法没有
session 参数,而 1024XEngineer#176 刚合入的 workflow_run/service.py 用的是「session 首参 + 关键字
入参 + 只 flush 不 commit」。两处不一致会让同一个仓里出现两种事务写法,故 orchestrator
跟上已确立的那套;事务边界仍归 windup_framework.db.get_session。

**保留主线的 SSE 文档与终态关流。** interface.py 的 SSE 契约说明、generation.py 的
_TERMINAL_EVENTS 与终态后 break,都是主线后来加的,比源分支的「前端轮询」更准确,
原样保留。终态关流治的是:服务端发完 task_update 就关流但带 retry: 3000,浏览器原生
EventSource 每 3 秒重连、45 秒内 15 次,每次重收同一条 completed。

**user_id 从 JWT 取,不信客户端。** 源分支的请求体里有 user_id: int = Field(gt=0),
客户端可以填别人的 id。改为 request.state.current_user.id,并加项目归属校验
(项目不存在或不属于当前用户一律 404,不区分两者以免泄露他人项目是否存在)。

## 后台执行的两处竞态与分层

**after_commit 再起线程。** create_task 只 flush,session 要等 handler 返回后才 commit。
直接起线程的话,后台 session 可能读不到未提交的任务行,update 静默跳过——任务永远停在
PENDING 且无任何报错。改为注册 after_commit 回调,提交成功后才派发。

**executor 挂 app.state,不 import 进 web 层。** import-linter 的分层契约禁止
app.web 直连 ai_engine,而 executor 要调 ai_engine.impl。放进 state 由 bootstrap
(唯一装配点)注入,契约与实现两边都成立。

## 依赖链

orchestrator/executor.py import 了 windup_ai_engine.{impl,ports,strategy.concrete} 与
windup_common.models,四层里主线只有 windup_framework.providers。故本分支 stack 在
ai_engine 那条线之上,那条合入后 rebase。

Refs 1024XEngineer#171
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 11, 2026
1024XEngineer#151 / 1024XEngineer#152 的内容迁到当前 main(efd230e)之上重提。主线上 orchestrator 只有
interface + model,缺 service / executor / task_repo;generation API 三个端点是
「接口待实现」。

## 迁移时按主线现状做的调整

**接口签名统一到 session-per-call。** 主线 orchestrator/interface.py 的三个方法没有
session 参数,而 1024XEngineer#176 刚合入的 workflow_run/service.py 用的是「session 首参 + 关键字
入参 + 只 flush 不 commit」。两处不一致会让同一个仓里出现两种事务写法,故 orchestrator
跟上已确立的那套;事务边界仍归 windup_framework.db.get_session。

**保留主线的 SSE 文档与终态关流。** interface.py 的 SSE 契约说明、generation.py 的
_TERMINAL_EVENTS 与终态后 break,都是主线后来加的,比源分支的「前端轮询」更准确,
原样保留。终态关流治的是:服务端发完 task_update 就关流但带 retry: 3000,浏览器原生
EventSource 每 3 秒重连、45 秒内 15 次,每次重收同一条 completed。

**user_id 从 JWT 取,不信客户端。** 源分支的请求体里有 user_id: int = Field(gt=0),
客户端可以填别人的 id。改为 request.state.current_user.id,并加项目归属校验
(项目不存在或不属于当前用户一律 404,不区分两者以免泄露他人项目是否存在)。

## 后台执行的两处竞态与分层

**after_commit 再起线程。** create_task 只 flush,session 要等 handler 返回后才 commit。
直接起线程的话,后台 session 可能读不到未提交的任务行,update 静默跳过——任务永远停在
PENDING 且无任何报错。改为注册 after_commit 回调,提交成功后才派发。

**executor 挂 app.state,不 import 进 web 层。** import-linter 的分层契约禁止
app.web 直连 ai_engine,而 executor 要调 ai_engine.impl。放进 state 由 bootstrap
(唯一装配点)注入,契约与实现两边都成立。

## 依赖链

orchestrator/executor.py import 了 windup_ai_engine.{impl,ports,strategy.concrete} 与
windup_common.models,四层里主线只有 windup_framework.providers。故本分支 stack 在
ai_engine 那条线之上,那条合入后 rebase。

Refs 1024XEngineer#171
johnnyzhang-eng added a commit to johnnyzhang-eng/game-asset-character that referenced this pull request Aug 11, 2026
1024XEngineer#151 / 1024XEngineer#152 的内容迁到当前 main(efd230e)之上重提。主线上 orchestrator 只有
interface + model,缺 service / executor / task_repo;generation API 三个端点是
「接口待实现」。

## 迁移时按主线现状做的调整

**接口签名统一到 session-per-call。** 主线 orchestrator/interface.py 的三个方法没有
session 参数,而 1024XEngineer#176 刚合入的 workflow_run/service.py 用的是「session 首参 + 关键字
入参 + 只 flush 不 commit」。两处不一致会让同一个仓里出现两种事务写法,故 orchestrator
跟上已确立的那套;事务边界仍归 windup_framework.db.get_session。

**保留主线的 SSE 文档与终态关流。** interface.py 的 SSE 契约说明、generation.py 的
_TERMINAL_EVENTS 与终态后 break,都是主线后来加的,比源分支的「前端轮询」更准确,
原样保留。终态关流治的是:服务端发完 task_update 就关流但带 retry: 3000,浏览器原生
EventSource 每 3 秒重连、45 秒内 15 次,每次重收同一条 completed。

**user_id 从 JWT 取,不信客户端。** 源分支的请求体里有 user_id: int = Field(gt=0),
客户端可以填别人的 id。改为 request.state.current_user.id,并加项目归属校验
(项目不存在或不属于当前用户一律 404,不区分两者以免泄露他人项目是否存在)。

## 后台执行的两处竞态与分层

**after_commit 再起线程。** create_task 只 flush,session 要等 handler 返回后才 commit。
直接起线程的话,后台 session 可能读不到未提交的任务行,update 静默跳过——任务永远停在
PENDING 且无任何报错。改为注册 after_commit 回调,提交成功后才派发。

**executor 挂 app.state,不 import 进 web 层。** import-linter 的分层契约禁止
app.web 直连 ai_engine,而 executor 要调 ai_engine.impl。放进 state 由 bootstrap
(唯一装配点)注入,契约与实现两边都成立。

## 依赖链

orchestrator/executor.py import 了 windup_ai_engine.{impl,ports,strategy.concrete} 与
windup_common.models,四层里主线只有 windup_framework.providers。故本分支 stack 在
ai_engine 那条线之上,那条合入后 rebase。

Refs 1024XEngineer#171
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

视频已生成、费用已产生,却因下载读 body 断一次就整单丢弃(无重试 / 不校验长度)

2 participants