39fe248f9f
- 新增 .claude/skills/new-project/SKILL.md(标准 skill 格式) - install-skill.sh 安装时组装 SKILL.md + verify.sh,单一源无重复 - verify.sh 改用索引数组替代 declare -A,兼容 macOS bash 3.2 - log_pass/log_skip 显式 return 0,避免 set -e 下非 verbose 误退出 - README 改为 skill 优先;补全 copier.yml/scripts/template 入库
123 lines
8.1 KiB
Markdown
123 lines
8.1 KiB
Markdown
# 架构与设计纪律
|
||
|
||
> 六边形 + SOLID + 契约式设计(DbC)+ 高内聚低耦合 + 扩展点机制概述。
|
||
|
||
整体遵循**六边形架构(Ports & Adapters)+ 整洁架构**;类/函数级遵循 **SOLID** 与**契约式设计(Design by Contract)**。
|
||
|
||
## 六边形架构(Ports & Adapters)
|
||
|
||
- **端口(Port)**:领域定义的接口(`LLMSession` / `ImageProvider` / `Exporter` / `StorageBackend`),表达"领域需要什么能力"
|
||
- **适配器(Adapter)**:端口的具体实现(当前 LLM CLI 适配器 / `pptx_exporter` …),位于最外层,可替换
|
||
- 依赖方向恒指向领域;替换外层实现不触动内层
|
||
|
||
## SOLID(类/模块级强制)
|
||
|
||
| 原则 | 约束 |
|
||
|------|------|
|
||
| S 单一职责 | 一个类/函数仅一个变更理由 |
|
||
| O 开闭 | 扩展靠加实现,不改既有代码(即扩展点机制) |
|
||
| L 里氏替换 | 任一端口实现可无差别替换,不破坏调用方契约 |
|
||
| I 接口隔离 | 端口按消费者拆分,不强加无关方法 |
|
||
| D 依赖倒置 | 依赖抽象(Port),不依赖具体(Adapter) |
|
||
|
||
## 契约式设计(Design by Contract)——编码前先定契约
|
||
|
||
**任何函数/类:先定义契约(签名 + 前置条件 + 后置条件 + 不变量),再写测试,再写实现。**
|
||
|
||
- 前置条件(Precondition):调用方须保证的输入约束
|
||
- 后置条件(Postcondition):函数承诺的输出与副作用
|
||
- 不变量(Invariant):对象生命周期内恒真的状态约束
|
||
- 契约落于类型签名 + docstring;关键不变量以断言运行时校验
|
||
- 公开接口的契约即其规格,**契约稳定性优先于实现自由**
|
||
|
||
## 核心设计原则(高内聚 / 低耦合)
|
||
|
||
上述纪律落到日常的可操作规则。**所有新功能从设计之初就遵循,不允许"先实现再重构"。**
|
||
|
||
1. **单一职责 + 高内聚**
|
||
每个模块/类/函数只做一件事。相关逻辑聚在一起(如 PTY 机制全在 `llm/pty_bridge.py`),不相关的拆开。判据是**职责**而非行数:一个函数做了两类事就该拆;文件超 ~300 行是「该审视是否多职责」的信号,不是硬上限——单一职责的长文件不必为凑行数而拆。
|
||
|
||
2. **依赖方向(低耦合的硬规则)**
|
||
依赖只能单向流动,禁止反向或跨层依赖(详见 `architecture.md`)。上层依赖下层,下层绝不反向 import 上层;`utils/` 是谁都可依赖的叶子层,自身不依赖任何业务模块。
|
||
|
||
3. **依赖抽象,不依赖实现(DI)**
|
||
业务代码依赖**接口(Protocol/ABC)**,不依赖具体类。具体实现在边界处注入。
|
||
范例:DAG 节点依赖 `LLMSession` 端口,不知道背后是哪个 CLI 厂商(端口见 `agent/domain/ports.py`)。
|
||
实现类**必须显式继承 Protocol**(`class MyImpl(MyProtocol):`)并用 `@override` 标注每个实现方法——见下「Protocol 实现强制写法」。
|
||
|
||
4. **稳定契约解耦服务**
|
||
三层之间只通过**消息契约**通信(WebSocket JSON 协议),不共享内部类型。契约是单一来源(见 `communication-protocol.md`),任一服务内部重构不影响其他服务。
|
||
|
||
5. **新功能 = 插到扩展点,不改核心**
|
||
加新能力时,优先实现已有扩展点接口;若没有合适接口,**先定义新接口**再实现,禁止往核心流程里塞 if/else 分支。
|
||
|
||
## 扩展点机制概述(插件接缝)
|
||
|
||
系统预留以下接口,保证可扩展性。新增同类能力 = 新增一个实现,**不改调用方**。端口统一声明于 `agent/domain/ports.py`(六边形架构:端口属领域核心);适配器实现散于对应目录。
|
||
|
||
| 扩展点(端口) | 当前适配器 | 适配器位置 | 未来扩展 |
|
||
|--------|---------|---------|---------|
|
||
| `LLMSession` | 当前 CLI 厂商适配器 | `agent/llm/` | 其他 CLI / API SDK |
|
||
| `ImageProvider` | unsplash / pexels / none | `agent/providers/` | 自建图库 / AI 生图 |
|
||
| `Exporter` | `pptx_exporter` | `agent/export/` | pdf_exporter / 长图 |
|
||
| `StorageBackend` | `local_storage`(本地 FS) | `agent/storage/` | S3 / OSS |
|
||
|
||
每个端口都是 `Protocol`,新适配器注册到工厂(`registry`)即生效,调用方按配置选择。具体端口签名与注册规范见 `agent.md`。
|
||
|
||
### Protocol 实现的强制写法(Python 3.12+)
|
||
|
||
实现端口时,**必须同时满足两条约束**,缺一不可:
|
||
|
||
1. **显式继承 Protocol**:`class MyImpl(MyProtocol):` — 让 mypy 能校验实现完整性,而非依赖鸭子类型隐式对齐
|
||
2. **`@override` 标注每个实现方法**:来自 `from typing import override`(Python 3.12)— 方法名拼错或接口改签名时,类型检查器立即报错,不等到运行时
|
||
|
||
```python
|
||
from typing import Protocol, override
|
||
|
||
# ✅ 强制写法
|
||
class GeminiCliSession(LLMSession): # 显式继承,非隐式鸭子类型
|
||
@override
|
||
async def complete(self, prompt: str) -> str:
|
||
result = await self._read_until_done()
|
||
return result or ""
|
||
|
||
# ❌ 禁止:隐式对齐,接口改名或拼写错误时静默脱钩
|
||
class BadSession:
|
||
async def complet(self, prompt: str) -> str: # 拼错了,运行时才 crash
|
||
...
|
||
```
|
||
|
||
违规信号(Claude Code 应直接指出):
|
||
- 实现类未继承 Protocol → mypy 无法验证实现完整性
|
||
- 实现方法缺少 `@override` → 接口改名后实现类静默脱钩,直到运行时才发现
|
||
|
||
## 可靠性纪律(Resilience,强制)
|
||
|
||
核心链路全是易抖动的外部依赖(LLM CLI 进程 / Chromium 截图 / 图床 HTTP),故跨边界调用必须假定**会超时、会瞬时失败、会被重复触发**。以下与「命令幂等」(见 `communication-protocol.md`)同属可靠性约束,从设计之初遵循,不允许"先实现再加兜底"。
|
||
|
||
1. **超时无处不在(Ubiquitous Timeout)**
|
||
任何跨进程/网络/IO 的等待**必须设显式超时**,禁止裸 `await` 无界等待。
|
||
- 外部调用统一用 `asyncio.timeout(...)` 或客户端原生 timeout 包裹;超时即抛领域错误(如 `GEMINI_TIMEOUT`),不静默挂起。
|
||
- 超时值经 `.env` 注入、不硬编码(见 `config.md`);每类外部依赖一个超时配置项。
|
||
- 反例:`await proc.read()` 无超时 → 进程卡死则会话永久僵死。
|
||
|
||
2. **重试 + 指数退避 + 抖动(Retry / Backoff + Jitter)**
|
||
对**幂等**操作的瞬时失败(网络抖动、图床 5xx、截图偶发失败)才允许重试;非幂等操作禁止盲目重试。
|
||
- 重试须有**上限**(次数 + 总时长双封顶),退避用指数 + 随机抖动,禁固定间隔猛刷。
|
||
- 重试逻辑收拢为一处可复用策略(如 `utils/retry.py`),不在各调用点散写循环。
|
||
- 用户输入触发的命令依赖幂等键去重(见 `communication-protocol.md`),保证重试安全。
|
||
|
||
3. **降级而非整体失败(Graceful Degradation)**
|
||
单个可选能力失败时,**降级到退化态**而非让整条生成链崩溃。
|
||
- 经端口的退化实现承接:`ImageProvider` 失败/超额 → 降级到 `none`(无图)继续生成,而非中断 Deck。
|
||
- 降级须**可观测**:记日志 + 必要时以事件告知前端(区别于 `error` 致命错误)。
|
||
- 哪些能力可降级、降级到何种退化态,在端口契约里显式声明。
|
||
|
||
4. **边界校验,内部信任(Validate at Boundary)**
|
||
不可信输入(前端命令、上传 `.md`、外部响应)在**适配器/路由边界一次性校验**(Pydantic / 类型 / 业务规则),校验失败回 `INVALID_INPUT`;越过边界进入领域后**信任已校验**,领域内不重复防御性判空。
|
||
- 校验是边界职责,不是领域职责——领域用前置条件断言不变量,而非兜外部脏数据。
|
||
|
||
5. **资源管理强制(RAII / Context Manager)**
|
||
持有外部资源(子进程 / pty fd / 浏览器页 / DB 连接 / 文件句柄)**必须用 context manager**(`with` / `async with`)或在 `finally` 确定性释放,禁手动 `open`/`close` 配对裸写。
|
||
- 目的:连接被取消(WS 断开 → Task cancel)时资源仍确定性回收,杜绝僵尸进程与 fd 泄漏(落地细节见各 `*-concurrency.md`)。
|