
凌晨零点二十五分,我盯着刚部署的页面,问了一个很直接的问题:
「你真觉得这个设计做得好看吗?」
不好看。颜色不一致,间距随意,字体大小没有规律。这不是某个组件的问题,是整体设计语言的缺失。页面是在 OPC Loop 里自动生成的,每一轮 subagent 按自己的理解做决定,没有一个统一的设计约束在上面。
OPC 需要 design lint。但 design lint 不应该写死在 OPC 的核心代码里——不是所有项目都需要它。有的项目是后端 API,根本没有 UI。
这就引出了一个更大的问题:怎么让别人给 OPC 加功能,而不让他们改坏核心?
插件系统的诱惑
当你的框架开始有用户的时候,第一个冲动是做一个插件系统。WordPress 有插件,VS Code 有 extension,Chrome 有 add-on。看起来很成熟对吧?
但插件系统有一个本质问题:它要求插件知道宿主的名字。
第一版 OPC extension 就犯了这个错。extension 的配置里写着 meta.nodes = ["code-review", "acceptance"]——这意味着 extension 知道 OPC 有一个叫 code-review 的节点和一个叫 acceptance 的节点。如果我改了节点名字,或者加了一个新的 flow template,所有 extension 都得跟着改。
一个评审 agent 给这个设计打了 2/5。评语很尖锐:
这不是技术债大小的问题,是架构方向的对错。现在 3 个 node、3 个 extension 还凑合能跑;10 个 flow、5 个 extension 的时候就是 N×M 的耦合矩阵。
我第一反应是想辩解——不就改几个字符串吗?但心里知道它说得对。
能力合约
核心思路很简单:两边不直接认识对方,只认识一个共享的词汇表。(技术上这叫 structural typing——认结构不认名字,灵感来自 Linux capabilities 和 TypeScript 的类型系统。)
重构后的设计是这样的:
节点侧不再说「我是 code-review」,而是说「我需要 code-quality-check 和 visual-consistency-check 这两种能力」。
扩展侧不再说「我在 code-review 节点运行」,而是说「我提供 visual-consistency-check 这种能力」。
匹配逻辑:如果一个扩展提供的能力和当前节点需要的能力有交集,就触发。核心代码三行:
(技术细节,跳过不影响理解下文:)
const requires = new Set(ctx.nodeCapabilities ?? []);
const provides = ext.meta.provides ?? [];
const fires = provides.some(p => requires.has(p));
从此 extension 不认识任何 OPC 节点的名字。我明天加一个叫 post-launch-sim 的新节点,只要它声明需要 user-simulation 能力,所有提供这个能力的 extension 就会自动在上面运行。不用改一行 extension 代码。
这就像人体的关节。关节是骨骼的连接点——肌肉通过肌腱连到关节上,不是直接焊在骨头中间。如果你想给手臂加一块新肌肉,你连到关节上就行,不需要在骨头上钻洞。骨头保持完整,肌肉可以替换。
关节是扩展点,骨头中间不能挂肌肉。 这就是 OPC extension 系统的核心隐喻。
一个 Extension 长什么样
每个 extension 是一个目录,放在 ~/.opc/extensions/ 下:
~/.opc/extensions/
design-contract/
manifest.json ← 声明能力、hook 点、依赖
prompt.md ← 注入到 subagent 的 prompt 片段
gate-check.js ← gate 阶段的验证逻辑
这个结构的关键决策是:prompt 和代码分离。 prompt.md 是给 AI 看的(「你必须使用 4px 的间距基数,颜色使用这个 palette」),gate-check.js 是给机器执行的(截图比对、颜色提取、自动评分)。两者由同一个 extension 提供,但作用于 pipeline 的不同阶段——一个在建造前影响 AI 的行为,一个在验收时检查 AI 的产出。
能力名字是自由字符串——打错了 runtime 不会报错,只是静默不触发。解法不是加 runtime validation,而是一个 CAPABILITIES.md 注册表列出所有合法能力名字。治理靠 convention,不靠代码。
四个 Happy Path 碰不到的坑
v1 到 v2 的重构中还修了四个 bug:.git 目录被当 extension 加载(扫描逻辑没跳过 dotfile);hook 没有 timeout(一个 hang 住的 promise 卡死整个 harness);hook 返回错类型就 crash(没做 type guard);normalizeHook 有两份不同步的实现(开发测过的 hook 到用户那里炸了)。

四个坑有一个共同特点:它们不影响 happy path。 你自己开发的时候不会碰到。但用户会碰到每一个。而且他们不会告诉你——他们只是不再用了。一个框架的死法不是 bug,是「我懒得用了」。
为什么不做插件
在设计过程中,AI 做了一次有价值的反驳:
「是不是不设计 extension system 才是最好的?」
这个问题值得认真回答。如果每个增强都很小——一段 prompt 加一个 check 函数——把它们直接写进配置文件里,用 convention over configuration 管理,可能比一套 manifest + registry + lifecycle 更简单。
我最后还是坚持了 extension 架构。原因很现实:OPC 已经有了三年的使用预期,用户不只我一个人了。如果没有标准化的接口,每个人都往核心里塞自己的 hook,三个月之后核心代码会变成垃圾场。
但我也没有做一个完整的插件系统。没有插件商店,没有版本管理,没有依赖解析。这些都是「如果需要,未来再加」的东西。现在只需要一个目录、一个 manifest、一套能力合约。
40 个 unit test 证明这套系统能跑。2 个 smoke test 证明真实 flow 里 extension 能正确触发。不多,但足够让我睡好觉。
框架的骨骼长出来了。关节是扩展点,骨头中间不能挂肌肉。这条规则,从这个版本开始,写进了 OPC 的 DNA。
硅基团队 S1: AI 能写代码,凭什么信它? ← S1E03: 从「能跑」到「能信」 | S1E05: 睡觉的时候让 AI 替我干活 →