首页
/ twenty-partners 架构深度解析:在 Twenty CRM 之上构建合作伙伴计划运营系统

twenty-partners 架构深度解析:在 Twenty CRM 之上构建合作伙伴计划运营系统

2026-09-07 12:24:01作者:滕妙奇

本仓库的 packages/twenty-apps/internal/twenty-partners 是一个真实运行的内部 Twenty 应用,它把 Twenty CRM 变成 Twenty 合作伙伴计划的“操作系统”:录入符合资格的商机(partner-eligible deal)、将其匹配给经过审核的市场合作伙伴、并端到端跟踪整条匹配流水线。本文以该应用的 CLAUDE.md 与由其引入的 AGENTS.md 为主体骨架,结合源码目录、常量与测试文件,系统讲解其业务模型、匹配状态机、自动匹配机制、纵向切片架构与依赖规则,帮助读者掌握“如何用 Twenty SDK 组织一个规模化内部业务应用”。

从一份只有一行内容的 CLAUDE.md 说起

twenty-partners/CLAUDE.md 的全文只有一行:

@AGENTS.md

这是本仓库典型的“代理指令文件”组织方式:仓库级的 CLAUDE.md 约定全体开发者通用的规范(kebab-case 文件命名、具名导出、禁用 any、用 types 而非 interface、简短 // 注释等),而应用级的 AGENTS.md 只补充 partners 特有的架构约束。因此要真正理解这个应用,需要以 AGENTS.md 与 README.md 为入口展开。

这个应用解决什么问题

按 README 的定位,twenty-partners 的目标是:把 CRM 变成 Twenty 合作伙伴计划的运营系统,具体承担三件事——

  1. 录入(intake):接收符合条件的商机/客户需求;
  2. 匹配(matching):把商机与经过审核(vetted)的市场合作伙伴配对;
  3. 跟踪(tracking):端到端跟踪从匹配到交付再到赢单/流失的完整管线。

整个应用与 twenty-apps 下的示例应用一样,构建在 Twenty SDK 之上(应用级配置文件见 src/application-config.ts),数据模型、业务逻辑、权限、界面导航都以“声明 + 逻辑函数”的形式声明在 SDK 中,而非传统的手写 CRUD。

核心数据模型:Partner 对象与 Opportunity 扩展

README 概括了应用的两类核心模型:

Partner(合作伙伴)对象承载合作伙伴画像的关键字段:

  • slug(唯一标识字符串)
  • status(合作状态)
  • availability(可承接新单的可用性)
  • served geographies(服务的地理区域)
  • languages spoken(使用的语言)
  • deployment expertise(部署专长)
  • Calendly 链接(用于预约/转介绍)
  • last-match timestamp(最近一次被匹配的时间戳,自动匹配的依据之一)

在源码结构中,这一对象的声明位于 src/modules/partner/objects、字段声明位于 src/modules/partner/fields,而字段的 UUID 常量集中在 partner-field-universal-identifiers.ts,选项值常量集中在 partner-option-values.constant.ts。此外 partner-country-view-groups.ts 这类文件用于按国家/地区分组的视图。

Opportunity(商机)扩展字段则描述一笔潜在合作的状态推进:

  • 匹配状态(match status)
  • 设计文档状态(design-doc status,例如 Listed 商机在 /twenty-lead-brief 中会写入 Design Doc URL)
  • 初次介绍(intro)与再次联系(relance)的时间戳
  • 与已匹配 Partner 的关系字段(relation)

关于商机所属的销售阶段,可在 opportunity-stage-options.ts 中看到完整定义:在 Twenty CRM 标准的 NEW → SCREENING → MEETING → PROPOSAL → CUSTOMER 五段销售漏斗之上,追加了归档用的 DONE(绿)与 DEAD(灰)两档;每个选项携带固定的 UUID、label、position 与颜色,并导出类型 OpportunityStageValue。文件中还导出了阶段字段的 universal identifier 20202020-6f76-477d-8551-28cd65b2b4b9,这类以 20202020- 开头的 UUID 正是 Twenty 生态中跨模块引用对象的“通用标识符”约定。

匹配状态流水线:matchStatus 生命周期

README 给出了商机生命周期中 matchStatus 的状态机,这是理解应用“怎么走流程”的核心:

状态 含义
TO_BE_MATCHED 默认态 —— 商机已录入,等待分配
MANUAL_MATCH 需要人工挑选合作伙伴
AUTO_MATCH 触发自动分配合作伙伴
MATCHED 已分配合作伙伴
INTRODUCED_TO_A_PARTNER 已发送客户介绍
WORKING_WITH_A_PARTNER 合作已启动,处于推进中
IMPLEMENTING 正在实施交付
WON 商机赢单
RECONNECT_LATER 暂停 —— 未来再联系
LOST 商机流失

可以看到它是一条“录入 → 分配 → 介绍 → 协作 → 赢单/流失”的推进链,同时也保留 RECONNECT_LATER 这类非线性的旁路状态,便于运营把暂时谈不拢的机会搁置而非丢失。

自动匹配与人工兜底

README 对自动匹配的描述是:当商机被置为 auto-match 时,系统会分配**闲置时间最长(longest-idle)且当前可用(available)**的合作伙伴,并把商机标记为 MATCHED;如果当时没有任何可用合作伙伴,则商机转入人工匹配流程(MANUAL_MATCH),并附带一条说明性备注解释原因。

与之对应的实现散落在 src/modules/opportunity/matching 域内,从文件名可以清晰还原其触发点与处理链:

  • on-opportunity-listed.logic-function.ts / on-opportunity-listed.test.ts——商机被“上架(Listed)”时的响应;
  • on-opportunity-partner-assigned.logic-function.ts 与其 .integration-test.ts——合作伙伴被分配后的响应;
  • on-opportunity-intro-sent.logic-function.ts / on-opportunity-intro-sent.test.ts——客户介绍已发送;
  • on-opportunity-partner-won.logic-function.ts / on-opportunity-partner-won.test.ts——赢单处理;
  • services/ 下的业务逻辑(如 notify-listed-brief.service.tspropagate-partner-user.service.tssync-application-outcomes.service.ts)承担真正可单测的逻辑。

即:UI 或自动化触发的是一个薄薄的 *.logic-function.ts,真正干活的是 service,这正是下文“依赖规则”所要求的形态。

视图:把流水线“看到”侧边栏

为了支撑运营,应用提供多组视图并在侧边栏暴露入口:

  • 等待匹配队列(waiting-for-match queue):TO_BE_MATCHED/MANUAL_MATCH 的商机待办;
  • 匹配漏斗总览(matching funnel overview):按匹配状态分组的漏斗统计;
  • 合作伙伴索引(partner index):全部 Partner 的可检索清单;
  • 已匹配商机日志(log of matched deals):历史匹配记录。

这些视图的声明集中在 src/modules/opportunity/viewssrc/modules/partner/directory,导航入口则在各模块的 navigation-menu-items/page-layouts/ 中声明。

目录结构:纵向切片(vertical-slice)模块化

AGENTS.md 记录了本应用最重要的组织决定:所有领域代码都下沉到 src/modules/ 下,一个领域一个文件夹;根目录 src/ 只保留六类不可移动的东西:

src/
  application-config.ts          # 应用 + 应用级变量 —— 不变
  default-role.ts                # 不变
  constants/universal-identifiers.ts   # ⚠️ 禁止移动(见“不变量”)
  scripts/                       # ⚠️ 禁止移动 —— seed/purge/rls/slugify 等脚本
  roles/                         # 应用级 RLS —— 保持在根(横跨所有对象)
  skills/                        # 打包随附的 Claude skills —— 不变
  workflows/                     # 打包随附的工作流 —— 不变
  __tests__/helpers/             # 共享测试设施(client、fixtures、cleanup)
  modules/
    shared/                      # 跨领域:secret-guard(http/)、find-or-create、
                                 #   paginate、utils、front-components、跨域导航目录
    <domain>/                    # partner · application · opportunity · …
      <name>.logic-function.ts   # 薄的 SDK 入口 —— 位于领域/功能根,不放子目录
      objects/ fields/ view-fields/ views/ navigation-menu-items/ page-layouts/
      constants/                 # 值列表 + 声明 UUID 映射(不是 types)
      services/                  # 业务逻辑(可测试)
      graphql/{queries,mutations}/   # 唯一允许存放裸 query/mutation 的地方
      connector/                 # 出站第三方 API,每个 API 一个目录
      mappers/  utils/  types/  front-components/

对照实际代码目录,当前 src/modules/ 下确实只有四个领域目录:partner/opportunity/application/shared/。其中:

  • partner/ 再按特性拆分为 self-service(合作伙伴自助维护资料)、marketplace(市场侧)、application-intake(申请录入与审核)、directory(合作伙伴名录)、onboarding(入驻)等;
  • opportunity/ 拆分为 intake(商机录入)、matching(匹配处理)、how-to-process(操作手册页面)、constantsfieldsviewspage-layoutsnavigation-menu-items 等;
  • application/ 这类单一功能的小领域则可以直接保持 <domain>/<primitive>/ 的扁平形态,不强制加 <feature>/ 层。

设计意图很明确:一个领域是“自包含切片”,它的声明(objects/fields/views…)和它的逻辑(logic-functions → services → graphql/connector)放在一起。新增功能等于新增一个文件夹,而不是把文件撒进十个扁平目录。

依赖规则:唯一需要严格遵守的架构红线

AGENTS.md 里最有分量的一句话是“the dependency rule(the one that matters —— never import upward)”,依赖方向被严格限定为单向:

logic-functions → services → { graphql, connector, mappers } → shared · utils · types

各层职责与禁则是:

  • 入口层(*.logic-function.ts,位于领域/功能根):只做 SDK 入口。解析入参 → 调用一个 service → 返回。不允许承载业务逻辑、不允许直接写裸 GraphQL、不允许发外部 HTTP。如果某个 logic-function 超过约 40 行,说明它越俎代庖干了 service 的活,应当抽取。
  • services/:全部业务逻辑所在。要求可测试、与 SDK/传输层解耦,并且不得 import 任何 logic-function
  • graphql/:唯一允许裸 query/mutation 存在的目录,且要求使用具名、带类型的操作,禁止在业务代码里内联 client.query(...)
  • connector/:唯一允许调用出站第三方 API 的地方,每个第三方 API 一个目录(<name>.connector.ts / config.ts / types.ts)。需要特别辨析:入站 webhook 不属于 connector——应用接收的 webhook 只是受 modules/shared 中 secret-guard 工具保护的普通 logic-function 入口。AGENTS.md 明确说明,当前真正的出站 connector 只有 Discord;TFT、client-brief、partner-application 都属于入站。
  • 禁止领域之间横向 import 逻辑:跨域共享一律走 modules/shared/。(唯一例外:关系字段天然引用对方对象的 ID 常量,这属于 schema 引用而非逻辑依赖,跨模块 import 是允许的。)

从代码看,opportunity/intake 下的 import-opportunity-from-tft.logic-function.tssubmit-client-brief.logic-function.ts 即为典型的薄入口;围绕它们各自配了单元测试(.test.ts)与集成测试(.integration-test.ts),入口背后是 graphql/mappers/services/ 三个支撑目录。

命名与测试规范

  • 命名<name>.<primitive>.ts,例如 submit-application.logic-function.tsresolve-candidacy.service.tspartner.object.ts。所有文件一律 kebab-case,即使 React 组件也不允许 PascalCase(如 profile-picture-upload.tsx)。
  • 测试与被测代码同目录放置,并按“种类”拆分
    • <name>.test.ts = 单元测试(mock client、快速、无需基础设施);
    • <name>.integration-test.ts = 真实 workspace 的集成测试(需要 live server + 全局 setup,由 tsconfig 排除出普通编译)。
  • 二者的区分通过 vitest 的 project 机制选择(--project unit),而不是按文件数量硬性规定;规范建议用一个 vitest.config.ts 挂两个 project。禁止把原本 mock 的单元测试改名成 .integration-test.ts
  • 共享测试设施集中在 src/tests/helpers。

例如 submit-client-brief.test.tssubmit-client-brief.integration-test.ts 同处一个目录,分别覆盖 mock 与真实 workspace 两种场景;matching 域内也能看到 on-opportunity-intro-sent.test.tson-opportunity-partner-assigned.integration-test.ts 这类按种类命名的对。

移动代码时必须守住的“不变量”

由于 Twenty SDK 按 universalIdentifier(而非文件路径)跟踪对象,重构/迁移时以下约束一旦被破坏会造成对象重复注册或失联:

  1. 一组 universalIdentifier UUID 在移动过程中必须逐字节保持一致——纯 git mv 是安全的,改了或删了 UUID 就等于重新注册或孤儿化一个对象。
  2. 禁止移动 src/constants/universal-identifiers.ts——env-toolkit 按路径逐 bundle 重写该文件。
  3. 禁止移动 src/scripts/*——package.json 按路径调用它们。
  4. 所有 import 该文件的人必须继续指向根文件,禁止把其中的 UUID 导出搬进某个模块。
  5. 禁止跨领域用悬空 ID import 拆分关系——成对的关系字段需要互相导出/导入对方的 field-ID 常量,要么把关系双方放在同一个领域,要么把共享 ID 提升到 shared/
  6. 迁移时每个 commit 只搬一个领域(只允许 git mv,import 修正必须在同一 commit 内完成)。

这些不变量解释了为什么根目录布局“锁死”的部分那么少但那么关键。

操作手册(playbook)与行为“锁步”维护

AGENTS.md 反复强调一个工程实践:凡是写到页面上的操作手册,必须与真实行为保持锁步(lockstep),改行为时必须同一次变更里更新手册,不允许在 twenty.com 或笔记里另存一份副本。

申请审核手册(Application review playbook)

位于 src/modules/partner/application-intake/(front-component + page layout + nav item)。当以下任一内容变更时,须在同一变更里更新该手册:

  • 落到 Partner 的官网表单字段;
  • Partner Applications 视图(filter、columns、sort);
  • 用作审核结论的 validationStage 取值;
  • 审核人员需要知晓的录入副作用——创建时发 Discord 通知;VALIDATED 时发欢迎邮件(邮件由 WELCOME_EMAIL_WORKFLOW_URL 指向的工作流驱动);
  • 市场排名加成规则,即 packages/twenty-website/src/partners-marketplace/completeness-score.ts 中关于 case study、cover、introduction、service、picture、calendar、rate、category、ghost rule 的加分项。

页面上的“事实”一律来自视图和录入逻辑所用的同一组常量,避免出现过期步骤。

商机处理手册(Opportunity process playbooks)

How to process(面向匹配管理员)与 How to apply(面向合作伙伴工作区)位于 src/modules/opportunity/how-to-process/。同样要求与真实操作行为锁步,变更以下任一环节时须同步更新手册及其文案测试:

  • 录入(Intake):marketplace brief 在“已上架”后发 Discord;Twenty Internal 不公开列出;/twenty-lead-brief 写入 Design Doc URL;
  • 上架(Listing)Listed 开关翻转 → Discord 通知、每日摘要、Open Briefs 成员资格;
  • 介绍(Intro):设置 Intro Sent At 会关闭 Listed;邀请制申请(Invited applications);
  • 赢单/关闭(Win):设置 Partner → Won/Declined;Backup(备选合作伙伴)保留;取消分配会把 Won 重新打开为 Applied;Deals 阶段按 New → Screening → Meeting → Proposal → Customer 推进;
  • 申请(Apply):Open Briefs 仅对已上架商机开放申请;有最低 pitch 要求;同一商机只能投一份申请;说明各状态含义;
  • 打包技能(Bundled skills)twenty-lead-brieftwenty-partner-shortlisttwenty-partner-intro 三者的本地运行方式与预期输出(可在 src/skills 下找到它们的声明);
  • 通过 MCP 匹配:操作员可以让 Agent 直接写 CRM 变更来完成匹配。

页面用“操作员语言”书写;How to process 允许出现技能名、斜杠触发词与打包技能的仓库路径;How to apply 必须包含 Open Briefs 与 My Applications 链接。规范特别提醒:不要从独立页面去打开 Apply——Apply 固定在 brief 记录上。

新代码该放哪:一张决策表

AGENTS.md 以极简的“Where does new code go?”收尾,可直接当作写代码时的决策索引:

要写的代码 放哪
外部 API 调用 connector/
裸 query/mutation graphql/
任何业务逻辑 services/
新的 SDK 触发器/函数 领域/功能根下的薄 *.logic-function.ts,只调 service
跨领域共享 modules/shared/

小结:一份面向 Agent 协作的“活架构文档”

twenty-partners 的 CLAUDE.md 用一个 @AGENTS.md 引用把文档组织成了“仓库级通用规范 + 应用级架构”两层,而 AGENTS.md 则以纵向切片、单向依赖、具名测试、UUID 不变量和“手册与行为锁步”几条铁律,把这个 300+ 文件的内部应用约束得清晰可维护。对读者而言,这套组织方式本身就是一份值得借鉴的模板:当业务应用规模超过“demo”量级时,如何用领域目录收敛声明与逻辑、用依赖箭头防止架构腐化、用不变量保护 SDK 的标识符契约——而这一切仍然运行在 Twenty CRM 之上,共享同一套对象、权限(见 src/roles)与视图体系,让 CRM 真正成为业务运营的“操作系统”。

登录后查看全文
热门项目推荐
相关项目推荐