Files
scaffold/template/.claude/rules/design-discipline.md
T
baozaotumao 39fe248f9f feat: 重组为标准 skill 包结构 + 修复 verify.sh bash 3.2 兼容
- 新增 .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 入库
2026-06-01 21:19:45 -07:00

123 lines
8.1 KiB
Markdown
Raw 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.
# 架构与设计纪律
> 六边形 + 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`)。