首页
/ 用 Twenty App 示例工程掌握全部实体类型:Postcard App 源码深度解读

用 Twenty App 示例工程掌握全部实体类型:Postcard App 源码深度解读

2026-09-07 20:55:52作者:卓艾滢Kingsley

Postcard App 是 Twenty 仓库中位于 packages/twenty-apps/examples/postcard 的一个完整示例应用,它在一个可运行的工程内同时演示了 Twenty SDK 提供的全部应用实体类型:从自定义对象、独立字段与关系、逻辑函数(含安装钩子)、前端组件,到角色权限、视图、导航、技能与 AI Agent、页面布局等。读完本文,你将掌握 twenty-sdk/define 声明式 API 的用法、各类实体的文件组织方式与配置参数,并可直接以该工程为模板搭建自己的 Twenty App。

一、示例工程概览:它到底"全"在哪里

Postcard App 的官方定位是"A rich example app showcasing all Twenty app entity types",即作为开发者构建自身应用时的对照参考。目录结构(根目录为仓库根 packages/twenty-apps/examples/postcard)本身就构成了一张"实体类型 ↔ 源码文件"的映射表:

实体类型 示例文件 演示内容
Application src/application.config.ts 应用元数据、应用级变量(applicationVariables)、服务端变量(serverVariables)
Objects src/objects/ 带内联字段的自定义对象(postCard),含枚举字段默认值、可空时间字段等
Fields src/fields/ 独立字段、关系字段(ONE_TO_MANY / MANY_TO_ONE)、扩展标准对象(Person)
Logic Functions src/logic-functions/ HTTP 路由、数据库事件触发器、cron 调度、工具函数、安装前后钩子等逻辑载体
Front Components src/components/ 渲染在 Twenty 界面内部的 React 组件
Roles src/roles/ 具备对象级与字段级访问控制的权限角色
Views src/views/ 带列配置的保存型表格视图
Navigation src/navigation-menu-items/ 指向视图的侧边栏导航链接
Skills src/skills/ 为 AI Agent 提供上下文的技能声明
Agents src/agents/ 带系统提示词的 AI Agent
Page Layouts src/page-layouts/ 嵌入前端组件 Widget 的自定义记录页

除上述实体外,工程还包含 src/command-menu-items/(命令菜单项,可挂在页面头部)以及 e2e/ 端到端测试,后者对 front component 与共享依赖打包(shared-dependencies-bundle.spec.ts)进行了 Playwright 验证。

二、环境与启动方式

工程通过 package.json 声明运行约束:Node 需为 ^24.5.0、使用 yarn(>=4.0.2,本工程锁定 yarn@4.13.0)、twenty CLI 版本不低于 2.23.0,并声明 keywordstwenty-app 以便生态识别。其核心依赖是 twenty-sdk(声明实体)与 twenty-client-sdk(调用 Twenty GraphQL API),前端组件部分共享 reactreact-dom/client(通过 frontComponentSharedDependencies 字段声明)。

官方 README 给出的启动步骤十分简单:

# 在示例工程目录内执行
yarn install
yarn twenty dev

结合 package.json 中的 scripts 可以补充完整的开发命令集合:yarn twentytwenty CLI(构建/发布/安装应用);yarn lint 使用 oxlint 静态检查;yarn test 运行 vitest 单元与集成测试;yarn test:e2e 运行基于 playwright.config.ts 的端到端测试。src 下还包含 src/tests/schema.integration-test.tssrc/tests/global-setup.ts,从命名可以推断其负责对应用 schema 做集成校验并为测试做全局准备。

三、Application:应用元数据与两级变量

每个 Twenty App 的入口是 application.config.ts,Postcard 的示例完整展示了三类声明:

// packages/twenty-apps/examples/postcard/src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from './roles/default-function.role';

export const APPLICATION_UNIVERSAL_IDENTIFIER =
  '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58';

export default defineApplication({
  universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
  displayName: 'Postcard App',
  description: 'Send postcards easily with Twenty',
  applicationVariables: {
    DEFAULT_RECIPIENT_NAME: {
      universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
      description: 'Default recipient name for postcards',
      value: 'Alex Karp',
      isSecret: false,
    },
  },
  serverVariables: {
    POSTCARD_API_KEY: {
      description: 'API key for the postcard printing service',
      isSecret: true,
      isRequired: true,
    },
    POSTCARD_SENDER_NAME: {
      description: 'Default sender name on postcards',
      isSecret: false,
      isRequired: false,
    },
  },
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});

可以对照逐项解析其参数含义:

  • universalIdentifier:应用级稳定 UUID,用于跨环境(本地、生产、不同 workspace)识别同一实体;同一对象被重新安装或迁移时不至于因 ID 漂移而失效。本示例统一集中定义在各文件中并导出(如 APPLICATION_UNIVERSAL_IDENTIFIER),供关系字段等跨文件引用。
  • displayName / description:应用在 Twenty 市场与安装界面的展示信息。
  • applicationVariables应用级变量,随应用包分发、可被前端组件与页面逻辑读取。示例中的 DEFAULT_RECIPIENT_NAME 给出默认收件人姓名,通过 isSecret: false 表明该值非敏感、可直接渲染。
  • serverVariables服务端变量,在安装时由管理员填写或由部署方注入。示例区分了两种形态:POSTCARD_API_KEYisSecret: true(密文存储,仅服务端逻辑函数可读)且 isRequired: true(未配置将阻止安装);POSTCARD_SENDER_NAME 是可选非敏感值。
  • defaultRoleUniversalIdentifier:把角色模块定义的默认角色接入应用装配。

四、Objects 与 Fields:自定义对象、独立字段与关系

4.1 自定义对象:内联字段声明

src/objects/post-card.object.ts 定义了一张"明信片"业务表,展示了 defineObject 的完整形态:

import { defineObject, FieldType } from 'twenty-sdk/define';

enum PostCardStatus { DRAFT, SENT, DELIVERED, RETURNED }

export const POST_CARD_UNIVERSAL_IDENTIFIER = '54b589ca-eeed-4950-a176-358418b85c05';

export default defineObject({
  universalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  nameSingular: 'postCard',
  namePlural: 'postCards',
  labelSingular: 'Post card',
  labelPlural: 'Post cards',
  description: 'A post card object',
  icon: 'IconMail',
  labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
  fields: [ /* ... */ ],
});

关键参数:nameSingular/namePlural 是底层 GraphQL/API 名称(决定如 createPostCards mutation 的命名),labelSingular/labelPlural 是界面文案,icon 取 Twenty 图标库名(如 IconMail),labelIdentifierFieldMetadataUniversalIdentifier 指定用于展示记录的"主标签"字段。

其内联字段覆盖了几种典型类型:

  • TEXT 字段namecontent,均带 iconIconAbc)与描述文案;
  • SELECT 枚举字段 status:通过 options 数组声明 DRAFT/SENT/DELIVERED/RETURNED 四个选项,每项含 valuelabelposition(排序位)、color(gray/orange/green 等界面色)以及稳定的 iddefaultValue: 'DRAFT'``(字符串包一层引号)让新记录默认落在草稿态;
  • DATE_TIME 可空字段 deliveredAtisNullable: truedefaultValue: null,对应"寄达时间未知"的语义。

从源码结构看,该对象中所有 universalIdentifier 都被抽成常量导出,这是为了让字段、角色、页面等模块在引用该对象时不依赖字符串硬编码。

4.2 独立字段:给标准对象"加字段"

src/fields/person-can-receive-postcards.field.ts 演示了如何扩展 Twenty 内置标准对象:它通过 STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier 指向标准 Person 对象,然后以 defineField 追加一个 BOOLEAN 字段 canReceivePostcards(默认 true),图标 IconMailbox。这意味着安装本应用后,Twenty 联系人页会自动多出"是否可接收明信片"的开关——这正是 Twenty 应用生态常见的"给核心对象打补丁"模式。

4.3 关系字段:一对多与多对一

关系在 Postcard 中是一对配对声明的字段,分别落在两个对象上:

recipient-on-post-card.field.ts(PostCard 侧):

type: FieldType.RELATION,
name: 'recipient',
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: POST_CARDS_ON_PERSON_ID,  // 对方字段的 ID
universalSettings: {
  relationType: RelationType.MANY_TO_ONE,
  onDelete: OnDeleteAction.SET_NULL,
  joinColumnName: 'recipientId',
}

post-cards-on-person.field.ts(Person 侧):

type: FieldType.RELATION,
name: 'postCards',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: RECIPIENT_ON_POST_CARD_ID, // 对方字段的 ID
universalSettings: {
  relationType: RelationType.ONE_TO_MANY,
}

要点归纳:

  • 关系的双向字段必须互相指向对方的 universalIdentifierrelationTargetFieldMetadataUniversalIdentifier 引用对侧),配对形成闭环;
  • 从关系的"一"方(Person)看是 ONE_TO_MANY(一个人可拥有多张明信片),从"多"方(PostCard)看是 MANY_TO_ONE,并在该侧配置 onDelete: OnDeleteAction.SET_NULL(删除联系人后明信片的 recipient 置空而不级联删除)与 joinColumnName: 'recipientId'(外键列名);
  • POST_CARDS_ON_PERSON_IDRECIPIENT_ON_POST_CARD_ID 两个 UUID 常量在字段文件间交叉 import,再次体现"ID 常量集中定义、供关系互相引用"的工程习惯。

五、Logic Functions:逻辑载体与安装钩子

src/logic-functions/ 目录存放应用的服务端逻辑。README 提到该类型可承载 HTTP 路由、数据库事件触发器、cron 调度、工具函数与安装钩子,Postcard 具体演示了安装生命周期钩子这一子类:pre-install(迁移前执行)与 post-install(安装后执行)。

src/logic-functions/post-install.ts 的核心是用客户端 SDK 做"安装即播种":

import { CoreApiClient } from 'twenty-client-sdk/core';
import { definePostInstallLogicFunction } from 'twenty-sdk/define';

const SEED_POST_CARDS = [
  { name: 'Greetings from Paris', content: 'Wish you were here! The Eiffel Tower is breathtaking.' },
  { name: 'Hello from Tokyo', content: 'Cherry blossoms are in full bloom. Sending love!' },
];

const handler = async () => {
  const client = new CoreApiClient();
  await client.mutation({
    createPostCards: {
      __args: { data: SEED_POST_CARDS as any },
      id: true,
    },
  } as any);
  console.log(`Seeded ${SEED_POST_CARDS.length} post cards on install.`);
  return {};
};

export default definePostInstallLogicFunction({
  universalIdentifier: '852c6321-1563-4396-b7c5-9d370f3d30a9',
  name: 'post-install',
  description: 'Runs after installation to set up the application.',
  timeoutSeconds: 30,
  handler,
});

值得注意的实现细节:

  • 通过 twenty-client-sdkCoreApiClient 构造 GraphQL mutation,createPostCards 正是由 4.1 节 namePlural: 'postCards' 生成的 API 名称,印证对象命名与 API 命名的对应关系;
  • 声明式 definePostInstallLogicFunction 包装了 universalIdentifiernamedescription、**timeoutSeconds(30s 超时上限)**与 handler,同时 handler 必须以对象 {} 返回;
  • 它演示了"安装后初始化数据 + 打印日志"的典型用途。

与之对应的 src/logic-functions/pre-install.ts 结构几乎一致,只是使用 definePreInstallLogicFunction、超时更短(timeoutSeconds: 10),并在 handler 中接收 params 打印参数后返回空对象。README 中描述的"HTTP routes、database event triggers、cron schedules、tool functions"属于同一实体类型的其他子类,本示例主要聚焦安装钩子以保持可自动演示。

六、Roles:对象级 + 字段级权限模型

src/roles/default-function.role.ts 定义了一个典型的"功能型最小权限角色"(DEFAULT_ROLE_UNIVERSAL_IDENTIFIER),并作为 defaultRoleUniversalIdentifier 挂到应用上。它展示了权限声明的最小化原则与两级粒度:

import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';

export default defineRole({
  universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  label: 'Default function role',
  description: 'Default role for function Twenty client',
  canReadAllObjectRecords: false,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,
  objectPermissions: [
    {
      objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
      canReadObjectRecords: true,
      canUpdateObjectRecords: true,
      canSoftDeleteObjectRecords: false,
      canDestroyObjectRecords: false,
    },
  ],
  fieldPermissions: [
    {
      objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
      fieldUniversalIdentifier: CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
      canReadFieldValue: true,
      canUpdateFieldValue: true,
    },
  ],
  permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});

解析要点:

  • 顶层布尔开关(canRead/Update/SoftDelete/DestroyAllObjectRecordscanUpdateAllSettings)全部置 false,角色不持有任何全局权限,属于最安全默认;
  • 三个 canBeAssignedTo* 均置 false,表明该角色不直接分发给人/Agent/API Key,而由逻辑函数以"Function Twenty client"身份运行(见 description);
  • objectPermissions 将读/写权限精确授予自定义的 postCards 对象,但软删除与物理删除仍然关闭;
  • fieldPermissions 进一步把 content 字段的读写显式放开,叠加出"对象只读其他字段、content 可编辑"的字段级控制;
  • permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS] 通过系统权限标志位(SDK 内置枚举)授予应用管理相关能力。

七、Views、Navigation 与 Command Menu:让数据可被找到

7.1 保存型视图

src/views/all-post-cards.view.ts 定义"所有明信片"的表格视图:包含列配置(字段选择与展示顺序),供侧边栏导航直接跳转。视图是"可保存的查询 + 布局",让不同角色/页面共享同一份记录列表界面。

7.2 侧边栏导航

src/navigation-menu-items/post-cards.navigation-menu-item.ts 把上述视图挂到 Twenty 左侧导航。这样应用安装后,用户即可从侧边栏进入明信片列表——它是"用户如何发现应用数据"的第一入口。

7.3 命令菜单项

src/command-menu-items/ 目录下有两个命令:generate-post-card(生成明信片)与 send-post-cards(寄出明信片)。从命名与第 9 节的页面布局可以看出,命令菜单项可被页面头部 Widget 引用(headerCommandMenuItemUniversalIdentifiers),实现"在记录页直接触发 AI 生成、一键寄出"的操作闭环。

八、Skills 与 Agents:给 AI 提供上下文与人格

Twenty 应用可以声明面向 Agent 的 AI 能力。Postcard 用两个文件构成一组最小可用的"AI 协作对":

export default defineAgent({
  universalIdentifier: 'b8d4f2a3-9c5e-4f7b-a012-3e4d5c6b7a8f',
  name: 'postcard-drafter',
  label: 'Postcard Drafter',
  icon: 'IconRobot',
  description: 'Helps draft postcard messages',
  prompt:
    'You are a postcard writing assistant. Help users draft concise, warm ' +
    'postcard messages. Follow the postcard writing guidelines. Ask for the ' +
    'recipient name and the occasion if not provided.',
});

该示例清晰展示了 Agent 实体的声明式写法:name 供系统识别、label/description 供界面展示、iconIconRobot,而 prompt 即系统提示词——它要求 Agent 扮演明信片写作助手、遵循写作准则并在信息不足时主动询问收件人与场合。结合角色一节中 canBeAssignedToAgents 的开关可以推断,Agent 执行实际写库等操作时仍需受角色权限约束,从而把"模型行为"与"数据安全"解耦。

九、Page Layouts 与 Front Components:自定义记录页

9.1 前端组件(Front Components)

src/components/card.front-component.tsx 是渲染在 Twenty 记录页内部的 React 组件,作用是"模拟显示明信片正面"。同目录还包含两个 effect 组件(generate-post-card-component-effect.tsxsend-post-cards-component-effect.tsx),从命名看负责承载"生成/寄出"的交互副作用;card-test-ids.ts 则导出稳定 test id 供 e2e 断言。工程通过 frontComponentSharedDependencies(react、react-dom/client)声明与宿主共享的运行库,避免重复打包——这一点在 e2e/shared-dependencies-bundle.spec.ts 中由 Playwright 做了专门验证。

9.2 页面布局:把组件拼进记录页

src/page-layouts/post-card-record-page.page-layout.ts 通过 definePageLayoutpostCard 对象定制记录页,是"布局即声明"的典型样例:

export default definePageLayout({
  universalIdentifier: 'f2bf4b9f-0485-46f0-89bb-9a65d2b939b1',
  name: 'Post Card Record Page',
  type: 'RECORD_PAGE',
  objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
  tabs: [
    {
      title: 'Fields', position: 0, icon: 'IconList',
      layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
      widgets: [{ title: 'Post Card Fields', type: 'FIELDS', configuration: { configurationType: 'FIELDS' } }],
    },
    {
      title: 'Preview', position: 50, icon: 'IconEye',
      widgets: [{
        title: 'Card Preview', type: 'FRONT_COMPONENT',
        configuration: {
          configurationType: 'FRONT_COMPONENT',
          frontComponentUniversalIdentifier: CARD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
          headerCommandMenuItemUniversalIdentifiers: [
            GENERATE_POST_CARD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER,
          ],
        },
      }],
    },
  ],
});

其建模逻辑可以拆解为三层:

  • 页面级type: 'RECORD_PAGE' 声明这是对象详情页,通过 objectUniversalIdentifier 绑定到 postCard 对象;
  • Tab 级position 决定 Tab 顺序,layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST 指示字段以纵向列表排布;
  • Widget 级:第一栏 FIELDS 展示对象字段;第二栏 PreviewFRONT_COMPONENT 类型的 Widget 加载明信片正面组件,并通过 headerCommandMenuItemUniversalIdentifiers 在头部注入"生成明信片"命令入口——至此,第 7.3 节的命令菜单项与本节组件在 UI 上完成装配。

十、从示例到自己的 App:可复用的工程套路

Postcard App 最大的价值不在于功能本身,而在于它揭示了 Twenty App 的标准工程组织方式。对照该工程清单,开发自己的应用时可以遵循以下套路:

  1. 一切实体都是声明式文件:用 defineApplication/defineObject/defineField/defineRole/defineAgent/definePageLayouttwenty-sdk/define 工厂逐个声明,并按 application.config.ts 聚合;
  2. ID 集中管理:把所有 universalIdentifier 抽成文件内导出的常量,关系字段、页面布局、角色通过常量互相引用,杜绝字符串散落;
  3. 最小权限默认:先像默认 function role 那样关掉全部全局权限,再按对象、按字段精确放开,并用 defaultRoleUniversalIdentifier 接入应用;
  4. 扩展标准对象优先于新建对象:给 Person 加 canReceivePostcards 这类"属性扩展"直接以独立 field 文件声明即可;
  5. 安装即就绪:用 pre/post-install 逻辑函数做迁移前检查和数据播种,让用户装完就能看到效果;
  6. 用测试守住边界:集成测试校验 schema(schema.integration-test.ts),Playwright 覆盖前端组件与共享依赖(e2e/);
  7. 版本纪律:在 package.json 的 engines 声明 Node/yarn/twenty 版本范围并维持 twenty >= 2.23.0,这是应用能被正确构建的前提。

十一、结语与延伸阅读

Postcard App 是 Twenty 仓库内 fixtures 之外最能完整展示实体类型组合方式的 example(其余示例如 hello-worlddocument-generator 各有侧重,而 internal/twenty-partners 则是更大型的内部应用)。如果你希望更系统地掌握声明式 API 的全部入参,可以直接阅读 twenty-sdk 的类型定义;若关注运行时行为,可进一步查看 twenty-apps/fixtures 中的各应用配置与对应测试。以本工程为起点,结合 AGENT.mdCLAUDE.md 中的约束说明,你便能快速搭出一个结构完整、权限收敛、具备 AI 能力并且可端到端验证的 Twenty App。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391