Files
twenty/packages/twenty-docs/l/zh/developers/extend/apps/building.mdx
T
github-actions[bot] 6af7e32c54 i18n - docs translations (#19113)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-30 12:45:45 +02:00

962 lines
40 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 构建应用
description: 使用 Twenty SDK 定义对象、逻辑函数、前端组件等。
---
<Warning>
应用目前处于 Alpha 测试阶段。 该功能可用,但仍在演进中。
</Warning>
## 使用 SDK 资源(类型与配置)
twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以下是你最常接触的关键部分。
### 辅助函数
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](/l/zh/developers/extend/apps/getting-started#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
| 函数 | 目的 |
| -------------------------------- | ------------------------ |
| `defineApplication` | 配置应用元数据(必需,每个应用一个) |
| `defineObject` | 定义带字段的自定义对象 |
| `defineField` | 使用附加字段扩展现有对象,或定义独立的关系字段。 |
| `defineLogicFunction` | 定义带处理程序的逻辑函数 |
| `definePreInstallLogicFunction` | 定义一个安装前逻辑函数(每个应用一个) |
| `definePostInstallLogicFunction` | 定义一个安装后逻辑函数(每个应用一个) |
| `defineFrontComponent` | 为自定义 UI 定义前端组件 |
| `defineRole` | 配置角色权限和对象访问 |
| `defineView` | 为对象定义已保存的视图 |
| `defineNavigationMenuItem` | 定义侧边栏导航链接 |
| `defineSkill` | 定义 AI 智能体技能 |
| `defineAgent` | 定义 AI 智能体 |
| `definePageLayout` | 定义自定义页面布局 |
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
### 定义对象
自定义对象同时描述工作空间中记录的架构与行为。 使用 `defineObject()` 以内置校验定义对象:
```typescript
// src/objects/postCard.object.ts
import { defineObject, FieldType } from 'twenty-sdk';
enum PostCardStatus {
DRAFT = 'DRAFT',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
RETURNED = 'RETURNED',
}
export default defineObject({
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
nameSingular: 'postCard',
namePlural: 'postCards',
labelSingular: 'Post Card',
labelPlural: 'Post Cards',
description: 'A post card object',
icon: 'IconMail',
fields: [
{
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
name: 'content',
type: FieldType.TEXT,
label: 'Content',
description: "Postcard's content",
icon: 'IconAbc',
},
{
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
name: 'recipientName',
type: FieldType.FULL_NAME,
label: 'Recipient name',
icon: 'IconUser',
},
{
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
name: 'recipientAddress',
type: FieldType.ADDRESS,
label: 'Recipient address',
icon: 'IconHome',
},
{
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
name: 'status',
type: FieldType.SELECT,
label: 'Status',
icon: 'IconSend',
defaultValue: `'${PostCardStatus.DRAFT}'`,
options: [
{ value: PostCardStatus.DRAFT, label: 'Draft', position: 0, color: 'gray' },
{ value: PostCardStatus.SENT, label: 'Sent', position: 1, color: 'orange' },
{ value: PostCardStatus.DELIVERED, label: 'Delivered', position: 2, color: 'green' },
{ value: PostCardStatus.RETURNED, label: 'Returned', position: 3, color: 'orange' },
],
},
{
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
label: 'Delivered at',
icon: 'IconCheck',
isNullable: true,
defaultValue: null,
},
],
});
```
关键点:
* 使用 `defineObject()` 以获得内置校验和更好的 IDE 支持。
* `universalIdentifier` 必须在各次部署间保持唯一且稳定。
* 每个字段都需要 `name`、`type`、`label` 以及其自身稳定的 `universalIdentifier`。
* `fields` 数组是可选的——你可以定义没有自定义字段的对象。
* 你可以使用 `yarn twenty add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
<Note>
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加标准字段
例如 `id`、`name`、`createdAt`、`updatedAt`、`createdBy`、`updatedBy` 和 `deletedAt`。
你无需在 `fields` 数组中定义这些字段——只需添加你的自定义字段。
你可以通过在你的 `fields` 数组中定义一个同名字段来覆盖默认字段,
但不建议这样做。
</Note>
### 在现有对象上定义字段
使用 `defineField()` 向你不拥有的对象添加字段——例如标准的 Twenty 对象(Person、Company 等)。 或来自其他应用的对象。 与在 `defineObject()` 中的内联字段不同,独立字段需要一个 `objectUniversalIdentifier` 来指定它们要扩展的对象:
```typescript
// src/fields/company-loyalty-tier.field.ts
import { defineField, FieldType } from 'twenty-sdk';
export default defineField({
universalIdentifier: 'f2a1b3c4-d5e6-7890-abcd-ef1234567890',
objectUniversalIdentifier: '701aecb9-eb1c-4d84-9d94-b954b231b64b', // Company object
name: 'loyaltyTier',
type: FieldType.SELECT,
label: 'Loyalty Tier',
icon: 'IconStar',
options: [
{ value: 'BRONZE', label: 'Bronze', position: 0, color: 'orange' },
{ value: 'SILVER', label: 'Silver', position: 1, color: 'gray' },
{ value: 'GOLD', label: 'Gold', position: 2, color: 'yellow' },
],
});
```
关键点:
* `objectUniversalIdentifier` 用于标识目标对象。 对于标准对象,请使用从 `twenty-sdk` 导出的 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`。
* 在 `defineObject()` 中以内联方式定义字段时,你不需要 `objectUniversalIdentifier`——它会从父对象继承。
* `defineField()` 是为非通过 `defineObject()` 创建的对象添加字段的唯一方式。
### 关系
关系用于将对象彼此连接。 在 Twenty 中,关系始终是双向的——你需要定义两侧,每一侧都引用另一侧。
关系有两种类型:
| 关系类型 | 描述 | 是否有外键? |
| ------------- | ------------------- | ------------------- |
| `MANY_TO_ONE` | 该对象的多条记录指向目标对象的一条记录 | 是(`joinColumnName` |
| `ONE_TO_MANY` | 该对象的一条记录拥有目标对象的多条记录 | 否(反向侧) |
#### 关系如何工作
每个关系都需要两个相互引用的字段:
1. **MANY_TO_ONE** 侧——位于持有外键的对象上
2. **ONE_TO_MANY** 侧——位于拥有集合的对象上
两个字段都使用 `FieldType.RELATION`,并通过 `relationTargetFieldMetadataUniversalIdentifier` 相互交叉引用。
#### 示例:Post Card 拥有多个收件人
假设一个 `PostCard` 可以发送到多个 `PostCardRecipient` 记录。 每个收件人只隶属于一张 Post Card。
**步骤 1:在 PostCard 上定义 ONE_TO_MANY 侧**(“一”侧):
```typescript
// src/fields/post-card-recipients-on-post-card.field.ts
import { defineField, FieldType, RelationType } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_RECIPIENTS_FIELD_ID = 'a1111111-1111-1111-1111-111111111111';
// Import from the other side
import { POST_CARD_FIELD_ID } from './post-card-on-post-card-recipient.field';
export default defineField({
universalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCardRecipients',
label: 'Post Card Recipients',
icon: 'IconUsers',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_FIELD_ID,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
});
```
**步骤 2:在 PostCardRecipient 上定义 MANY_TO_ONE 侧**(“多”侧——持有外键):
```typescript
// src/fields/post-card-on-post-card-recipient.field.ts
import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
// Export so the other side can reference it
export const POST_CARD_FIELD_ID = 'b2222222-2222-2222-2222-222222222222';
// Import from the other side
import { POST_CARD_RECIPIENTS_FIELD_ID } from './post-card-recipients-on-post-card.field';
export default defineField({
universalIdentifier: POST_CARD_FIELD_ID,
objectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
icon: 'IconMail',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
});
```
<Note>
\*\*循环导入:\*\*两个关系字段相互引用彼此的 `universalIdentifier`。 为避免循环导入问题,请在各自文件中将字段 ID 作为具名常量导出,并在另一个文件中导入它们。 构建系统会在编译时解析这些引用。
</Note>
#### 与标准对象建立关系
要与内置的 Twenty 对象(Person、Company 等)建立关系,请使用 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`
```typescript
// src/fields/person-on-self-hosting-user.field.ts
import {
defineField,
FieldType,
RelationType,
OnDeleteAction,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
import { SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER } from '../objects/self-hosting-user.object';
export const PERSON_FIELD_ID = 'c3333333-3333-3333-3333-333333333333';
export const SELF_HOSTING_USER_REVERSE_FIELD_ID = 'd4444444-4444-4444-4444-444444444444';
export default defineField({
universalIdentifier: PERSON_FIELD_ID,
objectUniversalIdentifier: SELF_HOSTING_USER_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
description: 'Person matching with the self hosting user',
isNullable: true,
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier: SELF_HOSTING_USER_REVERSE_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.SET_NULL,
joinColumnName: 'personId',
},
});
```
#### 关系字段属性
| 属性 | 必填 | 描述 |
| ------------------------------------------------- | ---------------- | -------------------------------------------------------------- |
| `类型` | 是 | 必须为 `FieldType.RELATION` |
| `relationTargetObjectMetadataUniversalIdentifier` | 是 | 目标对象的 `universalIdentifier` |
| `relationTargetFieldMetadataUniversalIdentifier` | 是 | 目标对象上匹配字段的 `universalIdentifier` |
| `universalSettings.relationType` | 是 | `RelationType.MANY_TO_ONE` 或 `RelationType.ONE_TO_MANY` |
| `universalSettings.onDelete` | 仅适用于 MANY_TO_ONE | 当被引用的记录被删除时的处理方式:`CASCADE`、`SET_NULL`、`RESTRICT` 或 `NO_ACTION` |
| `universalSettings.joinColumnName` | 仅适用于 MANY_TO_ONE | 外键的数据库列名(例如,`postCardId` |
#### 在 defineObject 中内联关系字段
你也可以直接在 `defineObject()` 内定义关系字段。 在这种情况下,省略 `objectUniversalIdentifier`——它会从父对象继承:
```typescript
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCardRecipient',
// ...
fields: [
{
universalIdentifier: POST_CARD_FIELD_ID,
type: FieldType.RELATION,
name: 'postCard',
label: 'Post Card',
relationTargetObjectMetadataUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
relationTargetFieldMetadataUniversalIdentifier: POST_CARD_RECIPIENTS_FIELD_ID,
universalSettings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: OnDeleteAction.CASCADE,
joinColumnName: 'postCardId',
},
},
// ... other fields
],
});
```
### 应用配置(application-config.ts
每个应用都有一个 `application-config.ts` 文件,用于描述:
* **应用的身份**:标识符、显示名称和描述。
* **函数如何运行**:它们用于权限的角色。
* **(可选)变量**:以环境变量形式提供给函数的键值对。
* **(可选)安装前函数**:在应用安装之前运行的逻辑函数。
* **(可选)安装后函数**:在应用安装后运行的逻辑函数。
使用 `defineApplication()` 定义你的应用配置:
```typescript
// src/application-config.ts
import { defineApplication } from 'twenty-sdk';
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
export default defineApplication({
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
displayName: 'My Twenty App',
description: 'My first Twenty app',
icon: 'IconWorld',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
});
```
备注:
* `universalIdentifier` 字段是你拥有的确定性 ID;生成一次并在多次同步中保持稳定。
* `applicationVariables` 会变成函数可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
* `defaultRoleUniversalIdentifier` 必须与角色文件一致(见下文)。
* 清单构建期间会自动检测安装前和安装后函数。 参见 [安装前函数](#pre-install-functions) 和 [安装后函数](#post-install-functions)。
#### 应用市场元数据
如果你计划[发布你的应用](/l/zh/developers/extend/apps/publishing),这些可选字段将控制你的应用在应用市场中的展示:
| 字段 | 描述 |
| ------------------ | -------------------------- |
| `作者` | 作者或公司名称 |
| `类别` | 用于应用市场筛选的应用类别 |
| `logoUrl` | 你的应用徽标的路径(相对于 `./assets/` |
| `screenshots` | 屏幕截图路径数组(相对于 `./assets/` |
| `aboutDescription` | 用于“关于”选项卡的更长 Markdown 描述 |
| `websiteUrl` | 你的网站链接 |
| `termsUrl` | 服务条款链接 |
| `emailSupport` | 支持电子邮件地址 |
| `issueReportUrl` | 问题跟踪器链接 |
#### 角色和权限
应用可以定义角色,以封装对工作空间对象与操作的权限。 `application-config.ts` 中的 `defaultRoleUniversalIdentifier` 字段指定你的应用逻辑函数所使用的默认角色。
* 作为 `TWENTY_API_KEY` 注入的运行时 API 密钥源自该默认函数角色。
* 类型化客户端将受限于该角色授予的权限。
* 遵循最小权限原则:创建一个仅包含你的函数所需权限的专用角色,然后引用其通用标识符。
##### 默认函数角色(*.role.ts
当你脚手架生成新应用时,CLI 也会创建一个默认角色文件。 使用 `defineRole()` 定义带内置校验的角色:
```typescript
// src/roles/default-role.ts
import { defineRole, PermissionFlag } from 'twenty-sdk';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
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: '9f9882af-170c-4879-b013-f9628b77c050',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
```
随后,该角色的 `universalIdentifier` 会在 `application-config.ts` 中被引用为 `defaultRoleUniversalIdentifier`。 换句话说:
* **\*.role.ts** 定义默认函数角色可以执行的操作。
* **application-config.ts** 指向该角色,使你的函数继承其权限。
备注:
* 从脚手架生成的角色开始,然后按照最小权限原则逐步收紧权限。
* 将 `objectPermissions` 和 `fieldPermissions` 替换为你的函数所需的对象/字段。
* `permissionFlags` 控制对平台级能力的访问。 尽量保持最小化;仅添加所需项。
* 在 Hello World 应用中查看可运行示例:[`packages/twenty-apps/hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts)。
### 逻辑函数的配置与入口点
每个函数文件都使用 `defineLogicFunction()` 导出包含处理程序和可选触发器的配置。
```typescript
// src/logic-functions/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-sdk/generated';
const handler = async (params: RoutePayload) => {
const client = new CoreApiClient();
const name = 'name' in params.queryStringParameters
? params.queryStringParameters.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
: 'Hello world';
const result = await client.mutation({
createPostCard: {
__args: { data: { name } },
id: true,
name: true,
},
});
return result;
};
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'create-new-post-card',
timeoutSeconds: 2,
handler,
triggers: [
// Public HTTP route trigger '/s/post-card/create'
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/post-card/create',
httpMethod: 'GET',
isAuthRequired: false,
},
// Cron trigger (CRON pattern)
// {
// universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
// type: 'cron',
// pattern: '0 0 1 1 *',
// },
// Database event trigger
// {
// universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
// type: 'databaseEvent',
// eventName: 'person.updated',
// updatedFields: ['name'],
// },
],
});
```
常见触发器类型:
* **route**:在\*\*`/s/` 端点\*\*下通过 HTTP 路径与方法公开你的函数:
> 例如 `path: '/post-card/create',` -> 调用 `<APP_URL>/s/post-card/create`
* **cron**:使用 CRON 表达式按计划运行你的函数。
* **databaseEvent**:在工作空间对象生命周期事件上运行。 当事件操作为 `updated` 时,可以在 `updatedFields` 数组中指定要监听的特定字段。 如果未定义或为空,任何更新都会触发该函数。
> 例如 `person.updated`
备注:
* `triggers` 数组是可选的。 没有触发器的函数可作为实用函数,被其他函数调用。
* 你可以在单个函数中混用多种触发器类型。
### 安装前函数
安装前函数是在你的应用安装到工作区之前自动运行的逻辑函数。 这对于执行验证任务、先决条件检查,或在主安装开始前准备工作区状态很有用。
当你使用 `create-twenty-app` 脚手架创建一个新应用时,会在 `src/logic-functions/pre-install.ts` 为你生成一个安装前函数:
```typescript
// src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Pre install logic function executed successfully!', payload.previousVersion);
};
export default definePreInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'pre-install',
description: 'Runs before installation to prepare the application.',
timeoutSeconds: 300,
handler,
});
```
你也可以随时使用 CLI 手动执行安装前函数:
```bash filename="Terminal"
yarn twenty exec --preInstall
```
关键点:
* 安装前函数使用 `definePreInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装前函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `preInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的准备任务。
* 安装前函数不需要触发器——它们会在安装前由平台调用,或通过 `exec --preInstall` 手动调用。
### 安装后函数
安装后函数是在你的应用安装到工作区后自动运行的逻辑函数。 这对于一次性设置任务很有用,例如填充默认数据、创建初始记录或配置工作区设置。
当你使用 `create-twenty-app` 脚手架创建一个新应用时,会在 `src/logic-functions/post-install.ts` 为你生成一个安装后函数:
```typescript
// src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallLogicFunctionPayload } from 'twenty-sdk';
const handler = async (payload: InstallLogicFunctionPayload): Promise<void> => {
console.log('Post install logic function executed successfully!', payload.previousVersion);
};
export default definePostInstallLogicFunction({
universalIdentifier: '<generated-uuid>',
name: 'post-install',
description: 'Runs after installation to set up the application.',
timeoutSeconds: 300,
handler,
});
```
你也可以随时使用 CLI 手动执行安装后函数:
```bash filename="Terminal"
yarn twenty exec --postInstall
```
关键点:
* 安装后函数使用 `definePostInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装后函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `postInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的设置任务,如数据填充。
* 安装后函数不需要触发器——它们会在安装过程中由平台调用,或通过 `exec --postInstall` 手动调用。
### 路由触发器负载
<Warning>
**破坏性变更(v1.16,2026 年 1 月):** 路由触发器的负载格式已更改。 在 v1.16 之前,查询参数、路径参数和请求体会直接作为负载发送。 从 v1.16 开始,它们被嵌套在结构化的 `RoutePayload` 对象中。
**v1.16 之前:**
```typescript
const handler = async (params) => {
const { param1, param2 } = params; // Direct access
};
```
**v1.16 之后:**
```typescript
const handler = async (event: RoutePayload) => {
const { param1, param2 } = event.body; // Access via .body
const { queryParam } = event.queryStringParameters;
const { id } = event.pathParameters;
};
```
**迁移现有函数:** 将处理程序更新为从 `event.body`、`event.queryStringParameters` 或 `event.pathParameters` 解构,而不是直接从参数对象解构。
</Warning>
当路由触发器调用你的逻辑函数时,它会接收一个遵循 AWS HTTP API v2 格式的 `RoutePayload` 对象。 从 `twenty-sdk` 导入该类型:
```typescript
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
const handler = async (event: RoutePayload) => {
// Access request data
const { headers, queryStringParameters, pathParameters, body } = event;
// HTTP method and path are available in requestContext
const { method, path } = event.requestContext.http;
return { message: 'Success' };
};
```
`RoutePayload` 类型具有以下结构:
| 属性 | 类型 | 描述 |
| ---------------------------- | ------------------------------------- | ------------------------------------------------- |
| `headers` | `Record<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) |
| `queryStringParameters` | `Record<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) |
| `pathParameters` | `Record<string, string \| undefined>` | 从路由模式中提取的路径参数(例如,`/users/:id` -> `{ id: '123' }` |
| `body` | `object \| null` | 已解析的请求体(JSON) |
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 |
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE |
| `requestContext.http.path` | `string` | 原始请求路径 |
### 转发 HTTP 请求头
出于安全原因,默认**不会**将传入请求的 HTTP 请求头传递给你的逻辑函数。 如需访问特定请求头,请在 `forwardedRequestHeaders` 数组中显式列出:
```typescript
export default defineLogicFunction({
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
name: 'webhook-handler',
handler,
triggers: [
{
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
type: 'route',
path: '/webhook',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: ['x-webhook-signature', 'content-type'],
},
],
});
```
随后你可以在处理程序中访问这些请求头:
```typescript
const handler = async (event: RoutePayload) => {
const signature = event.headers['x-webhook-signature'];
const contentType = event.headers['content-type'];
// Validate webhook signature...
return { received: true };
};
```
<Note>
请求头名称会被规范化为小写。 请使用小写键访问它们(例如,`event.headers['content-type']`)。
</Note>
你可以通过两种方式创建新函数:
* **脚手架生成**:运行 `yarn twenty add` 并选择添加新逻辑函数的选项。 这将生成一个包含处理程序和配置的入门文件。
* **手动**:创建一个新的 `*.logic-function.ts` 文件,并使用 `defineLogicFunction()`,遵循相同的模式。
### 将逻辑函数标记为工具
逻辑函数可以作为供 AI 智能体和工作流使用的**工具**对外提供。 当函数被标记为工具时,Twenty 的 AI 功能即可发现它,并可在工作流自动化中将其选作一个步骤。
要将逻辑函数标记为工具,请设置 `isTool: true`,并提供 `toolInputSchema`,使用 [JSON Schema](https://json-schema.org/) 描述预期的输入参数:
```typescript
// src/logic-functions/enrich-company.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (params: { companyName: string; domain?: string }) => {
const client = new CoreApiClient();
const result = await client.mutation({
createTask: {
__args: {
data: {
title: `Enrich data for ${params.companyName}`,
body: `Domain: ${params.domain ?? 'unknown'}`,
},
},
id: true,
},
});
return { taskId: result.createTask.id };
};
export default defineLogicFunction({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'enrich-company',
description: 'Enrich a company record with external data',
timeoutSeconds: 10,
handler,
isTool: true,
toolInputSchema: {
type: 'object',
properties: {
companyName: {
type: 'string',
description: 'The name of the company to enrich',
},
domain: {
type: 'string',
description: 'The company website domain (optional)',
},
},
required: ['companyName'],
},
});
```
关键点:
* **`isTool`** (`boolean`, 默认: `false`): 当设置为 `true` 时,该函数会被注册为工具,并可供 AI 代理和工作流自动化使用。
* **`toolInputSchema`** (`object`, 可选): 描述函数可接受参数的 JSON Schema 对象。 AI 代理使用此架构来理解该工具期望的输入并验证调用。 如果省略,架构将默认为 `{ type: 'object', properties: {} }`(无参数)。
* 设置为 `isTool: false`(或未设置)的函数**不会**被暴露为工具。 它们仍可直接执行或被其他函数调用,但不会出现在工具发现中。
* **工具命名**: 当作为工具对外暴露时,函数名会被自动规范化为 `logic_function_<name>`(转换为小写,非字母数字字符替换为下划线)。 例如,`enrich-company` 将变为 `logic_function_enrich_company`。
* 你可以将 `isTool` 与触发器结合使用——一个函数既可以作为工具(由 AI 代理调用),也可以同时由事件(cron、数据库事件、路由)触发。
<Note>
**写一个好的 `description`。** AI 代理会依赖该函数的 `description` 字段来决定何时使用该工具。 明确说明该工具的作用以及应在何时调用。
</Note>
### 前端组件
前端组件使你可以构建在 Twenty 的 UI 中渲染的自定义 React 组件。 使用 `defineFrontComponent()` 以内置校验定义组件:
```typescript
// src/front-components/my-widget.tsx
import { defineFrontComponent } from 'twenty-sdk';
const MyWidget = () => {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>My Custom Widget</h1>
<p>This is a custom front component for Twenty.</p>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-widget',
description: 'A custom widget component',
component: MyWidget,
});
```
关键点:
* 前端组件是在 Twenty 中的隔离上下文中渲染的 React 组件。
* `component` 字段引用你的 React 组件。
* 组件会在 `yarn twenty dev` 期间自动构建并同步。
你可以通过两种方式创建新的前端组件:
* **脚手架生成**:运行 `yarn twenty add` 并选择添加新前端组件的选项。
* **手动**:创建一个新的 `.tsx` 文件,并使用 `defineFrontComponent()`,遵循相同的模式。
### 技能
技能定义了可复用的指令和能力,AI 智能体可在你的工作区中使用。 使用 `defineSkill()` 定义带内置校验的技能:
```typescript
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
关键点:
* `name` 是该技能的唯一标识字符串(推荐使用 kebab-case)。
* `label` 是在 UI 中显示的人类可读名称。
* `content` 包含技能指令——这是 AI 智能体使用的文本。
* `icon`(可选)设置在 UI 中显示的图标。
* `description`(可选)提供有关技能用途的更多上下文。
你可以通过两种方式创建新技能:
* **脚手架生成**:运行 `yarn twenty add` 并选择添加新技能的选项。
* **手动**:创建一个新文件,并使用 `defineSkill()`,遵循相同的模式。
### 类型化 API 客户端(`twenty-client-sdk`
`twenty-client-sdk` 包提供了两个类型化的 GraphQL 客户端,供你的逻辑函数和前端组件与 Twenty API 交互:
| 客户端 | 导入 | 端点 | 是否生成? |
| ------------------- | ---------------------------- | ------------------------ | --------- |
| `CoreApiClient` | `twenty-client-sdk/core` | `/graphql`——工作区数据(记录、对象) | 是,在开发/构建时 |
| `MetadataApiClient` | `twenty-client-sdk/metadata` | `/metadata`——工作区配置、文件上传 | 否,已预构建提供 |
#### CoreApiClient
`CoreApiClient` 是用于查询和变更工作区数据的主要客户端。 它会在执行 `yarn twenty dev` 或 `yarn twenty build` 时根据你的工作区架构生成,因此能完全类型化以匹配你的对象和字段。
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
const client = new CoreApiClient();
// Query records
const { companies } = await client.query({
companies: {
edges: {
node: {
id: true,
name: true,
domainName: true,
},
},
},
});
// Create a record
const { createCompany } = await client.mutation({
createCompany: {
__args: {
data: {
name: 'Acme Corp',
},
},
id: true,
name: true,
},
});
```
该客户端使用选择集语法:传入 `true` 以包含某字段,使用 `__args` 传递参数,并通过嵌套对象表示关系。 你将基于工作区架构获得完整的自动补全和类型检查。
<Note>
**CoreApiClient 在开发/构建时生成。** 如果在未先运行 `yarn twenty dev` 或 `yarn twenty build` 的情况下尝试使用它,将会抛出错误。 生成过程是自动完成的——CLI 会自省你的工作区 GraphQL 架构,使用 `@genql/cli` 生成类型化客户端,将生成的源码写入 `node_modules/twenty-client-sdk/dist/core/generated/`,并替换 `node_modules/twenty-client-sdk/dist/core.mjs` 和 `node_modules/twenty-client-sdk/dist/core.cjs` 中的存根。
</Note>
#### 使用 CoreSchema 进行类型标注
`CoreSchema` 提供与工作区对象相匹配的 TypeScript 类型,可用于为组件状态或函数参数进行类型标注:
```typescript
import { CoreApiClient, CoreSchema } from 'twenty-client-sdk/core';
import { useState } from 'react';
const [company, setCompany] = useState<
Pick<CoreSchema.Company, 'id' | 'name'> | undefined
>(undefined);
const client = new CoreApiClient();
const result = await client.query({
company: {
__args: { filter: { position: { eq: 1 } } },
id: true,
name: true,
},
});
setCompany(result.company);
```
#### MetadataApiClient
`MetadataApiClient` 随 SDK 一并提供,已预构建(无需生成)。 它会查询 `/metadata` 端点以获取工作区配置、应用以及文件上传:
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const metadataClient = new MetadataApiClient();
// Query workspace info
const { currentWorkspace } = await metadataClient.query({
currentWorkspace: { id: true, displayName: true },
});
// List installed applications
const { findManyApplications } = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
version: true,
},
});
```
#### 运行时凭据
当你的代码在 Twenty 上运行(逻辑函数或前端组件)时,平台会以环境变量的形式注入凭据:
* `TWENTY_API_URL`——Twenty API 的基础 URL
* `TWENTY_API_KEY`——作用域限定于你的应用默认函数角色的短期密钥
你无需将这些值传递给客户端——它们会自动从 `process.env` 读取。 API 密钥的权限由你的 `application-config.ts` 中 `defaultRoleUniversalIdentifier` 引用的角色决定。
#### 上传文件
`MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到文件类型字段。 它实现了[GraphQL 多部分请求规范](https://github.com/jaydenseric/graphql-multipart-request-spec)
```typescript
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import * as fs from 'fs';
const metadataClient = new MetadataApiClient();
const fileBuffer = fs.readFileSync('./invoice.pdf');
const uploadedFile = await metadataClient.uploadFile(
fileBuffer, // file contents as a Buffer
'invoice.pdf', // filename
'application/pdf', // MIME type
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universalIdentifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
| 参数 | 类型 | 描述 |
| ---------------------------------- | -------- | ------------------------------------------------ |
| `fileBuffer` | `Buffer` | 原始文件内容 |
| `filename` | `string` | 文件名称(用于存储和显示) |
| `contentType` | `string` | 文件的 MIME 类型(如果省略,默认为 `application/octet-stream` |
| `fieldMetadataUniversalIdentifier` | `string` | 你的对象上文件类型字段的 `universalIdentifier` |
关键点:
* 使用字段的 `universalIdentifier`(而不是其工作区特定的 ID),因此你的上传代码可在安装了你的应用的任何工作区中运行。
* 返回的 `url` 是一个签名 URL,你可以用它来访问已上传的文件。
### Hello World 示例
在[此处](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world)查看一个最小的端到端示例,展示对象、逻辑函数、前端组件和多种触发器。