Resource 框架
Resource 框架是 Coffer 的核心抽象。理解它,是理解系统如何随着新管理实体类型的加入而优雅扩展的关键。
一切皆资源 kind
Coffer 中每一个由用户管理的实体都是一个 Resource(资源),由形如 <kind>:<name> 的稳定字符串标识。当前已注册六个 kind:
| Kind | 规范 | 说明 |
|---|---|---|
mcp_server | 001-mcp-gateway | 一个已注册的上游 MCP 服务器:传输配置、凭据引用,以及网关所需的各服务器策略。 |
agent | 004-agent-registry | 一个已注册的本地 AI 编码助手(如 Claude Code):其配置目录、Coffer-MCP 安装状态及派生的工作区切面。 |
skill | 005-skill-manager | 一个主技能包,Coffer 将其分发到一个或多个 agent 的技能目录中。 |
knowledge_base | 006-knowledge-base | 共享知识基底的 KB 面:任意格式上传 → markdown 真相 + grep / FTS5 / 向量检索。 |
memory | 007-memory | 同一基底的 memory 面:按事实的 markdown + 重新生成的 MEMORY.md,在各 agent 间共享。 |
channel | 009-channels | 一个消息通道绑定(Telegram、SeaTalk):传输配置、凭据引用和一个默认 agent。 |
knowledge_base 和 memory 是同一个知识基底的两个面——磁盘上的 markdown 文件是真相之源,SQLite 是可重建的索引(ADR-012)。新的 kind 接入同一个框架,无需对框架本身做任何修改。加密凭据存储和多机同步是刻意设计的跨切面关注点,而非 kind:它们服务于每一个 kind,本身并不是被管理的实体。
框架提供四件事,且仅此四件:
| 关注点 | 框架做什么 |
|---|---|
| 身份 (Identity) | 为每个资源分配 kind、name,以及复合稳定引用 <kind>:<name>。 |
| 生命周期 (Lifecycle) | 定义并强制执行资源可处于的状态:registered、enabled、disabled、deleted。 |
| 审计 (Audit) | 为每一次生命周期变更记录时间戳和操作者(cli、api、ui、system)。 |
| 模式校验 (Schema validation) | 在 application 层边界分发每个 kind 对应的 Pydantic schema 校验,而分发机制本身与 kind 无关。 |
框架不做什么
Resource 框架不统一调用语义。每个 kind 自行定义其能力的使用方式。没有通用的 invoke() 方法,没有共享调用路径,没有跨 kind 行为。框架描述的是资源如何被注册、如何被自描述、如何被治理——而不是使用它时会发生什么。
为什么提前设计 kind 无关框架(ADR-001)
章程通常将跨层公共抽象推迟到第二个 feature 也需要它时才抽取(「在第二个 feature 时抽取」)。Resource 框架是一个明确的例外,原因在于成本不对称。
框架横跨每一层:domain 实体、数据库 schema、审计表、保留框架、接口面路由(REST API 子路由、CLI 子命令组)。如果这个抽象在第一个规范中被设计为 mcp_server 的专用实现,然后等第二个 kind 到来时再抽取,重构成本将不是简单的模块移动——它需要同时重建审计表、接口面路由和保留框架。「第二个 feature」时的重构将是一次重大且高风险的迁移,而非干净的模块分离。
构建各 kind 独立孤岛、没有共享抽象的替代方案同样被拒绝:已经有多个 kind 在高置信度的规划中(当前已注册六个),为每个 kind 分别构建身份 + 生命周期 + 审计 + 接口面 CRUD,将产生更多代码和更大的漂移风险。
结果是第一个规范(001-mcp-gateway)在只有一个具体 kind 的情况下承担了框架的抽象开销。这被接受为已知的成本,明确以避免未来重构为收益来平衡。
标识符格式:<kind>:<name>(ADR-003)
资源在外部以 <kind>:<name> 字符串引用:
- CLI:
coffer resource show mcp_server:filesystem - REST API URL:
/api/v1/resources/mcp_server/filesystem - 配置引用:agent 的
tools字段中列出mcp_server:filesystem
格式是自描述的:前缀告诉你应该查询哪个 kind,在许多代码路径中省去一次额外查询。它对人类可读,调试时无需解码器。
内部,数据库使用代理主键 id INTEGER PRIMARY KEY AUTOINCREMENT 用于 join 和外键,UNIQUE (kind, name) 约束强制外部标识符的唯一性。领域层的值对象 ResourceRef(kind: str, name: str) 在 application 层边界处理解析与序列化——原始字符串永远不进入 domain 层。
被拒绝的替代方案:
- 完整 URN(
urn:coffer:mcp_server:filesystem):被拒绝,因为 Coffer 是单用户本地优先的——不存在需要区分的另一个 Coffer 实例,urn:coffer:前缀是纯粹的形式主义。 - 纯 UUID:被拒绝,因为不透明、不自描述,且强迫每个引用都携带单独的
kind字段。 - 路径风格(
mcp_server/filesystem):功能上等价,但被拒绝,因为斜杠在 URL、文件路径和许多 DSL 中已经过载;冒号使 kind 命名空间关系更清晰直观。
能力状态模型(ADR-004)
每个资源在一个明确定义的生命周期中流转。状态机有意保持简单:
registered(已注册) — 资源已存在于数据库中并附带其配置,尚未被明确启用或禁用。
enabled(已启用) — 资源处于活跃状态。对于 mcp_server,这意味着守护进程会连接到它并向 MCP 客户端暴露其工具。
disabled(已禁用) — 资源配置被保留,但守护进程不会连接到它或暴露其工具。禁用是非破坏性的:重新启用后无需重新注册即可恢复。
deleted(已删除) — 资源被移除,这是一个终态。以相同名称重新添加服务器将是一次全新注册。
每一次状态转换都被记录到审计日志中,并附带操作者信息。审计日志无法通过普通 API 修改或删除——它是只追加的。
对于 mcp_server,还存在并行的能力级别状态:上游服务器暴露的每个单独工具、资源或提示都可以单独启用或禁用(见 ADR-004)。数据库只存储这些能力的用户偏好标志——不缓存能力 schema 或描述。这些内容在每次请求时从上游实时获取,以每会话内存缓存的形式保存,TTL 为 60 秒。
Kind 在组装入口处注册
框架不使用全局注册表,也不依赖 import 副作用。每个 kind 在组装入口处通过 KindModule dataclass 显式装配。组装入口是 surfaces/http/app.py(FastAPI 装配)和 surfaces/cli/main.py(Typer 装配)。
添加新 kind 是机械性工作:在每一层创建 kind 的子目录(domain/<kind>/、application/<kind>/、infrastructure/<kind>/、surfaces/http/<kind>/、surfaces/cli/<kind>/),实现 kind 特定的逻辑,然后在组装入口注册一个 KindModule。审计、保留和资源列表接口面将自动继承。
为什么「一切皆资源 kind」(ADR-007)
信息架构遵循与领域模型相同的原则:存在单一轴向的导航模型,每一个面向用户的被管理实体都是一个资源 kind,通过同一个侧边栏分组呈现。不存在与 resource 概念并列的独立「surface」概念。
这消除了每个未来规范都需要回答的问题:「这个新东西是 kind 还是 surface?」运维工具——可观测性、设置——出现在单独的 System 分组中;用户管理的一切都出现在 Resources 分组中。
由此产生一条刻意的策略:不设「即将推出」占位符。一个 kind 在 UI 或侧边栏中不显示,直到它真正可用。UI 始终应被读解为「这是 Coffer 现在能做的」,而不是「这是 Coffer 计划要做的」。
参见: ADR-001:Resource 框架提前设计,ADR-007:一切皆资源 kind,ADR-003:资源标识符格式,ADR-004:能力状态模型