Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Backend CI

on:
push:
pull_request:

# 同一 ref 新 run 取消旧的,省额度
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

# 开源项目最小权限
permissions:
contents: read

jobs:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend # uv workspace 在 backend/,不是 repo 根
steps:
- uses: actions/checkout@v7

- name: Install uv
# setup-uv 自 v8.0.0 起停止发布 floating major tag(@v7/@v8),
# 出于安全只发布 immutable tag,故用具体版本号
uses: astral-sh/setup-uv@v8.3.2
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.12

- name: Install dependencies
run: uv sync --frozen # 用提交的 uv.lock 锁定版本,不偷偷升级

- name: Ruff
run: uv run ruff check .

- name: Import-linter (分层契约)
run: uv run lint-imports

- name: Pytest
run: uv run pytest -q
108 changes: 108 additions & 0 deletions .github/workflows/naming.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Naming Convention

on:
push:
pull_request:

# 与 backend.yml 的 concurrency group 区分,避免两个 workflow 互相取消
concurrency:
group: naming-${{ github.ref }}
cancel-in-progress: true

# 开源项目最小权限
permissions:
contents: read

jobs:
# ── 命名规范门禁 ─────────────────────────────────────────────
# 分支名:<type>/<description>,如 feat/login、fix/redirect、docs/api
# 提交信息:Conventional Commits,<type>(<scope>)?: <描述>
# 长期分支(main/develop/release/*)与无斜杠的扁平分支名豁免;
# 上游 squash-merge 提交(结尾 (#NN))豁免--贡献者无法改写上游历史。
validate-branch:
name: Branch name
runs-on: ubuntu-latest
steps:
- name: Check branch name
env:
# 用 env 传参,避免 ${{ }} 直接插值进 shell 造成命令注入
EVENT_NAME: ${{ github.event_name }}
HEAD_REF: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
run: |
set -uo pipefail
if [ "$EVENT_NAME" = "pull_request" ]; then
branch="$HEAD_REF"
else
branch="$REF_NAME"
fi
echo "Branch: $branch"
# 长期分支豁免
case "$branch" in
main|master|develop) echo "豁免(长期分支): $branch"; exit 0 ;;
release/*|hotfix/*) echo "豁免(release/hotfix): $branch"; exit 0 ;;
esac
# 无斜杠的扁平分支名豁免(如 backend-architecture、upstream-sync)
case "$branch" in
*/*) ;;
*) echo "豁免(扁平名): $branch"; exit 0 ;;
esac
PATTERN='^(feat|fix|docs|doc|chore|refactor|test|style|perf|ci|build|revert|explore|wip)/.+'
if printf '%s' "$branch" | grep -Eq "$PATTERN"; then
echo "OK: '$branch'"
exit 0
fi
echo "::error::分支 '$branch' 不符合 <type>/<description> 规范。"
echo "允许的 type: feat fix docs doc chore refactor test style perf ci build revert explore wip"
echo "示例: feat/backend-architecture、fix/login-redirect、docs/api-reference"
exit 1

validate-commits:
name: Commit messages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # 需要完整历史来算 merge-base
- name: Check commit messages
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF_PR: ${{ github.base_ref }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "pull_request" ]; then
BASE_REF="$BASE_REF_PR"
else
BASE_REF="$DEFAULT_BRANCH"
fi
echo "Base ref: $BASE_REF"
git fetch --no-tags origin "$BASE_REF"
BASE=$(git merge-base "origin/$BASE_REF" HEAD)
echo "检查范围: $BASE..HEAD(merge 提交与上游 squash (#NN) 豁免)"
TYPE='(feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert)'
SCOPE='(\([^)]+\))?'
PATTERN="^${TYPE}${SCOPE}!?: .+"
fail=0; total=0
for sha in $(git rev-list --no-merges "$BASE..HEAD"); do
total=$((total + 1))
subject=$(git log -1 --format=%s "$sha")
if printf '%s' "$subject" | grep -Eq '\(#[0-9]+\)$'; then
echo "skip(上游 squash): $sha '$subject'"
continue
fi
if printf '%s' "$subject" | grep -Eq "$PATTERN"; then
echo "ok: $sha '$subject'"
else
echo "::error::commit $sha 不符合 Conventional Commits: '$subject'"
fail=1
fi
done
echo "共检查 $total 个非 merge 提交"
if [ "$fail" -ne 0 ]; then
echo ""
echo "格式: <type>(<scope>)?: <简要描述>"
echo "type: feat fix docs chore refactor test style perf ci build revert"
echo "示例: feat: 添加健康检查路由 fix(parser): 修复空指针"
exit 1
fi
27 changes: 25 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
# macOS
# Python
__pycache__/
*.py[cod]
*.egg-info/

# 虚拟环境
.venv/
venv/

# IDE
.idea/
.vscode/

# 系统文件
.DS_Store

# 依赖与构建产物由各子项目的 .gitignore 负责
# 敏感配置(切勿提交)
.env
.env.*

# 运行产物
output/

# 构建缓存
.ruff_cache/
.pytest_cache/
.import_linter_cache/
25 changes: 25 additions & 0 deletions backend/packages/ai_engine/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[project]
name = "windup-ai-engine"
version = "0.1.0"
description = "windup 资产生产引擎:langgraph 图 / 策略 / 切片 / 后处理 / prompt"
requires-python = ">=3.12"
dependencies = [
"windup-common",
"windup-framework",
"langgraph>=0.2",
"langchain-core>=0.3",
"pillow>=10.4",
"numpy>=1.26",
# "rembg", # 抠图(按需启用)
]

[tool.uv.sources]
windup-common = { workspace = true }
windup-framework = { workspace = true }

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/windup_ai_engine"]
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
30 changes: 30 additions & 0 deletions backend/packages/app/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[project]
name = "windup-app"
version = "0.1.0"
description = "windup 应用:server 领域 + web API + worker MQ 适配 + bootstrap 装配"
requires-python = ">=3.12"
dependencies = [
"windup-common",
"windup-framework",
"windup-ai-engine",
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
"pydantic>=2.7",
"sqlalchemy>=2.0",
"python-multipart>=0.0.9",
]

[project.scripts]
windup = "windup_app.bootstrap.app:main"

[tool.uv.sources]
windup-common = { workspace = true }
windup-framework = { workspace = true }
windup-ai-engine = { workspace = true }

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/windup_app"]
Empty file.
Empty file.
15 changes: 15 additions & 0 deletions backend/packages/app/src/windup_app/bootstrap/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""FastAPI 应用工厂与装配入口。

``create_app`` 负责创建 FastAPI 实例并挂载路由 / 中间件 / 异常处理,
是整个 web 服务的唯一装配点(composition root)。
"""

from fastapi import FastAPI

from windup_app.web.api.media import router as media_router


def create_app() -> FastAPI:
app = FastAPI(title="windup", version="0.1.0")
app.include_router(media_router)
return app
Empty file.
17 changes: 17 additions & 0 deletions backend/packages/app/src/windup_app/server/character/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""角色领域。"""

from windup_app.server.character.model import (
Character,
CharacterAction,
CharacterData,
CharacterFrame,
CharacterOutfit,
)

__all__ = [
"Character",
"CharacterAction",
"CharacterData",
"CharacterFrame",
"CharacterOutfit",
]
50 changes: 50 additions & 0 deletions backend/packages/app/src/windup_app/server/character/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""角色领域服务接口。

角色 API 只依赖本模块定义的抽象接口。具体实现(SQLAlchemy 等)应在
应用装配层继承 :class:`CharacterService` 后通过依赖注入提供。

约定
----
- session-per-call: ``session`` 由调用方(FastAPI 的 ``get_session`` 依赖)按请求传入。
- 具体实现保持无状态,可作为模块级单例。
- ``character_data`` 内造型/动作/帧的校验由 Pydantic schema 在 API 层完成,
接口层不感知其内部结构。
"""

from abc import ABC, abstractmethod

from sqlalchemy.orm import Session

from windup_app.server.character.model import Character


class CharacterService(ABC):
"""角色 CRUD 用例的抽象边界。"""

@abstractmethod
def create_character(self, session: Session, **fields) -> Character:
"""创建角色。

``fields`` 对齐请求体的字段集,由实现组装成 :class:`Character` 后持久化。
"""

@abstractmethod
def get_character(self, session: Session, character_id: int) -> Character | None:
"""按 ID 查询角色。"""

@abstractmethod
def list_characters(
self, session: Session, *, project_id: int, page: int, page_size: int,
) -> tuple[list[Character], int]:
"""分页查询项目下的角色列表,返回 (当前页数据, 总数)。"""

@abstractmethod
def update_character(self, session: Session, character_id: int, **fields) -> Character | None:
"""更新角色描述、参考图或 character_data 等字段。

返回更新后的角色;不存在时返回 ``None``。
"""

@abstractmethod
def delete_character(self, session: Session, character_id: int) -> bool:
"""删除角色并返回是否找到。"""
Loading