Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c042937
feat(docker): add Docker Compose deployment for backend and PostgreSQL
xiaocheny214 Aug 4, 2026
dc3c362
fix(deploy): make the container build and the API reachable from the …
Soli22de Aug 4, 2026
0fb5bf0
fix(deps): docker-compose 添加 Redis 服务,注入 REDIS_URL
xiaocheny214 Aug 6, 2026
f99f30b
fix(db): init.sql 补充 windup_project 唯一约束,对齐 ORM 声明
xiaocheny214 Aug 6, 2026
746152c
feat(db): add workflow_run table and character.workflow_run_id FK
xiaocheny214 Aug 7, 2026
f4969f2
fix(app): remove orchestrator import, not needed for deploy
xiaocheny214 Aug 7, 2026
8905066
fix(app): remove character/project router imports, not present on dep…
xiaocheny214 Aug 7, 2026
8029170
fix(deploy): align backend POSTGRES_PASSWORD default with postgres se…
xiaocheny214 Aug 7, 2026
bc11ae0
Merge branch 'main' into feat/deploy
xiaocheny214 Aug 7, 2026
04d226c
chore(deploy): remove init.sql mount, schema now managed by ORM
xiaocheny214 Aug 7, 2026
251d0c2
chore(deploy): mount postgres data to host dir, add data/ to gitignore
xiaocheny214 Aug 7, 2026
f17f4c5
chore(deploy): mount redis data to host dir for persistence
xiaocheny214 Aug 7, 2026
86c3ca0
chore(db): remove init.sql, schema now managed by ORM
xiaocheny214 Aug 7, 2026
de24b29
refactor(deploy): use env_file to load .env, only override Docker-spe…
xiaocheny214 Aug 7, 2026
83fd9e4
docs: add POSTGRES_HOST/PORT, REDIS_URL, data dir to .env.example
xiaocheny214 Aug 7, 2026
abfefbb
refactor(app): make CORS origin regex configurable via env var
xiaocheny214 Aug 7, 2026
21a3c60
docs: replace real password with placeholder in .env.example
xiaocheny214 Aug 7, 2026
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
48 changes: 48 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ── Windup 环境变量配置示例 ───────────────────────────────────────────
# 复制此文件为 .env 并填入实际值
# cp .env.example .env

# ── PostgreSQL 配置 ──
POSTGRES_USER=root
POSTGRES_PASSWORD=your-password-here
POSTGRES_DB=windup
POSTGRES_HOST=localhost
POSTGRES_PORT=7856
# 宿主机访问数据库的端口(Docker 映射用)
POSTGRES_EXTERNAL_PORT=7856
# 数据持久化目录(默认 ./data/postgres)
POSTGRES_DATA_DIR=./data/postgres

# ── Redis 配置 ──
REDIS_URL=redis://localhost:6379/0
Comment thread
nighca marked this conversation as resolved.
# 数据持久化目录(默认 ./data/redis)
REDIS_DATA_DIR=./data/redis

# ── LLM 配置 ──
LLM_API_KEY=your-api-key-here
LLM_MODEL_ID=doubao-seed-2.0-mini
LLM_BASE_URL=https://api.qnaigc.com/v1
LLM_IMAGE_MODEL_ID=gemini-3.0-pro-image-preview
LLM_VIDEO_MODEL_ID=

# ── 搜索 API ──
SERPAPI_API_KEY=your-serpapi-key

# ── 七牛云存储 ──
QINIU_ACCESS_KEY=your-access-key
QINIU_SECRET_KEY=your-secret-key
QINIU_BUCKET_NAME=your-bucket
QINIU_BUCKET_DOMAIN=your-domain.com
QINIU_PRIVATE_SPACE=false

# ── AI Provider ──
AI_BASE_URL=https://api.qnaigc.com/v1
AI_API_KEY=your-ai-api-key

# ── 服务配置 ──
WINDUP_HOST=127.0.0.1
WINDUP_PORT=8000
# 跨域来源(逗号分隔,默认覆盖 localhost:5173/3000)
WINDUP_CORS_ORIGINS=
# 跨域正则匹配(默认允许 *.vercel.app)
WINDUP_CORS_ORIGIN_REGEX=https://.*\.vercel\.app
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ venv/
# 敏感配置(切勿提交)
.env
.env.*
!.env.example

# Docker 数据
data/

# 运行产物
output/
Expand Down
57 changes: 57 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ── 后端 Dockerfile ──────────────────────────────────────────────────
# 多阶段构建:builder 装依赖 → runtime 只拷贝产物,镜像更小

# ── 阶段 1: 构建 ──
FROM python:3.12-slim AS builder

# 安装 uv(比 pip 快 10x)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# ── 国内网络:走镜像源 + 拉长超时 ────────────────────────────────────────
# 实测(2026-08-04,这台服务器):宿主机访问 pypi.org 需 8s,构建容器内默认超时
# 会在下载大包(uvloop)时 "operation timed out" 直接失败。
ENV UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \
UV_HTTP_TIMEOUT=180

# 必须与 runtime 阶段同路径 —— venv 内的 shebang/.pth 是绝对路径,跨路径拷贝会失效
WORKDIR /app

# 先拷贝依赖定义,利用 Docker layer cache
COPY pyproject.toml uv.lock ./
COPY packages/common/pyproject.toml packages/common/
COPY packages/framework/pyproject.toml packages/framework/
COPY packages/ai_engine/pyproject.toml packages/ai_engine/
COPY packages/app/pyproject.toml packages/app/

# 安装依赖(不含 dev 依赖)
RUN uv sync --frozen --no-dev --no-install-workspace

# 拷贝源码并安装
COPY packages/ packages/
RUN uv sync --frozen --no-dev

# ── 阶段 2: 运行时 ──
FROM python:3.12-slim AS runtime

WORKDIR /app

# 从 builder 拷贝虚拟环境和包
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/packages /app/packages

# 把 venv/bin 加入 PATH
ENV PATH="/app/.venv/bin:$PATH"

# 默认环境变量(可被 docker-compose / .env 覆盖)
ENV WINDUP_HOST=0.0.0.0
ENV WINDUP_PORT=8000
ENV WINDUP_RELOAD=false

EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1

# 启动命令
CMD ["uvicorn", "windup_app.bootstrap.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"]
88 changes: 80 additions & 8 deletions backend/packages/app/src/windup_app/bootstrap/app.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,96 @@

"""FastAPI 应用工厂与装配入口。

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

``main`` 是开发启动入口:``python -m windup_app`` 或 ``windup`` 命令。
"""

import os
from contextlib import asynccontextmanager

import windup_framework.db # noqa: F401 组装时显式触发 DB engine/session 初始化
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from windup_app.web.api.agent import router as ai_router
from windup_app.web.api.generation import router as generation_router
from windup_app.web.api.media import router as media_router
from windup_app.web.api.workflow_run import router as workflow_run_router
from windup_app.web.handler.exception_handlers import register_exception_handlers


def create_app() -> FastAPI:
app = FastAPI(title="windup", version="0.1.0")
def _env_flag(name: str) -> bool:
"""把环境变量解析为真正的布尔值:仅 1/true/yes/on(忽略大小写与空白)视为 True。"""
return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}



def _cors_origins() -> list[str]:
"""允许跨域的前端来源,逗号分隔的 WINDUP_CORS_ORIGINS 覆盖。

不配这个中间件的话,浏览器会把前端的**所有**请求拦在预检那一步
(OPTIONS 返回 405、响应无 access-control-* 头),后端日志里连请求都看不到。
默认值覆盖本地 dev server 与 Vercel 预览域名。
"""
raw = os.getenv("WINDUP_CORS_ORIGINS", "").strip()
if raw:
return [o.strip() for o in raw.split(",") if o.strip()]
return ["http://localhost:5173", "http://127.0.0.1:5173",
"http://localhost:3000", "http://127.0.0.1:3000"]


def _cors_origin_regex() -> str | None:
"""CORS 正则匹配的额外来源,WINDUP_CORS_ORIGIN_REGEX 覆盖。

默认允许所有 Vercel 预览域名。
"""
return os.getenv("WINDUP_CORS_ORIGIN_REGEX", r"https://.*\.vercel\.app").strip() or None


def print_banner() -> None:
"""启动时打印 banner(占位实现,后续替换为正式 ASCII banner)。"""
print("windup 0.1.0 starting ...")

# 业务路由

@asynccontextmanager
async def _lifespan(app: FastAPI):
"""应用启动时打印 banner,关闭时无特殊处理。"""
print_banner()
yield


def create_app() -> FastAPI:
app = FastAPI(title="windup", version="0.1.0", lifespan=_lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins(),
allow_origin_regex=_cors_origin_regex(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(media_router)
app.include_router(generation_router)
app.include_router(workflow_run_router)
app.include_router(ai_router)

register_exception_handlers(app)
return app


def main() -> None:
"""开发启动入口:用 uvicorn 跑 ``create_app``。

host/port/reload 可用 ``WINDUP_HOST`` / ``WINDUP_PORT`` / ``WINDUP_RELOAD`` 覆盖。
"""
import uvicorn

uvicorn.run(
"windup_app.bootstrap.app:create_app",
factory=True,
host=os.getenv("WINDUP_HOST", "127.0.0.1"),
port=int(os.getenv("WINDUP_PORT", "8000")),
reload=_env_flag("WINDUP_RELOAD"),
)



if __name__ == "__main__":
main()
76 changes: 76 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ── Windup Docker Compose ─────────────────────────────────────────────
# 使用方式:
# 启动服务: docker compose up -d
# 查看日志: docker compose logs -f [service]
# 停止服务: docker compose down
# 清理数据: docker compose down -v (⚠️ 会删除数据库数据)
# 重新构建: docker compose up -d --build

services:
# ── Redis 缓存(验证码 / refresh_token) ──
redis:
image: redis:7-alpine
container_name: windup-redis
restart: unless-stopped
volumes:
- ${REDIS_DATA_DIR:-./data/redis}:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- windup-net

# ── PostgreSQL 数据库 ──
postgres:
image: postgres:16-alpine
container_name: windup-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-root}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-W1ndup@2026!Secure}
POSTGRES_DB: ${POSTGRES_DB:-windup}
ports:
- "${POSTGRES_EXTERNAL_PORT:-7856}:5432"
volumes:
- ${POSTGRES_DATA_DIR:-./data/postgres}:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-root} -d ${POSTGRES_DB:-windup}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- windup-net

# ── 后端 API 服务 ──
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: windup-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file:
- .env
environment:
# 容器内部通信用 Docker 主机名,覆盖 .env 中的本地开发值
REDIS_URL: redis://redis:6379/0
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
ports:
- "${WINDUP_PORT:-8000}:8000"
networks:
- windup-net

networks:
windup-net:
driver: bridge
# 宿主机链路 MTU 是 1480(eno1),compose 自建网络不会继承 daemon 的 mtu 设置,
# 默认仍是 1500 → 大包被丢,表现为 TLS 握手超时(七牛上传域名连不上、pip 下载卡死)。
driver_opts:
com.docker.network.driver.mtu: "1450"