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:
baozaotumao
2026-06-01 23:15:05 -07:00
parent bb6503c3f8
commit df48c36422
23 changed files with 257 additions and 285 deletions
@@ -5,11 +5,11 @@ paths:
# Agent 并发模型与资源管理
事件驱动的异步 I/O、进程池下放 CPU 密集任务、并发上限背压、浏览器复用、生命周期取消、工作目录清理与日志规范。
事件驱动的异步 I/O、进程池下放 CPU 密集任务、并发上限背压、昂贵外部资源复用、生命周期取消、工作目录清理与日志规范。
## 并发模型与资源管理
Agent 是最重的并发场景:单进程 FastAPI(单事件循环)要同时服务多个会话,每个会话持有{% if llm_provider == 'gemini-cli' %}一个 LLM CLI 子进程 + {% endif %}一个 Chromium 上下文 + 阶段性 CPU 密集合成。**核心铁律:绝不阻塞事件循环。**
Agent 是最重的并发场景:单进程 FastAPI(单事件循环)要同时服务多个会话,每个会话持有{% if llm_provider == 'gemini-cli' %}一个 LLM CLI 子进程 + {% endif %}若干外部资源 + 阶段性 CPU 密集任务。**核心铁律:绝不阻塞事件循环。**
### 工作类型与处理方式
@@ -19,7 +19,7 @@ Agent 是最重的并发场景:单进程 FastAPI(单事件循环)要同时
{% if llm_provider == 'gemini-cli' -%}
| 阻塞 fd 读 | PTY master_fd 读取 | **非阻塞 fd + `loop.add_reader`**,不要 thread-per-session |
{% endif -%}
| CPU 密集 | python-pptx 合成、图像处理 | `ProcessPoolExecutor` via `run_in_executor` |
| CPU 密集 | 文档/图像处理、加解密、压缩等 | `ProcessPoolExecutor` via `run_in_executor` |
{% if llm_provider != 'gemini-cli' -%}
| LLM 流式 | SDK `async for token in stream` | 直接 `await`,事件驱动 |
{% endif %}
@@ -47,19 +47,19 @@ def on_pty_readable():
### 1. CPU 密集:进程池
{% endif %}
`synthesize` 节点(python-pptx 合成)是纯 CPU + 持 GIL,会卡住整个事件循环 1–3 秒,影响所有其他会话。必须下放到进程池:
CPU 密集节点(如文档合成、图像处理)是纯 CPU + 持 GIL,会卡住整个事件循环 1–3 秒,影响所有其他会话。必须下放到进程池:
```python
process_pool = ProcessPoolExecutor(max_workers=os.cpu_count())
await loop.run_in_executor(process_pool, build_pptx, screenshots_dir, pptx_path)
await loop.run_in_executor(process_pool, build_output, input_dir, out_path)
```
进程池函数必须是**纯函数**(参数 + 返回值可序列化),禁止访问共享状态。
### {% if llm_provider == 'gemini-cli' %}3{% else %}2{% endif %}. 并发上限与背压
每个会话 = {% if llm_provider == 'gemini-cli' %}1 个 CLI 进程 + {% endif %}1 个 Chromium context,内存/CPU 开销大。用全局信号量封顶,超出排队并向前端反馈位置:
每个会话 = {% if llm_provider == 'gemini-cli' %}1 个 CLI 进程 + {% endif %}若干外部资源,内存/CPU 开销大。用全局信号量封顶,超出排队并向前端反馈位置:
```python
session_semaphore = asyncio.Semaphore(settings.max_concurrent_sessions)
@@ -69,21 +69,20 @@ async with session_semaphore:
# 排队期间向前端推 {"type": "queued", "position": N}
```
### {% if llm_provider == 'gemini-cli' %}4{% else %}3{% endif %}. Playwright 浏览器复用
### {% if llm_provider == 'gemini-cli' %}4{% else %}3{% endif %}. 昂贵外部资源复用
启动时拉起**一个** Chromium,每个会话开独立 `browser_context`(廉价),用完关 context,不要每次 `launch()`
若某外部资源初始化昂贵(如浏览器、连接池、客户端),启动时拉起**一个**全局实例,每个会话借用廉价的子句柄,用完即还,不要每次重新 `launch()` / 建连
```python
# lifespan 启动
app.state.browser = await playwright.chromium.launch()
# lifespan 启动:全局单例(示范用浏览器,按需替换为连接池等)
app.state.pool = await create_expensive_resource()
# 截图节点
context = await app.state.browser.new_context(viewport={"width": 1920, "height": 1080})
# 会话节点:借一个廉价子句柄
handle = await app.state.pool.acquire()
try:
page = await context.new_page()
...
finally:
await context.close()
await handle.close()
```
### {% if llm_provider == 'gemini-cli' %}5{% else %}4{% endif %}. 资源生命周期与取消
@@ -100,7 +99,7 @@ finally:
{% else %}
await llm_session.close() # 释放 HTTP 连接
{% endif %}
await context.close()
await handle.close() # 归还借用的外部资源句柄
```
### 共享状态线程安全