Twenty 应用开发 Agent 指南:UUID v4 约束与视图、前端组件两大常见陷阱
本文以 Twenty 仓库中 Postcard 示例应用附带的 Agent 指导文档 packages/twenty-apps/examples/postcard/LLMS.md 为主体,逐条拆解其中提出的三条开发约束:universalIdentifier 必须为 UUID v4、View 必须关联 NavigationMenuItem、Front Component 应响应固定 Widget 尺寸。结合 twenty-sdk 的 manifest 校验源码与 Postcard 示例的真实实体文件,你可以获得一套可直接复制的实践清单,用于编写符合 Twenty App SDK 构建规范的扩展应用。
什么是 LLMS.md:写给编码 Agent 的应用开发守则
在 packages/twenty-apps/examples/postcard/ 目录下,除 README.md 外还并列存在三份内容完全相同的指导文件:LLMS.md、AGENT.md 和 CLAUDE.md。它们面向的是各类 LLM 编码助手(LLM 通用提示词、通用 Agent 约定、Claude 专属提示词是同一份内容的三个分发入口)。
Postcard 应用本身是 Twenty 官方维护的"富示例应用",用于演示 Twenty SDK 的全部实体类型,README.md 以表格列出了每类实体对应的目录:
| 实体 | 文件 | 演示内容 |
|---|---|---|
| Application | src/application.config.ts |
应用元数据、应用变量、服务端变量 |
| Objects | src/objects/ |
内联字段与关联表的自定义对象 |
| Fields | src/fields/ |
独立字段、关系字段(ONE_TO_MANY / MANY_TO_MANY)、扩展标准对象 |
| Logic Functions | src/logic-functions/ |
HTTP 路由、数据库事件触发器、定时任务、安装钩子 |
| Front Components | src/components/ |
渲染进 Twenty UI 的 React 组件 |
| Roles / Views / Navigation / Skills / Agents / Page Layouts | 对应目录 | 权限角色、表格视图、侧边栏导航、AI 技能与智能体、自定义记录页布局 |
LLMS.md 正是站在"给这个示例应用做二次开发的 Agent"视角,补充了三块官方文档之外的高频踩坑经验,全文分为三节:基础文档指引(Base documentation)、UUID 约束(UUID requirement)、常见陷阱(Common Pitfalls)。下面逐一展开。
基础文档指引与本地运行方式
原文档的 Base documentation 一节指向 Twenty 官方应用的 Getting Started 文档与仓库中的 rich-app 示例(packages/twenty-apps/fixtures/rich-app)。在仓库内,rich-app 的本地路径为 packages/twenty-apps/fixtures/rich-app,它是比 Postcard 更"完整"的参考实现,两份文档互为对照:Postcard 演示实体覆盖面,rich-app 演示工程组织方式。
验证本文所有约束的最直接方式,是在 Postcard 目录下跑起本地开发:
# 从 packages/twenty-apps/examples/postcard 目录
yarn install
yarn twenty dev
twenty dev 来自 Twenty SDK 提供的 CLI,它会在构建/同步过程中对应用 manifest 做校验——这正是下一节 UUID 约束落地的位置。
UUID 约束:所有生成的 universalIdentifier 必须是 UUID v4
LLMS.md 的原文只有一行:
All generated UUIDs must be valid UUID v4.(所有生成的 UUID 必须是合法的 UUID v4。)
这条约束之所以被单独立项强调,是因为 Twenty App 的每一个实体都以 universalIdentifier 作为跨工作区(workspace)稳定的唯一标识,而 SDK 在构建期会硬校验这一格式,不合法直接报错阻断构建。
源码级证据:manifest 校验如何强制 UUID v4
校验逻辑位于 SDK 的 manifest 构建工具中,manifest-validate.ts 中对收集到的每一个 universalIdentifier 做两级检查:
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
// ...
if (!uuidValidate(identifier)) {
errors.push(`Universal identifier "${identifier}" is not a valid UUID.`);
continue;
}
const version = uuidVersion(identifier);
if (version < MINIMUM_UNIVERSAL_IDENTIFIER_UUID_VERSION) {
errors.push(
`Universal identifier "${identifier}" is UUID version ${version}. ` +
`Only UUID version ${MINIMUM_UNIVERSAL_IDENTIFIER_UUID_VERSION} or higher is allowed.`,
);
}
而版本下限常量定义在 manifest-validation-helpers.ts:
export const MINIMUM_UNIVERSAL_IDENTIFIER_UUID_VERSION = 4;
也就是说,文档中"必须是 UUID v4"的表述与源码完全一致:UUID 格式非法会报 "not a valid UUID",版本低于 4 会报 "Only UUID version 4 or higher is allowed"。这里的 "or higher" 说明 v5/v7 等更高版本在数值比较上也能通过下限检查,但文档仍要求统一使用 v4,是为了保证整个应用内标识符风格与生成方式一致。
示例应用中的正确示范
Postcard 应用中的每个实体都严格遵守了该约束,例如:
- 视图 all-post-cards.view.ts:
ALL_POST_CARDS_VIEW_ID = 'b1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',且defineView内 5 个列定义各自携带独立 UUID(见 第 18-54 行); - 导航项 post-cards.navigation-menu-item.ts:
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d'; - 前端组件 card.front-component.tsx:
CARD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER = '88c15ae2-5f87-4a6b-b48f-1974bbe62eb7'。
对开发者/Agent 的实操建议是:新增实体或字段时,用 crypto.randomUUID()(Node 19+ 与主流浏览器原生提供)或 uuid 库的 v4() 生成新标识符,不要手写——手写极易产出非法 UUID 或 v1/v3 版本;同时注意 identifier 是应用内引用关系(如 View 的 objectUniversalIdentifier 指向 Object)的一部分,一旦写入多处引用就不要变更。
常见陷阱一:创建 View 时未关联 navigationMenuItem
LLMS.md 原文:
Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.(创建了一个未关联 navigationMenuItem 的视图。这会让该视图出现在左侧边栏上。)
这条陷阱指向一个导航与视图的配套关系:View 挂在 Object 之下,而 Object 是否出现在侧边栏,由 NavigationMenuItem 决定。Postcard 的完整链路可以作为一个标准范本:
- 对象定义在 post-card.object.ts,导出
POST_CARD_UNIVERSAL_IDENTIFIER; - 表格视图通过
objectUniversalIdentifier绑定该对象,见 all-post-cards.view.ts 第 11-17 行:
export default defineView({
universalIdentifier: ALL_POST_CARDS_VIEW_ID,
name: 'All Post Cards',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: ViewType.TABLE,
icon: 'IconMail',
position: 0,
fields: [ /* 每列: universalIdentifier + fieldMetadataUniversalIdentifier + position + isVisible + size */ ],
});
- 导航项把该对象接进侧边栏,见 post-cards.navigation-menu-item.ts:
export default defineNavigationMenuItem({
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
position: 0,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
});
从源码结构看,defineNavigationMenuItem 的 type: OBJECT 配合 targetObjectUniversalIdentifier 建立了"侧边栏入口 → 对象"的映射;按文档的说法,若对象缺少这个入口映射,其下的视图就会以非预期方式暴露在左侧边栏(例如对象本身没有被导航正确接管时,视图入口的呈现位置会偏离设计)。因此实操规则可以概括为一句话:每新增一个对象 + 视图组合,都要同时提交一个 NavigationMenuItemType.OBJECT 类型的导航项指向该对象,三者(object / view / navigation-menu-item)应作为一组提交、一组审查。Postcard 目录下 src/objects/、src/views/、src/navigation-menu-items/ 三个目录一一对应,正是这个分组习惯的体现。
常见陷阱二:Front Component 自带滚动,而非响应 Widget 的固定尺寸
LLMS.md 原文:
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.(前端组件内部做了滚动,而不是响应其固定的 Widget 高度和宽度——除非它是专门为 canvas 标签页设计的。)
Twenty 的 Front Component 以 Widget 形式嵌入记录页等位置,Widget 容器由宿主赋予固定的高宽,组件应当自适应填充这个空间,而不是在内部再套一层 overflow: scroll 的滚动容器;例外情况是明确用于 canvas 标签页的组件,那里需要独立滚动是合理的。
Postcard 的 card.front-component.tsx 是一个正面示例,可以从中提取两条做法:
- 不设置任何内部滚动或固定高度:根节点只使用
padding与内容流式排版(第 36-40 行),加载态才用height: '100%'占满容器做居中,内容本身完全交给宿主 Widget 的尺寸约束; - 通过 SDK 而非全局状态获取上下文:组件用
useRecordId()拿到当前记录 id,再用CoreApiClient按id: { eq: recordId }查询postCard记录并每 3 秒轮询刷新(第 182-236 行),渲染逻辑只依赖"容器给多少空间我就占多少空间"这一原则。
组件的最终注册形式为:
export default defineFrontComponent({
universalIdentifier: CARD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
name: 'card-component',
description: 'A component using an external component file',
component: PostCardPreview,
});
该组件随后被 post-card-record-page.page-layout.ts 组装进记录页布局的 Widget 中——Widget 的固定尺寸由 page layout 侧定义,组件自身不与其对抗,这正是文档所要求的"responsive to its fixed widget height and width"。
实践清单:把 LLMS.md 的三条约束落成可核查规则
汇总 LLMS.md 的全部要求,配合源码证据,形成如下检查清单,可在新建/修改 Twenty App 时逐条核对:
| # | 约束(来自 LLMS.md) | 核查方式 | 仓库证据 |
|---|---|---|---|
| 1 | 所有生成的 UUID 必须是合法 UUID v4 | 本地执行 yarn twenty dev,manifest 构建校验会拦截非法/低版本 UUID |
manifest-validate.ts、manifest-validation-helpers.ts |
| 2 | View 必须与 NavigationMenuItem 关联,避免视图意外暴露到左侧边栏 | 每新增 object + view,检查是否同步新增 NavigationMenuItemType.OBJECT 导航项并指向同一对象 |
all-post-cards.view.ts、post-cards.navigation-menu-item.ts |
| 3 | Front Component 不应自带滚动,应响应固定 Widget 尺寸(canvas 标签页组件除外) | 审查组件样式:无 overflow: auto/scroll 内部滚动容器、无写死高度,仅做流式布局 |
card.front-component.tsx |
最后说明适用前提:本文所有结论基于当前仓库中的 twenty-sdk manifest 校验实现与 Postcard 示例源码,约束随 SDK 校验逻辑演化而可能变化;若你使用的是其他版本的 SDK,建议以本地 yarn twenty dev 构建输出和 packages/twenty-sdk/src/cli/utilities/build/manifest/ 下的实际校验代码为准。
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 StartedRust0624
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