2026-09-23·by Sijie Wang#standmeet#architecture#design

backend-domain-modules

后端领域模块——按领域组织

上级: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 导入 usecasesAgentToolConnectorErrMailNotConfigured)——一个领域为了自己的概念反过来向上伸手,因为这些概念在该领域里没有家。
  • 带类型的 category 表面: contract.CalendarProxy 是一个编译期 Go interface——这正是 capabilities 的外部化原则禁止 kernel 持有的那种"带类型的 handle"。

原则

每个领域 = 一个自包含的模块 internal/<domain>/,拥有 entity + usecase + repo + public facade。共享层只留给真正无所属领域的东西。Controller 存在于 internal/routes/*(真正的入站层);一个模块自己的"内部 controller"其实就是它的 public facade。

两条插件轴——同一个元结构

capabilitiesconnector同一个抽象,区别只在调用约定上:

{ declaration (data) → implementation → instance → ONE opaque call door }
  • declaration = 一份存在 internal/ 之外数据清单(backend/connectors/<id>/manifest.yamlbackend/capabilities/<id>/manifest.yaml,来自磁盘或由 owner 注册)——category+verbs / capability+tools 加上 schema。永远不是 Go interface,永远不在 internal/ 里。 calendarVerbs/mailVerbs 那些字面量已经没了,但截至 2026-09-07,contract.CalendarProxy / MailProxyinternal/connector/contract/contract.go)仍然是消费方面向编程的带类型 category 表面——这是还没迁移、需要提取成数据的那一块。
  • implementation = 满足某个 declaration 的 adapter/plugin。
  • instance = 运行时的、有作用域的、可调用的东西。作用域按轴而不同:connector 是 owner 持久化的(账号 + 凭证);capability 是 session 临时的(冷启动 sandbox)。
  • 每条轴一扇不透明的门——按 name 索引、不透明 JSON,被调用方永远看不到调用方。

每条轴两个 registry

capreg 是一个declaration registry(类型)——它对应的是 connector 的 spec store不是 HubHub 是 connector 的instance registry,对应的是 per-session 的 capability 绑定。

declaration registry(类型)instance registry(运行时)
capabilitycapreg — 内置 plugin + owner 注册的 MCP / 已安装的 skill每个 session 冷启动的 sandbox
connectorconnector spec store — 内置项 + owner 上传的 specHub(owner 的账号 + 凭证)

Owner:两条轴各有一条类型路径和一条实例路径

注册一个类型创建一个instance
connectorPOST /connectors(+/validate-spec)→ Hub.Upsert 上传的 spec/credentials + /connect + /activate
capabilityPOST /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/search Meili 子包——是 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.bookcorpus.retrievalsummarize_conversationask_visitormail.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/sandboxwscapsocket 住在 capabilities 下;configcmd/server/configsearchcorpus——它是 corpus 形状的 DAO(硬编码 corpus_notes 索引),不是通用 infra;mailerconnectorpromptsownerjobregistrystats。)规则: 具体的、按领域的数据访问(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.shinternal/ 严格圈定为 {上述 8 个核心模块 · capabilities · routes · infra} —— 它的 ALLOWED 列表,十一个名字,没有 usecases;任何其他子目录都算 lint 失败,而那份只收紧的基线文件已经删掉(纯红)。任何没有出现在类图里的目录(比如 plugins)都不许再回来。

会消失的东西

  • 三个按层组织的上帝包(usecases/domain/postgres)——被重新分配进各个模块。
  • contract.CalendarProxy / 任何带类型的 category 表面——declaration 变成数据
  • connector → usecases 这条反向依赖——概念搬回自己的家。
  • 改名:capreg → capability 的 declaration registry;Hubconnection(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 探路

  1. 已完成: connector.invoke controller → internal/routes/connector(瘦壳),已上架构锁。
  2. Declaration → 数据(不再把 contract.CalendarProxy 当作那个类型)—— 截至 2026-09-07 未做internal/connector/contract/contract.go 里还定义着它)。
  3. 随结构自然完成(2026-07-27): usecases 已不存在,反向依赖也就不可能存在。
  4. 拆分 registry:spec store(类型)与 Hub(instance → connection)分开 —— 未做hub.go 仍然同时 upsert 启动时和 owner 上传的 spec。
  5. 已完成(2026-07-27): 已按领域复制完毕;三个上帝包已拆解,每个切片先转绿再动下一个。最后剩下的残留:usecases/obsidiancorpus/obsidianusecases/report_*conversation/usecaseplugins/bookerowner/{entity,usecase}plugins/ownercoreowner/ownercoreConnector 是唯一还没拆的核心模块——上面第 2-4 步就是尚待完成的工作。

子模块是它们自己的节点。 owner/jobscorpus/obsidianconversation/inference 保留自己的边界(有自己的入口点,不是所属领域的 DDD 内脏);owner/ownercore 在被拆解之前也是其中之一(f35e82c04,2026-08-02 —— 这个名字还留在 backend/tools/archcheck/main.go:46 的子模块集合里,无害,背后已没有目录)。check-domain-facade-boundarycheck-domain-acyclic 都把这同一组东西当作独立节点处理——否则一个合理地横跨多个领域的聚合器(ownercore 曾触达每个领域的 facade)就会给它仅仅是挨着的那个 core 伪造出一条环。每个领域的core 仍然必须是一个干净的节点,acyclic 这道关卡在遇到真正的 core-to-core 环时依然会亮红。

相关:structure · capabilities · connector · key-designs

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

backend-domain-modules