Twenty 应用扩展开发规范:以 Last Contact 为例拆解 UUID v4、dev:add 脚手架与布局陷阱
在 Twenty 平台上,任何自定义应用(App)都通过一组可同步的"实体"(Entity)与引擎协作。仓库中的 Last Contact AGENTS 文档 正是面向 AI 编码代理(Agent)的二十扩展开发规范,它浓缩了 yarn twenty dev:add 脚手架用法、实体目录约定、UUID v4 硬性约束与两类高频陷阱。本文以仓库内的 Last Contact 与 Postcard 富示例 应用为实证,逐条拆解这份规范背后的源码依据,帮助你(或你正在驱动的 Agent)在 Twenty 上高效、合规地开发 CRM 扩展应用。
一、AGENTS.md 的定位:给 Agent 的"扩展开发知识索引"
AGENTS.md 通常作为仓库级 Agent 指引存在,而这份文档把 Twenty App 开发者的完整知识体系分成七大类,每类都对应源码中的具体落点:
| 知识域 | 关注内容 | 仓库内对应实证 |
|---|---|---|
| Getting started | 快速上手、核心概念、项目结构、本地服务器、脚手架、故障排查 | Twenty SDK CLI 提供的 dev / dev:add / typecheck 命令族 |
| Config | 应用声明、角色、安装钩子、公共资源 | application-config.ts、default-role.ts |
| Data | 对象、扩展对象、关系 | src/fields/ 下的字段定义,如 last-contact-by.field.ts |
| Logic | 逻辑函数、技能与 Agent、连接 | src/logic-functions/,如 on-email-interaction.ts |
| Layout | 视图、导航项、页面布局、前端组件、命令菜单项 | Twenty 前端渲染器 与各应用的 src/views/、src/navigation-menu-items/ |
| Operations | CLI、测试、发布 | SDK 的 deploy / install / publish 命令及 src/__tests__/、src/**/__tests__/ 测试目录 |
| 富示例 | 完整可运行的参考应用 | postcard 示例(被文档直接推荐为 rich app example) |
对开发者的启示是:遇到不确定的实体定义方式时,优先查看本地 src/ 目录结构与已发布应用(public 目录下每个子目录就是一个真实 App)的既有写法,而不是凭空猜测。
二、UUID 硬性约束:所有实体 ID 必须是 UUID v4
文档明确要求:"All generated UUIDs must be valid UUID v4."。在 Twenty 的同步模型里,universalIdentifier 是应用实体跨环境稳定引用的主键——同一实体的字段、对象、视图都需要靠它完成与引擎元数据的映射,因此"生成合法 UUID v4"是硬性约束而非建议。
从 Last Contact 的实现可以反推这一规范为何被单列为一条:
- 应用、角色、各逻辑函数、各字段的
universalIdentifier全部集中在 universal-identifiers.ts 一个文件中,例如应用66a504cc-0a75-410e-a43f-cdeae1db1522、邮件交互函数87c8926b-15e3-43ed-a303-60f6d072f351。它们均为 36 位标准 v4 格式(含连字符,版本位为4)。 - 定义字段时,
defineField的universalIdentifier与relationTargetFieldMetadataUniversalIdentifier都要引用该常量文件,避免字符串散落造成不一致(见 last-contact-by.field.ts)。
手动复制 UUID 极易出错(多一个字符、非 v4、重复引用),这正是下一节 dev:add 会成为"最佳实践"的根本原因。
三、最佳实践:用 yarn twenty dev:add 生成实体
文档的核心建议是:创建新实体时优先使用 yarn twenty dev:add,让脚手架替你生成合法 ID 与规范模板。命令的实体清单及产物如下:
| 实体类型 | 命令 | 生成文件 |
|---|---|---|
| Object | yarn twenty dev:add object |
src/objects/<name>.ts |
| Field | yarn twenty dev:add field |
src/fields/<name>.ts |
| Logic function | yarn twenty dev:add logicFunction |
src/logic-functions/<name>.ts |
| Front component | yarn twenty dev:add frontComponent |
src/front-components/<name>.tsx |
| Role | yarn twenty dev:add role |
src/roles/<name>.ts |
| Skill | yarn twenty dev:add skill |
src/skills/<name>.ts |
| Agent | yarn twenty dev:add agent |
src/agents/<name>.ts |
| View | yarn twenty dev:add view |
src/views/<name>.ts |
| Navigation menu item | yarn twenty dev:add navigationMenuItem |
src/navigation-menu-items/<name>.ts |
| Page layout | yarn twenty dev:add pageLayout |
src/page-layouts/<name>.ts |
注意:命令实际支持的实体类型比表格更多。在 CLI 实现 add.ts 中,EntityAddCommand.execute 还覆盖了 viewField、pageLayoutTab、commandMenuItem、timelineActivityType 与 connectionProvider,并且生成目录统一取实体的 kebab-case 复数形式(src/${kebabCase(entity)}s/),命名风格与 Last Contact 的 src/fields/、src/logic-functions/ 完全一致。
3.1 脚手架自动生成 UUID v4
这是"省心"的关键来源:add.ts 的 getEntityData 分支中,Object 和 View 的 universalIdentifier 直接通过 v4()(uuid 包)现场生成:
- 创建 Object 时自动生成
objectUniversalIdentifier与nameFieldUniversalIdentifier两个 v4(代码约在 add.ts 的getObjectData返回后); - 创建 View 时需要传入
applicationUniversalIdentifier派生相关 ID。
也就是说,规范表里"Object / View"等实体的 ID 均由脚手架内置的 v4() 填充,天然满足"UUID v4"约束,开发者无需手抄 UUID。
3.2 Object 的配套实体联动生成
add.ts 的另一个亮点是 promptAndCreateObjectCompanions:创建 Object 后,命令行会询问是否同时生成配套的 View(all-<object>.ts)、Navigation menu item、Record page layout,三者的 universalIdentifier 同样用 v4() 分配。这与文档 "Common Pitfalls" 第一条(见下节)互相印证:Twenty 的对象默认就应配套视图、导航与记录页布局,避免产生"孤儿视图"。
3.3 交互式校验规则
脚手架大量使用 inquirer 交互,并对命名做了即时校验:
- Object 的名称单复数不可相同(如
company/companies); - Field 需要指定归属对象的
universalIdentifier,关系字段还额外要求目标对象/目标字段的 ID、关系类型(RelationType)与删除行为(RelationOnDeleteAction,默认CASCADE); - View 必须绑定到某个对象(
objectUniversalIdentifier,默认fill-later,需要后续回填)。
这些交互把文档中关于 Config / Data / Layout 的抽象规则,落地成了"输错就提示、缺参就占位"的可执行流程。
四、Common Pitfalls 详解:两条最容易被 Agent 踩中的坑
文档只列了两条高频陷阱,但每条背后都有明确的工程原因。
4.1 视图必须绑定导航项,否则会"裸奔"进左侧边栏
原文要点:"Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar."
即:如果你创建了一个 View,却没有同时创建与之关联的 Navigation menu item,该视图会被引擎直接暴露到左侧边栏,污染全局导航。这正是上一节对象脚手架要连带创建 navigation menu item 的原因——getNavigationMenuItemBaseFile 通过 targetObjectUniversalIdentifier 把导航项指向对象,View 才拥有正确的"入口"而非游离在侧边栏。
从数据关系看,Navigation menu item 与 View/Object 之间是强绑定:菜单项必须携带目标(对象或视图)的引用。因此规范建议在 navigation-menu-items 实体族中"先建目标、后建入口",或在创建对象时直接用配套生成避免遗漏。
4.2 前端组件必须自适应,而不是自带滚动条
原文要点:"Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab."
Twenty 的 Front component 会被嵌入固定宽高的 Widget 中渲染,其宿主是 twenty-front-component-renderer(提供 polyfills、host 与 remote 运行时)。若组件内部用 overflow: scroll 之类的方式"假装"容纳超量内容,而不是根据 widget 的固定尺寸做响应式布局,在常规 Tab 中就会表现为双滚动条或内容被裁切——唯一例外是明确用于 canvas tab 的场景。
从渲染架构可以推断,front component 运行在受限的 remote 沙箱里,高度、宽度由宿主注入。因此开发者应让内容自适应 widget height/width,把可滚动、可交互的复杂界面保留给 canvas tab。这条对"用 Agent 生成界面组件"尤其重要,因为 LLM 生成的组件最容易随手加一个外层 div + overflow: auto。
五、把规范落到 Last Contact:一份真实应用如何组织
Last Contact 应用是这份 AGENTS.md 的最佳注脚——它展示了规范描述的全部要素如何协同:
- 目录即实体类型:
src/fields/存放 20+ 个字段定义(last-contact-at、last-outbound-at、last-email、按对象 × 事件类型拆分的*-for-*-on-message/calendar-event等);src/logic-functions/存放事件与定时触发的处理函数;src/constants/统一管理所有 universal identifier。 - 字段定义契合 Config/Data 规范:last-contact-by.field.ts 展示了关系字段的完整写法:类型
RELATION、目标workspaceMember、反向字段 UUID、many-to-one、onDelete: SET_NULL、joinColumnName以及isUIEditable: false(防止用户手工编辑计算值)。 - 角色即最小权限:default-role.ts 遵循"最少权限"原则:对 person/company/opportunity 允许读写;对 message/messageParticipant/calendarEvent 及其参与者只读;
canReadAllObjectRecords、canBeAssignedToUsers/ApiKeys等全局开关全部为false。 - 逻辑函数双触发模型:邮件侧用数据库事件触发(
messageParticipant.updated且更新字段含personId,见 on-email-interaction.ts),日历侧则用定时任务 cron 触发(每*/N * * * *扫描刚结束的会议,见 on-calendar-event-started.ts),两者都在 60 秒超时内用CoreApiClient完成分页查询与回写。 - 测试与发布:每个函数/工具目录下都有
__tests__,外加 集成测试,支撑文档 Operations 域的 testing/publishing 章节。
六、写给 Agent 与开发者的执行清单
把 AGENTS.md 压缩成可执行的规则,推荐在 yarn twenty dev:add 工作流中遵守:
- 新增对象:
yarn twenty dev:add object,并接受"同时创建 View、导航项、记录页布局"的建议,避免孤儿视图。 - 新增字段:
yarn twenty dev:add field,关系字段必须一次性填对目标对象/目标字段的universalIdentifier与RelationType。 - 新增逻辑:
yarn twenty dev:add logicFunction,按需选择事件触发器或 cron 触发器,并把常量的 UUID 收口到src/constants/universal-identifiers.ts统一维护。 - 新增界面:
yarn twenty dev:add frontComponent,遵守"自适应固定 widget 尺寸、不内置滚动条"原则;普通 tab 与 canvas tab 的使用场景要分开设计。 - 权限边界:用
role显式声明每个对象记录的读写能力,默认关闭全局读写与人员/API Key 委派。 - 兜底校验:任何手写的
universalIdentifier都必须复核为合法 UUID v4,切勿从旧文件复制粘贴。 - 参考实现:复杂需求先参考 postcard 富示例 与 Last Contact 源码,确认 CLI 命令与既有写法后再动手。
这套流程同时是"喂给 AI Agent 的提示词模板"——当你在其他 Twenty 扩展仓库里维护代码时,直接把以上清单连同 AGENTS.md 原文交给编码代理,即可显著减少因 UUID 非法、视图缺导航、组件带滚动条导致的无效改动。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
