首页
/ Twenty 应用开发 Agent 指南:UUID v4 约束与视图、前端组件两大常见陷阱

Twenty 应用开发 Agent 指南:UUID v4 约束与视图、前端组件两大常见陷阱

2026-09-06 11:50:33作者:温艾琴Wonderful

本文以 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.mdAGENT.mdCLAUDE.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 应用中的每个实体都严格遵守了该约束,例如:

对开发者/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 的完整链路可以作为一个标准范本:

  1. 对象定义在 post-card.object.ts,导出 POST_CARD_UNIVERSAL_IDENTIFIER
  2. 表格视图通过 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 */ ],
});
  1. 导航项把该对象接进侧边栏,见 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,
});

从源码结构看,defineNavigationMenuItemtype: 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 是一个正面示例,可以从中提取两条做法:

  1. 不设置任何内部滚动或固定高度:根节点只使用 padding 与内容流式排版(第 36-40 行),加载态才用 height: '100%' 占满容器做居中,内容本身完全交给宿主 Widget 的尺寸约束;
  2. 通过 SDK 而非全局状态获取上下文:组件用 useRecordId() 拿到当前记录 id,再用 CoreApiClientid: { 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.tsmanifest-validation-helpers.ts
2 View 必须与 NavigationMenuItem 关联,避免视图意外暴露到左侧边栏 每新增 object + view,检查是否同步新增 NavigationMenuItemType.OBJECT 导航项并指向同一对象 all-post-cards.view.tspost-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/ 下的实际校验代码为准。

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