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 运行。
This commit is contained in:
@@ -4,7 +4,7 @@ paths: ["backend/**"]
|
||||
|
||||
# backend-db
|
||||
|
||||
数据库({% if database == 'sqlite' %}SQLite{% else %}PostgreSQL{% endif %} + SQLAlchemy async)、Session 模型、Alembic 迁移、Checkpoint 断点恢复与会话保留。
|
||||
数据库({% if database == 'sqlite' %}SQLite{% else %}PostgreSQL{% endif %} + SQLAlchemy async)、Session 模型、Alembic 迁移、Checkpoint 断点恢复与会话保留。下文实体字段以「订单」作示范。
|
||||
|
||||
## 数据库配置
|
||||
|
||||
@@ -72,6 +72,8 @@ AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
`Session` 是领域核心实体,**纯 Python,零框架/IO 依赖**。业务规则(状态迁移、恢复判断)封装在实体方法内,不外泄到 service 层。
|
||||
|
||||
> 字段与状态为**示范**(订单域),按你的业务替换;状态机/恢复模式不变。
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from .value_objects import SessionStatus
|
||||
@@ -79,25 +81,20 @@ from .value_objects import SessionStatus
|
||||
@dataclass
|
||||
class Session:
|
||||
id: str
|
||||
topic: str
|
||||
language: str = "中文"
|
||||
style: str | None = None
|
||||
image_source: str | None = None
|
||||
input_summary: str = "" # 输入摘要(示范)
|
||||
work_dir: str = ""
|
||||
status: SessionStatus = SessionStatus.IDLE
|
||||
html_path: str | None = None
|
||||
pptx_path: str | None = None
|
||||
result_path: str | None = None
|
||||
error_message: str | None = None
|
||||
|
||||
def mark_screenshotting(self, html_path: str) -> None:
|
||||
if self.status != SessionStatus.GENERATING:
|
||||
raise InvalidTransitionError(self.status, SessionStatus.SCREENSHOTTING)
|
||||
self.html_path = html_path
|
||||
self.status = SessionStatus.SCREENSHOTTING
|
||||
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_synthesizing(self, pptx_path: str) -> None:
|
||||
self.pptx_path = pptx_path
|
||||
self.status = SessionStatus.SYNTHESIZING
|
||||
def mark_finalizing(self) -> None:
|
||||
self.status = SessionStatus.FINALIZING
|
||||
|
||||
def mark_done(self) -> None:
|
||||
self.status = SessionStatus.DONE
|
||||
@@ -116,16 +113,16 @@ class Session:
|
||||
from enum import StrEnum
|
||||
|
||||
class SessionStatus(StrEnum):
|
||||
IDLE = "idle"
|
||||
DIALOG = "dialog"
|
||||
GENERATING = "generating"
|
||||
SCREENSHOTTING = "screenshotting"
|
||||
SYNTHESIZING = "synthesizing"
|
||||
DONE = "done"
|
||||
ERROR = "error"
|
||||
IDLE = "idle"
|
||||
INTAKE = "intake"
|
||||
PROCESSING = "processing"
|
||||
VALIDATING = "validating"
|
||||
FINALIZING = "finalizing"
|
||||
DONE = "done"
|
||||
ERROR = "error"
|
||||
|
||||
def can_recover(self) -> bool:
|
||||
return self in (self.SCREENSHOTTING, self.SYNTHESIZING, self.DONE)
|
||||
return self in (self.VALIDATING, self.FINALIZING, self.DONE)
|
||||
```
|
||||
|
||||
## SessionRecord ORM 映射(db/models.py)
|
||||
@@ -137,15 +134,11 @@ class SessionRecord(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
topic: Mapped[str] = mapped_column(String(500))
|
||||
style: Mapped[str | None] = mapped_column(String(100))
|
||||
language: Mapped[str] = mapped_column(String(20), default="中文")
|
||||
image_source: Mapped[str | None] = mapped_column(String(50))
|
||||
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)
|
||||
html_path: Mapped[str | None] = mapped_column(String(500))
|
||||
pptx_path: Mapped[str | None] = mapped_column(String(500))
|
||||
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())
|
||||
@@ -175,8 +168,7 @@ class SQLAlchemySessionRepository(SessionRepository):
|
||||
async def save(self, session: Session) -> None:
|
||||
record = await self._db.get(SessionRecord, session.id)
|
||||
record.status = session.status
|
||||
record.html_path = session.html_path
|
||||
record.pptx_path = session.pptx_path
|
||||
record.result_path = session.result_path
|
||||
record.error_message = session.error_message
|
||||
record.checkpoint_at = func.now()
|
||||
await self._db.flush()
|
||||
@@ -184,8 +176,7 @@ class SQLAlchemySessionRepository(SessionRepository):
|
||||
@override
|
||||
async def create(self, session: Session) -> None:
|
||||
record = SessionRecord(
|
||||
id=session.id, topic=session.topic, language=session.language,
|
||||
style=session.style, image_source=session.image_source,
|
||||
id=session.id, input_summary=session.input_summary,
|
||||
work_dir=session.work_dir, status=session.status,
|
||||
)
|
||||
self._db.add(record)
|
||||
@@ -193,10 +184,9 @@ class SQLAlchemySessionRepository(SessionRepository):
|
||||
|
||||
def _to_entity(self, r: SessionRecord) -> Session:
|
||||
return Session(
|
||||
id=r.id, topic=r.topic, language=r.language,
|
||||
style=r.style, image_source=r.image_source, work_dir=r.work_dir,
|
||||
id=r.id, input_summary=r.input_summary, work_dir=r.work_dir,
|
||||
status=SessionStatus(r.status),
|
||||
html_path=r.html_path, pptx_path=r.pptx_path,
|
||||
result_path=r.result_path,
|
||||
error_message=r.error_message,
|
||||
)
|
||||
```
|
||||
@@ -213,14 +203,14 @@ uv run alembic downgrade -1
|
||||
|
||||
## Checkpoint / 断点恢复
|
||||
|
||||
`slides.html` 成功落盘是 checkpoint 分界线:
|
||||
中间结果成功落盘是 checkpoint 分界线:
|
||||
|
||||
| 断连时状态 | 能否恢复 | 恢复行为 |
|
||||
|-----------|---------|---------|
|
||||
| `dialog` / `generating` | ❌ | 推送 `checkpoint_lost`,提示用户重新开始 |
|
||||
| `screenshotting` | ✅ | 跳过 LLM,重跑截图 |
|
||||
| `synthesizing` | ✅ | 截图已在磁盘,重跑合成 |
|
||||
| `done` | ✅ | 直接推送 `html_ready` + `pptx_ready` |
|
||||
| `intake` / `processing` | ❌ | 推送 `checkpoint_lost`,提示用户重新开始 |
|
||||
| `validating` | ✅ | 跳过 LLM,重跑校验 |
|
||||
| `finalizing` | ✅ | 中间结果已在磁盘,重跑落地 |
|
||||
| `done` | ✅ | 直接推送 `result_ready` |
|
||||
|
||||
## DB 依赖注入(core/deps.py)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user