feat(backend): 后端全栈实现——基础设施 + 领域模块 + 用户认证 + API 契约 - #75
Conversation
Describe the backend package layout (common/framework/app) and the server domain module split: user, project, asset, character with action/character_template/wearable subdomains, generation and media. All modules currently define abstract interfaces only.
Add the shared response/exception kernel in windup_common: Response/ListResponse unified bodies (HTTP always 200, business code in body), BizException base, ModelException for LLM calls, and the BizCode/ModelErrorType enums. Add the app-level global exception handlers that convert these into Response.fail.
Add windup_framework infrastructure abstractions: SQLAlchemy db base/session, config loaders (database/provider/storage), LLM provider protocols (chat/image/video) and Kodo object storage.
Add abstract service interfaces and domain models for the server modules: user, project, asset, character (with action/character_template/wearable subdomains), generation and media. Drop the now-obsolete .gitkeep placeholders.
Drop the placeholder health router and its registration in create_app; the web layer now revolves around the global exception handlers.
Expose the windup CLI entrypoint (windup_app.bootstrap.app:main) and add python-multipart for form/file uploads; refresh uv.lock accordingly.
Point the sub-entity references at the nested character subpackages (character.action / character_template / wearable) instead of the old top-level paths.
Remove unused imports flagged by ruff F401 in generation/interface.py and user/model.py.
将 validate-branch 与 validate-commits 从 backend.yml 拆到独立的 naming.yml。命名规范门禁不局限于 backend 范畴,应作为仓库级 CI 单独维护(PR review:@minorcell 建议)。 backend.yml 仅保留 lint-and-test;naming.yml 用独立 concurrency group 避免与 backend 共用 group 互相取消。
Add SqlAlchemyProjectService inheriting ProjectService; implements create/get/list/delete plus project_name_exists against SQLAlchemy. - interface: reconcile ProjectService to session-per-call; session is passed per request via get_session, create_project takes **fields. - service: stateless impl as a module-level `service` singleton; flush only, commit/rollback handled by get_session; list ordered by id desc. - model: id uses BigInteger().with_variant(Integer, "sqlite") so the in-memory SQLite test DB autoincrements; Postgres unchanged. - api/bootstrap: import service from the service module, register project_router, add a print_banner startup stub.
Add SQLite in-memory test fixtures and project module tests. - conftest: SQLite in-memory fixtures (StaticPool, single connection) with no Docker Postgres dependency; get_session override for the API TestClient. - test_project_service: 14 unit tests for create/exists/get/list/delete covering the unique constraint, pagination, id-desc ordering and user filter. - test_project_api: 9 end-to-end tests via TestClient covering CRUD and the unified response contract (HTTP 200, code in body, 400/404).
Reflect character/media/generation/user module redesigns in the split document.
Replace strategy pattern with typed submit methods.
- model: add CharacterImageOutput/CharacterActionOutput/CharacterActionFrame
as typed task results, remove generic GenerationResult
- interface: split submit into generate_character_image and
generate_character_action with typed inputs
- document SSE streaming flow (GET /generation/tasks/{id}/stream)
replacing frontend polling
Remove OAuth abstract methods and models from UserService. Commented-out methods: get_oauth_authorize_url, login_by_oauth, bind_oauth, get_oauth_bindings. Related imports OAuthCallbackInput and UserOAuth removed from interface. The OAuth region is preserved as a placeholder for future extension.
Add ObjectStorageMediaService implementing MediaService. - service: upload to Kodo via KodoStorage adapter, auto-generated object keys with category prefix, no user filenames in keys - app: register media router (POST /media/upload) in create_app
refactor:asset character media
ci: split naming gate into its own workflow
feat:project module
跨层契约(common,无内部依赖):ActionType/GenRoute/CharacterCard/ActionSpec/AssetPackageRef。ai_engine 与 app 均依赖此。Refs 1024XEngineer#53。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SufyVideoProvider(kling i2v,OpenAI 兼容异步 /videos)+ OnnxU2NetMatteProvider(onnxruntime 直跑 u2netp,替代 rembg 在 3.12 无轮子的 numba 链)+ 能力接口(Image/Video/MatteProvider)。同时声明 framework 代码实际 import 的依赖(langchain-openai/onnxruntime/qiniu/pillow/numpy)——补齐 1024XEngineer#64 机器审指出的缺依赖。Refs 1024XEngineer#53 #35。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
填充空骨架:slicing(抽帧 + 周期/一次性选帧)、strategy(ROUTE_MATRIX 分流 + VideoFrameStrategy 图生视频)、prompt(walk/jump/attack/idle 提示词库)、postprocess(像素化 + 脚线对齐 + 打包 + 逐帧时长)、generator 串联。MVP:root_motion 暂不做(character_data.frames 无该字段,见 1024XEngineer#63);graph/ 留占位非本 PR。Refs 1024XEngineer#53 #35 #21。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove dead storage module (empty __init__.py and kodo.py) from framework - Implement actual Qiniu Kodo SDK upload in ObjectStorageMediaService - Move MediaCategory to windup_common.enums.media, remove duplicate from media/model - Update media __init__ to import MediaCategory from common
Move qiniu import inside upload() so module collection does not fail when qiniu is not installed (e.g. CI test runs).
按 MVP 边界(与作者对齐):CharacterGeneratorPort.generate(card, action, master, progress) → GeneratedAction(frames/durations/fps);母版由 server 以 bytes 传入,不再经 ArtifactStore;去掉 sprite sheet 打包与存储引用(上传/落库/导出在 server/export 侧)。补 VideoFrameStrategy.derive 离线端到端测试(monkeypatch 抽帧,不解码 mp4/不联网)。Refs 1024XEngineer#53。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(media): implement Qiniu upload and deduplicate MediaCategory
There was a problem hiding this comment.
补几条 08:18 那轮未覆盖的。与那轮重合的不重复,但跨用户越权(project.py:75 / generation.py:185)同属本次必须处理的范围。
以下均在 4ee4236 上核实。
已由 xiaocheny214#35 处理,不必再改(Refs #143):
router 注册被删除、CORS 通配配 credentials。两条属机械修复无设计取舍,我直接改了。
下面四条涉及设计决策,留给作者。
P0 · num_images / num_frames 无上限,直通付费循环
generation.py:88,101 无 le=;executor.py:367 为 for _ in range(max(1, input.num_images)),循环体是付费 gen_image。
实测 num_images=100000 被接受、原样落库。ratelimit.py:92 在 Redis 不可用时 return await call_next(request),即整体放行,每分钟 60 次的兜底也没有。
P0 · daemon 线程 + 无优雅退出
generation.py:149 起 daemon=True 线程;bootstrap/app.py:54-57 的 _lifespan 在 yield 后无任何处理(docstring 写「关闭时无特殊处理」)。backend/packages/ 下 atexit|signal\.|\.join\(\)|on_shutdown|add_event_handler 零命中。
后果:每次 deploy / reload 杀掉正在跑的 i2v。此时 RUNNING 已 commit、费用已产生,COMPLETED 永远不会写入,且重启后无扫描逻辑回收。生成耗时数分钟,轮询上限 30 分钟(sufy.py:53-61),窗口不小。
P1 · 起线程失败时静默返回 200
_dispatch_after_commit 的 after_commit 在 get_session teardown 时触发,此刻响应已生成。Thread.start() 抛 RuntimeError 时客户端已拿到 200「任务已提交」+ task id,DB 里一行 pending 无人执行,前端会一直轮询。
P1 · 生成全程独占一条 DB 连接
executor.py:165 的 _load_constraints 发 SELECT(autobegin),:166 的 _produce_action 随即跑数分钟,其间不 commit / close。实测生成期间 pool.checkedout() 恒为 1。
config/database.py:28-29 为 pool_size=5, max_overflow=10,即 15 条封顶。15 个并发生成即耗尽连接池,此后所有 HTTP 请求(含前端轮询任务状态)阻塞在等连接,表现为整站假死而非生成变慢。run_image_task 同(:302-303)。
以上四条修完(或说明为何不改)我复核后撤销本次 request changes。
…are 层级问题 BaseHTTPMiddleware 中 raise BizException 会跳过内部 ExceptionMiddleware, 被最外层 ServerErrorMiddleware 兜底捕获,返回 INTERNAL_ERROR 而非预期业务码。 改为在 dispatch 内直接构造 JSONResponse 返回。
- POST /projects: user_id 从 request.state.current_user 取
- GET /projects: 移除 user_id query param,强制只查自己的项目
- GET /projects/{id}: 加归属校验,非本人项目返回 404
- DELETE /projects/{id}: 同上
- ProjectCreate 移除 user_id 字段,ProjectOut 独立声明
- POST /generation/image: user_id 从 JWT 取,校验 project 归属
- POST /generation/action: 同上
- GET /generation/tasks/{task_id}: 改用 get_task_by_user 校验归属
- 请求模型移除 user_id 字段
CharacterCreate.name 未传入 service.create_character(),导致返回 name=None。 补上 name=body.name 并新增 3 个集成测试覆盖命名场景。
后端验证码/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)。
合并 main 时丢掉了 1024XEngineer#76 刚加入的两个 router 注册,合并后 /workflow-runs 与 /ai/chat 由可用变 404,而两个 api 文件仍在仓库里,静默失效无报错。 CORS 两处:去掉写死的 vercel 通配正则,并把 allow_credentials 置 False (鉴权走 Authorization 头不走 cookie);默认来源补 4173(vite preview)。 实测:端点数 16 → 19,恢复 /ai/chat 与 /workflow-runs{,/{run_id}}; 4173 过预检且响应无 allow-credentials 头,任意 vercel 域不再放行。 Refs 1024XEngineer#143 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(app): 补回 workflow-run / ai 两个 router,收紧 CORS(Refs 1024XEngineer#143)
不阻塞合并。两条已由 xiaocheny214#35 修复(Refs #143),其余四条记在 #145 备查。
johnnyzhang-eng
left a comment
There was a problem hiding this comment.
Approve。
合并顺序提醒:xiaocheny214#35 若未先合进 feat/backend-dev,本 PR 会把 #76 刚加入 main 的 workflow_run / ai 两个 router 注册删掉,/workflow-runs 与 /ai/chat 变 404。那个 PR 只改 1 个文件、门禁全绿。
其余四条记在 #145,不阻塞。
|
更新:补充了遗漏的 ai_engine 模块,现在共 5 个小 PR:
|
概述
本 PR 实现 Windup 后端完整能力:基础设施层、领域业务层、用户认证体系、生成管线,以及配套的 Docker 部署与测试覆盖。
新增能力
基础设施层(framework)
领域业务层(app/server)
API 层(web)
Response[T]/ListResponse[T]BizException继承体系)request.state.current_user注入)部署与 CI
init.sql建表脚本(含唯一约束)安全修复
BizException跳过ExceptionMiddlewareJSONResponseuser_id,可越权project_id无归属校验create_character漏传name,名称丢失init.sql缺唯一约束,生产可重名测试覆盖
test_user_service.py:22 用例(注册/登录/验证码/登出/刷新/改密/限流)test_project_api.py:9 用例(CRUD + 权限归属)test_character_api.py:3 用例(命名持久化)test_generation_orchestration.py:生成编排test_sufy_video_download.py:视频下载重试关联 Issue