Files
baozaotumao df48c36422 refactor: 规则中性化——剥离 PPT 业务,统一为订单示范域
把规则从原 ppt-gen 项目泛化为通用脚手架,覆盖 19 个文件:
- ddd/architecture/design-discipline/error-codes/communication-* 等:
  领域术语、错误码、消息示例、扩展点、状态流统一为「订单」示范域
- agent-dag/agent.md/agent-concurrency/agent-llm-*:DAG 节点、配置、并发、
  LLM 适配从「对话→预览→生成→截图→合成」改为「intake→process→validate→finalize」
- backend-db:Session 实体/状态机/ORM/仓储/checkpoint 对齐新流水线
- 移除幻灯片专属依赖与产物:agent 去 playwright/python-pptx,
  Dockerfile 去 Chromium 系统库,.gitignore 去 *.pptx
- 保留合法通用项:ANSI(PTY 处理)、Playwright(E2E 测试工具)、Gemini CLI(真实 provider)

验证:gemini-cli/openai-api 两变体生成退出 0,生成项目零 PPT 残留,
backend/agent 骨架均可 import 运行。
2026-06-01 23:15:05 -07:00

236 lines
7.5 KiB
Django/Jinja
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
paths: ["backend/**"]
---
# backend-db
数据库({% if database == 'sqlite' %}SQLite{% else %}PostgreSQL{% endif %} + SQLAlchemy async)、Session 模型、Alembic 迁移、Checkpoint 断点恢复与会话保留。下文实体字段以「订单」作示范。
## 数据库配置
{% if database == 'sqlite' %}
### 文件位置
```
~/{{ project_slug }}/{{ project_slug }}.db # 生产
:memory: # 测试(conftest.py 中覆盖)
```
### 引擎配置(db/engine.py
```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
engine = create_async_engine(
f"sqlite+aiosqlite:///{settings.db_path}",
echo=settings.env == "dev",
connect_args={"check_same_thread": False},
)
# WAL 模式:提升并发读性能
@event.listens_for(engine.sync_engine, "connect")
def set_wal_mode(dbapi_conn, _):
dbapi_conn.execute("PRAGMA journal_mode=WAL")
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
```
SQLite 无需连接池配置——`aiosqlite` 内部以串行化方式处理并发写。
{% else %}
### 连接字符串
```
# .env
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/{{ project_slug }}
```
### 引擎配置(db/engine.py
```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
engine = create_async_engine(
settings.database_url,
echo=settings.env == "dev",
pool_size=10,
max_overflow=20,
pool_pre_ping=True, # 连接健康检查,防止使用断开的连接
pool_recycle=3600, # 1 小时回收连接,防止 idle timeout
)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
```
### PostgreSQL 迁移注意事项
- 大表加列(NOT NULL)需提供服务端默认值,避免全表 lock
- 建索引用 `CREATE INDEX CONCURRENTLY`(非阻塞)
- 枚举类型变更需显式 `ALTER TYPE`Alembic autogenerate 不自动处理
{% endif %}
## Session 聚合根(domain/entities.py
`Session` 是领域核心实体,**纯 Python,零框架/IO 依赖**。业务规则(状态迁移、恢复判断)封装在实体方法内,不外泄到 service 层。
> 字段与状态为**示范**(订单域),按你的业务替换;状态机/恢复模式不变。
```python
from dataclasses import dataclass
from .value_objects import SessionStatus
@dataclass
class Session:
id: str
input_summary: str = "" # 输入摘要(示范)
work_dir: str = ""
status: SessionStatus = SessionStatus.IDLE
result_path: str | None = None
error_message: str | None = None
def mark_validating(self, result_path: str) -> None:
if self.status != SessionStatus.PROCESSING:
raise InvalidTransitionError(self.status, SessionStatus.VALIDATING)
self.result_path = result_path
self.status = SessionStatus.VALIDATING
def mark_finalizing(self) -> None:
self.status = SessionStatus.FINALIZING
def mark_done(self) -> None:
self.status = SessionStatus.DONE
def mark_error(self, message: str) -> None:
self.error_message = message
self.status = SessionStatus.ERROR
def can_recover(self) -> bool:
return self.status.can_recover()
```
`SessionStatus` 值对象(`domain/value_objects.py`):
```python
from enum import StrEnum
class SessionStatus(StrEnum):
IDLE = "idle"
INTAKE = "intake"
PROCESSING = "processing"
VALIDATING = "validating"
FINALIZING = "finalizing"
DONE = "done"
ERROR = "error"
def can_recover(self) -> bool:
return self in (self.VALIDATING, self.FINALIZING, self.DONE)
```
## SessionRecord ORM 映射(db/models.py
ORM 类仅做持久化映射,**不含任何业务逻辑**。
```python
class SessionRecord(Base):
__tablename__ = "sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
input_summary: Mapped[str] = mapped_column(String(500))
work_dir: Mapped[str] = mapped_column(String(500))
status: Mapped[str] = mapped_column(String(30), default="idle")
error_message: Mapped[str | None] = mapped_column(Text)
result_path: Mapped[str | None] = mapped_column(String(500))
checkpoint_at: Mapped[datetime | None] = mapped_column(DateTime)
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now())
completed_at: Mapped[datetime | None] = mapped_column(DateTime)
```
## SessionRepository 适配器(db/repositories.py
`SQLAlchemySessionRepository` 实现 `domain/ports.py` 的 `SessionRepository` 端口,负责 ORM ↔ 领域实体的互转。调用方(services)对 SQLAlchemy 零感知。
```python
from typing import override
from domain.ports import SessionRepository
from domain.entities import Session
from .models import SessionRecord
class SQLAlchemySessionRepository(SessionRepository):
def __init__(self, db: AsyncSession) -> None:
self._db = db
@override
async def get(self, session_id: str) -> Session | None:
record = await self._db.get(SessionRecord, session_id)
return self._to_entity(record) if record else None
@override
async def save(self, session: Session) -> None:
record = await self._db.get(SessionRecord, session.id)
record.status = session.status
record.result_path = session.result_path
record.error_message = session.error_message
record.checkpoint_at = func.now()
await self._db.flush()
@override
async def create(self, session: Session) -> None:
record = SessionRecord(
id=session.id, input_summary=session.input_summary,
work_dir=session.work_dir, status=session.status,
)
self._db.add(record)
await self._db.flush()
def _to_entity(self, r: SessionRecord) -> Session:
return Session(
id=r.id, input_summary=r.input_summary, work_dir=r.work_dir,
status=SessionStatus(r.status),
result_path=r.result_path,
error_message=r.error_message,
)
```
## 迁移工作流(Alembic
```bash
uv run alembic revision --autogenerate -m "描述变更"
uv run alembic upgrade head
uv run alembic downgrade -1
```
**规则:禁止直接修改已提交的迁移脚本。每次 model 变更必须生成新迁移脚本。**
## Checkpoint / 断点恢复
中间结果成功落盘是 checkpoint 分界线:
| 断连时状态 | 能否恢复 | 恢复行为 |
|-----------|---------|---------|
| `intake` / `processing` | ❌ | 推送 `checkpoint_lost`,提示用户重新开始 |
| `validating` | ✅ | 跳过 LLM,重跑校验 |
| `finalizing` | ✅ | 中间结果已在磁盘,重跑落地 |
| `done` | ✅ | 直接推送 `result_ready` |
## DB 依赖注入(core/deps.py
路由层通过 `get_session_repo` 获得 `SessionRepository` 实例,**不直接依赖 `get_db`**
```python
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
async def get_session_repo(db: AsyncSession = Depends(get_db)) -> SessionRepository:
return SQLAlchemySessionRepository(db)
```
## 会话保留(DB_RETENTION_DAYS
会话记录保留 `DB_RETENTION_DAYS`(默认 30 天),`lifespan` 定时任务软删→硬删过期记录。