后端领域模块——按领域组织
上级:structure · 仓库文档:
docs/design/backend-domain-modules.md
后端曾经(到 2026-07-27 为止)是按层打包的,而不是按领域打包的。这是一整类纠缠问题背后的根本技术债(比如 connector 为了自己的概念去导入 capabilities 的上帝包;一个 kernel 本不该持有的带类型 category 表面)。本节点定下的目标是:按领域打包——每个领域拥有自己完整的纵向切片,只对外暴露一个公共 facade。
病灶——三个按层切分的上帝包(已于 2026-07-27 治愈)
internal/usecases (106) ← every domain's usecase together [dissolved]
internal/domain ( 55) ← every domain's entity together [dissolved]
internal/postgres ( 66) ← every domain's repo together [dissolved]
三者已全部消失,连同 internal/plugins(第四个这样的桶,装的是 owner 侧的 capability 代码)一起。internal/ 现在恰好只剩类图上的那一组:8 个核心模块 + capabilities + routes + infra。这道 fence 以一个空基线运行——纯红,没有任何历史豁免。
- 反向依赖:
connector/slots.go导入usecases(AgentToolConnector、ErrMailNotConfigured)——一个领域为了自己的概念反过来向上伸手,因为这些概念在该领域里没有家。 - 带类型的 category 表面:
contract.CalendarProxy是一个编译期 Go interface——这正是 capabilities 的外部化原则禁止 kernel 持有的那种"带类型的 handle"。
原则
每个领域 = 一个自包含的模块 internal/<domain>/,拥有 entity + usecase + repo + public facade。共享层只留给真正无所属领域的东西。Controller 只存在于 internal/routes/*(真正的入站层);一个模块自己的"内部 controller"其实就是它的 public facade。
两条插件轴——同一个元结构
capabilities 和 connector 是同一个抽象,区别只在调用约定上:
{ declaration (data) → implementation → instance → ONE opaque call door }
- declaration = 一份存在
internal/之外的数据清单(backend/connectors/<id>/manifest.yaml和backend/capabilities/<id>/manifest.yaml,来自磁盘或由 owner 注册)——category+verbs / capability+tools 加上 schema。永远不是 Go interface,永远不在internal/里。calendarVerbs/mailVerbs那些字面量已经没了,但截至 2026-09-07,contract.CalendarProxy/MailProxy(internal/connector/contract/contract.go)仍然是消费方面向编程的带类型 category 表面——这是还没迁移、需要提取成数据的那一块。 - implementation = 满足某个 declaration 的 adapter/plugin。
- instance = 运行时的、有作用域的、可调用的东西。作用域按轴而不同:connector 是 owner 持久化的(账号 + 凭证);capability 是 session 临时的(冷启动 sandbox)。
- 每条轴一扇不透明的门——按 name 索引、不透明 JSON,被调用方永远看不到调用方。
每条轴两个 registry
capreg 是一个declaration registry(类型)——它对应的是 connector 的 spec store,不是 Hub。Hub 是 connector 的instance registry,对应的是 per-session 的 capability 绑定。
| declaration registry(类型) | instance registry(运行时) | |
|---|---|---|
| capability | capreg — 内置 plugin + owner 注册的 MCP / 已安装的 skill | 每个 session 冷启动的 sandbox |
| connector | connector spec store — 内置项 + owner 上传的 spec | Hub(owner 的账号 + 凭证) |
Owner:两条轴各有一条类型路径和一条实例路径
| 注册一个类型 | 创建一个instance | |
|---|---|---|
| connector | POST /connectors(+/validate-spec)→ Hub.Upsert 上传的 spec | /credentials + /connect + /activate |
| capability | POST /mcp-servers · marketplace 的 InstallSkill | 每个 session 的 sandbox spawn(+ enable 门控) |
两扇门以及它们的交界处
Capabilities 位于 connectors 之上;一个 capability instance 通过 connector.invoke 向下触达。connector 从不向 capreg 索取任何东西。唯一的跨轴边就是connector-deps。
为什么用 category: 自托管 → 每个 owner 自带自己的日历/邮箱。一个 capability 是针对一个category(可替换的接口)编程的;owner 绑定当前生效的 provider。参见 connector。
领域清单
核心模块(internal/<domain>/,各自拥有 entity+usecase+repo+facade):
- corpus — raw/wiki/output/writing/note/tree/citation/subjectivity/crosslink/seo(+ 自己的
corpus/searchMeili 子包——是 corpus 形状的 DAO,不是 infra) - conversation — chat/dialog/message/ghost/visitor-session(+ inference 作为 agent-core 引擎)
- connector — connection/integration/mail_connector/connectorsvc + adapters(registry/door → platform 轴)
- access — access_code/access_request/role/role_snapshot/dock_buttons/api_key(+ session)
- owner — owner/account/instance/page_content/microsite(由 custom_page 改名,e7fe80e91,2026-09-05;+
microsite_store,每个 microsite 自己的持久化存储,a94909aa9,2026-09-04)/appearance/keypair/login/password/recovery + mail(mail_otp/outbound)+ prompts +owner/jobs(求职闭环) - security — captcha/banned_ip/login-guard/anti-replay(认证=access;防护=security)
- marketplace — marketplace/skill/mcp_server
- stats — stats_activity/growth/jobs / inference_usage / system_info
Capability 轴(internal/capabilities/)——standmeet 自己的 agent 加载/派发 MCP capability 的机制。子包:capreg(declaration registry)· capsocket(sandbox 里的 cap 用来回调 host 的 socket)· mcpclient/mcpplugin/mcputil(我们用来拨号连接 owner 注册的外部 MCP server 的client传输层)· capstore(每个 plugin 隔离的存储)· capconfig(plugin 声明的 owner 可调字段,cc5c1db47,2026-07-31)· capquota(按码的用量上限,在 plugin 自己的存储里计数,aab8abe90,2026-08-01)· sandbox(跑 owner skill 脚本的 docker run runner)· sandboxws(MCP 沙箱的 bwrap 工作区)。只是一个通用加载器——零具体 capability:每一个具体的 MCP(booker/retrieval/summarize/mail-sender/ask-visitor/……)都被外部化了(见下文),所以光读 capabilities 这一层代码,你根本看不出它们的存在(由 core-agnostic ratchet 强制保证)。它不是 connector(那条轴持有 owner 的凭证并触达外部服务),也不是入站的 MCP-server facade(routes/mcphandle,外部 agent 从这里触达我们的工具)。ghost 不是一个 capability——它是 conversation 的核心部分。
完全外部化的 capability(sandbox 化,不属于 core,住在顶层的 mcp-servers/ 里):booker · retrieval · summarize · ask-visitor · mail-sender —— 五个,正好对上 backend/capabilities/<id>/manifest.yaml 的五份声明(calendar.book、corpus.retrieval、summarize_conversation、ask_visitor、mail.send)。没有 report 这个 server,从来也没有过(git log -- mcp-servers/report 为空);报告是 summarize 产出的 chat_reports 产物。Owner 侧还在进程内的受信任 cap 只剩 owner/jobs(求职闭环);ownercore 已经没了(f35e82c04,2026-08-02 —— 它的 cap 变成了各领域自己声明的 op,见 owner-facade-from-registry)。
共享 infra(无所属领域,internal/infra/,截至 2026-09-07): pgstore(pgxpool + 启动时套迁移,bd45353f4,2026-08-31)· cryptobox · httpx · retry · storage · gotenberg · session · middleware · apierr · hostop · periodic · paritymanifest · facadeparity · providermodels · mailthrottle(按收件人的出站节流,9d6d10d95,2026-09-06)· snowflake(短 id,dbb803288,2026-09-06)· buildnotify · clientaddr · depcheck · plaintext · selfstat · textcut。(sandbox/sandboxws 和 capsocket 住在 capabilities 下;config 是 cmd/server/config;search→corpus——它是 corpus 形状的 DAO(硬编码 corpus_notes 索引),不是通用 infra;mailer→connector;prompts→owner;jobregistry→stats。)规则: 具体的、按领域的数据访问(SQL/索引)属于该领域自己的 repository;infra 只保留真正无所属领域的底座(pgx pool、http client、crypto box),绝不能是某个领域的行形状定制的查询/索引。
平台机制: capability 轴(上文的 capabilities)+ connector registry · paritymanifest · facadeparity。(plugins 这个进程内加载器已经拆解 —— fea7ae93f,2026-07-29:它的 Plugin/Registry 机制并入了 capabilities,它的实现并入了 owner/mcp-servers。)
结构上强制执行: infra/scripts/check-internal-dirs.sh 把 internal/ 严格圈定为 {上述 8 个核心模块 · capabilities · routes · infra} —— 它的 ALLOWED 列表,十一个名字,没有 usecases;任何其他子目录都算 lint 失败,而那份只收紧的基线文件已经删掉(纯红)。任何没有出现在类图里的目录(比如 plugins)都不许再回来。
会消失的东西
- 三个按层组织的上帝包(usecases/domain/postgres)——被重新分配进各个模块。
contract.CalendarProxy/ 任何带类型的 category 表面——declaration 变成数据。connector → usecases这条反向依赖——概念搬回自己的家。- 改名:
capreg→ capability 的 declaration registry;Hub→ connection(instance) registry;socket-op handler →routes/<domain>/里的 controller。
外部化不等于搬家
一个 capability 只有在host 一点逻辑都不再保留时,才算真正被外部化。把 host 那份代码挪到 internal/ 里一个更整洁的地址,能通过每一道结构性关卡,却什么也没改变——这些关卡度量的是形状,而一个语义上的重复品完全可以拥有合法的形状。
booker 就是这样一个案例:mcp-servers/booker/policy.go 和 kernel 里的 booking-policy 求值器,是同一套规则的两份实现(同样的冲突 token、同样的时段常量),每个文件的头部注释都断言归属权在对方那里。根因是一个机制上的缺口——一个 sandbox 化的 capability 只能面向 visitor,所以它面向 owner 的那部分表面不得不在 host 侧重新实现一遍。修复方式是 mcpplugin.Manifest.OwnerTools:把 owner 工具做成 declaration 数据,在调用时才拨号进 sandbox。
这个重复品带来的不只是漂移风险:只有 host 二进制导入了 time/tzdata,所以一旦这个求值器跑在 sandbox 里,每一个具名的 IANA 时区都会失败,list_slots 就会返回一个空列表——和"没有可用时段"完全无法区分。一个重复品会把"到底哪一份代码携带着算法所依赖的环境"这件事藏起来。两份代码的错误约定也不一样(isError vs {ok:false,error,detail}),所以外部化这一步实际上改变了面向 owner 的契约。
已还清(f614c08f3,2026-07-31): 取消这一簇(uc_booking_cancel.go / uc_booking_cancel_own.go)曾重复了 sandbox 里的 deleteBooking;唯一的差别只在查找方式(booking-id 还是 conversation+event-id)。owner 作用域的 calendar_cancel_booking 工具现在住在 mcp-servers/booker/main.go,卡片早已不用的 REST 取消路径被退役,host 侧那两个 usecase 都删了。
迁移——connector 探路
- 已完成:
connector.invokecontroller →internal/routes/connector(瘦壳),已上架构锁。 - Declaration → 数据(不再把
contract.CalendarProxy当作那个类型)—— 截至 2026-09-07 未做(internal/connector/contract/contract.go里还定义着它)。 - 随结构自然完成(2026-07-27):
usecases已不存在,反向依赖也就不可能存在。 - 拆分 registry:spec store(类型)与
Hub(instance → connection)分开 —— 未做;hub.go仍然同时 upsert 启动时和 owner 上传的 spec。 - 已完成(2026-07-27): 已按领域复制完毕;三个上帝包已拆解,每个切片先转绿再动下一个。最后剩下的残留:
usecases/obsidian→corpus/obsidian、usecases/report_*→conversation/usecase、plugins/booker→owner/{entity,usecase}、plugins/ownercore→owner/ownercore。Connector 是唯一还没拆的核心模块——上面第 2-4 步就是尚待完成的工作。
子模块是它们自己的节点。 owner/jobs、corpus/obsidian 和 conversation/inference 保留自己的边界(有自己的入口点,不是所属领域的 DDD 内脏);owner/ownercore 在被拆解之前也是其中之一(f35e82c04,2026-08-02 —— 这个名字还留在 backend/tools/archcheck/main.go:46 的子模块集合里,无害,背后已没有目录)。check-domain-facade-boundary 和 check-domain-acyclic 都把这同一组东西当作独立节点处理——否则一个合理地横跨多个领域的聚合器(ownercore 曾触达每个领域的 facade)就会给它仅仅是挨着的那个 core 伪造出一条环。每个领域的core 仍然必须是一个干净的节点,acyclic 这道关卡在遇到真正的 core-to-core 环时依然会亮红。
相关:structure · capabilities · connector · key-designs。