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

8.1 KiB
Raw Blame History

架构与设计纪律

六边形 + 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)。 实现类必须显式继承 Protocolclass 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. 显式继承 Protocolclass MyImpl(MyProtocol): — 让 mypy 能校验实现完整性,而非依赖鸭子类型隐式对齐
  2. @override 标注每个实现方法:来自 from typing import overridePython 3.12)— 方法名拼错或接口改签名时,类型检查器立即报错,不等到运行时
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 managerwith / async with)或在 finally 确定性释放,禁手动 open/close 配对裸写。

    • 目的:连接被取消(WS 断开 → Task cancel)时资源仍确定性回收,杜绝僵尸进程与 fd 泄漏(落地细节见各 *-concurrency.md)。