Files
twenty/packages/twenty-docs/l/zh/developers/extend/capabilities/apps.mdx
T
fb34d2ce80 i18n - docs translations (#18938)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-25 01:55:36 +01:00

1428 lines
61 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: Twenty 应用
description: 以代码的形式构建并管理 Twenty 自定义项。
---
<Warning>
应用目前处于 Alpha 测试阶段。 该功能可用,但仍在演进中。
</Warning>
## 什么是应用?
应用使你能够**以代码的形式**构建和管理 Twenty 自定义项。 无需通过 UI 配置所有内容,你可以在代码中定义数据模型和逻辑函数——从而更快地构建、维护,并推广到多个工作空间。
**你现在可以做什么:**
* 以代码定义自定义对象和字段(受管理的数据模型)
* 构建带有自定义触发器的逻辑函数
* 为 AI 定义技能和代理
* 将同一个应用部署到多个工作空间
## 先决条件
* Node.js 24+ 和 Yarn 4
* Docker (用于本地 Twenty 开发服务器)
## 开始使用
使用官方脚手架创建一个新应用。 它可以为你自动启动一个本地 Twenty 实例:
```bash filename="Terminal"
# Scaffold a new app — the CLI will offer to start a local Twenty server
npx create-twenty-app@latest my-twenty-app
cd my-twenty-app
# Start dev mode: automatically syncs local changes to your workspace
yarn twenty dev
```
### 本地服务器管理
该 SDK 包含用于管理本地 Twenty 开发服务器的命令(该服务器是一体化 Docker 镜像,内含 PostgreSQL、Redis、服务器和工作进程):
```bash filename="Terminal"
# Start the local server (pulls the image if needed)
yarn twenty server start
# Check server status
yarn twenty server status
# Stream server logs
yarn twenty server logs
# Stop the server
yarn twenty server stop
# Reset all data and start fresh
yarn twenty server reset
```
本地服务器预置了一个工作区和用户 (`tim@apple.dev` / `tim@apple.dev`),因此你可以无需任何手动设置即可立即开始开发。
### 身份验证
使用 OAuth 将你的应用连接到本地服务器:
```bash filename="Terminal"
# 通过 OAuth 进行身份验证 (将打开浏览器)
yarn twenty remote add --local
```
脚手架工具支持两种模式,用于控制包含哪些示例文件:
```bash filename="Terminal"
# 默认(完整):所有示例(对象、字段、逻辑函数、前端组件、视图、导航菜单项、技能、代理)
npx create-twenty-app@latest my-app
# 最小化:仅核心文件(application-config.ts 和 default-role.ts
npx create-twenty-app@latest my-app --minimal
```
### 如何使用本地 Twenty 实例
如果你已经在本地运行一个 Twenty 实例,你可以连接到它,而不是使用 Docker。 指定本地服务器正在监听的端口(默认:`3000`):
```bash filename="Terminal"
# 在脚手架过程中
npx create-twenty-app@latest my-app --port 3000
# 或在脚手架之后
yarn twenty remote add --local --port 3000
```
从这里您可以:
```bash filename="Terminal"
# 向你的应用添加一个新实体(引导式)
yarn twenty entity:add
# 监听你的应用函数日志
yarn twenty function:logs
# 按名称执行一个函数
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# 执行安装前函数
yarn twenty function:execute --preInstall
# 执行安装后函数
yarn twenty function:execute --postInstall
# 为分发构建应用
yarn twenty build
# 将应用发布到 npm 或 Twenty 服务器
yarn twenty publish
# 从当前工作区卸载该应用
yarn twenty uninstall
# 显示命令帮助
yarn twenty help
```
另请参阅:[create-twenty-app](https://www.npmjs.com/package/create-twenty-app) 和 [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk) 的 CLI 参考页面。
## 项目结构(脚手架生成)
当你运行 `npx create-twenty-app@latest my-twenty-app` 时,脚手架将:
* 将一个最小的基础应用复制到 `my-twenty-app/` 中
* 添加本地 `twenty-sdk` 依赖和 Yarn 4 配置
* 创建与 `twenty` CLI 关联的配置文件和脚本
* 生成核心文件(应用配置、默认函数角色、安装前/安装后函数),并基于脚手架模式生成示例文件
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
```text filename="my-twenty-app/"
my-twenty-app/
package.json
yarn.lock
.gitignore
.nvmrc
.yarnrc.yml
.yarn/
install-state.gz
.oxlintrc.json
tsconfig.json
README.md
public/ # 公共资源文件夹(图片、字体等)
src/
├── application-config.ts # 必需 - 主应用配置
├── roles/
│ └── default-role.ts # 逻辑函数的默认角色
├── objects/
│ └── example-object.ts # 示例自定义对象定义
├── fields/
│ └── example-field.ts # 示例独立字段定义
├── logic-functions/
│ ├── hello-world.ts # 示例逻辑函数
│ ├── pre-install.ts # 安装前逻辑函数
│ └── post-install.ts # 安装后逻辑函数
├── front-components/
│ └── hello-world.tsx # 示例前端组件
├── views/
│ └── example-view.ts # 示例已保存视图定义
├── navigation-menu-items/
│ └── example-navigation-menu-item.ts # 示例侧边栏导航链接
├── skills/
│ └── example-skill.ts # 示例 AI 代理技能定义
└── agents/
└── example-agent.ts # 示例 AI 代理定义
```
使用 `--minimal` 时,只会创建核心文件(`application-config.ts`、`roles/default-role.ts`、`logic-functions/pre-install.ts` 和 `logic-functions/post-install.ts`)。
总体来说:
* **package.json**:声明应用名称、版本、引擎(Node 24+、Yarn 4),并添加 `twenty-sdk` 以及一个 `twenty` 脚本,该脚本会委托给本地的 `twenty` CLI。 运行 `yarn twenty help` 以列出所有可用命令。
* **.gitignore**:忽略常见产物,如 `node_modules`、`.yarn`、`generated/`(类型化客户端)、`dist/`、`build/`、覆盖率文件夹、日志文件以及 `.env*` 文件。
* **yarn.lock**、**.yarnrc.yml**、**.yarn/**:锁定并配置项目使用的 Yarn 4 工具链。
* **.nvmrc**:固定项目期望的 Node.js 版本。
* **.oxlintrc.json** and **tsconfig.json**: Provide linting and TypeScript configuration for your app's TypeScript sources.
* **README.md**:应用根目录中的简短 README,包含基本说明。
* **public/**: 一个用于存储公共资源(图像、字体、静态文件)的文件夹,这些资源将随你的应用程序一起提供。 放置在此处的文件会在同步期间上传,并可在运行时访问。
* **src/**:你以代码形式定义应用的主要位置
### 实体检测
该 SDK 通过在你的 TypeScript 文件中解析 **`export default define<Entity>({...})`** 调用来检测实体。 每种实体类型都有一个从 `twenty-sdk` 导出的对应辅助函数:
| 辅助函数 | 实体类型 |
| ---------------------------------- | -------------------------------- |
| `defineObject()` | 自定义对象定义 |
| `defineLogicFunction()` | 逻辑函数定义 |
| `definePreInstallLogicFunction()` | 安装前逻辑函数(在安装之前运行) |
| `definePostInstallLogicFunction()` | 安装后逻辑函数(在安装之后运行) |
| `defineFrontComponent()` | 前端组件定义 |
| `defineRole()` | 角色定义 |
| `defineField()` | 现有对象的字段扩展 |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
| `defineAgent()` | AI 代理定义 |
<Note>
**文件命名是灵活的。** 实体检测基于 AST — SDK 会扫描你的源文件以查找 `export default define<Entity>({...})` 模式。 你可以按照自己的喜好组织文件和文件夹。 按实体类型分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
</Note>
已检测实体的示例:
```typescript
// This file can be named anything and placed anywhere in src/
import { defineObject, FieldType } from 'twenty-sdk';
export default defineObject({
universalIdentifier: '...',
nameSingular: 'postCard',
// ... rest of config
});
```
后续命令将添加更多文件和文件夹:
* `yarn twenty dev` 会在 `node_modules/twenty-sdk/clients` 中自动生成两个类型化 API 客户端:`CoreApiClient`(通过 `/graphql` 获取工作区数据)和 `MetadataApiClient`(通过 `/metadata` 处理工作区配置和文件上传)。
* `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## 身份验证
首次运行 `yarn twenty auth:login` 时,你将被提示输入:
* API URL(默认为 http://localhost:3000 或你当前的工作空间配置)
* API 密钥
你的凭据按用户存储在 `~/.twenty/config.json` 中。 你可以维护多个配置文件并在它们之间切换。
### 管理工作空间
```bash filename="Terminal"
# Login interactively (recommended)
yarn twenty auth:login
# Login to a specific workspace profile
yarn twenty auth:login --workspace my-custom-workspace
# List all configured workspaces
yarn twenty auth:list
# Switch the default workspace (interactive)
yarn twenty auth:switch
# Switch to a specific workspace
yarn twenty auth:switch production
# Check current authentication status
yarn twenty auth:status
```
使用 `yarn twenty auth:switch` 切换工作空间后,后续所有命令将默认使用该工作空间。 你仍可通过 `--workspace <name>` 临时覆盖。
## 使用 SDK 资源(类型与配置)
twenty-sdk 提供你在应用中使用的类型化构件和辅助函数。 以下是你最常接触的关键部分。
### 辅助函数
该 SDK 提供辅助函数用于定义你的应用实体。 如 [实体检测](#entity-detection) 中所述,你必须使用 `export default define<Entity>({...})` 才能让你的实体被检测到:
| 函数 | 目的 |
| ---------------------------------- | ------------------------------- |
| `defineApplication()` | 配置应用元数据(必需,每个应用一个) |
| `defineObject()` | 定义带字段的自定义对象 |
| `defineLogicFunction()` | 定义带处理程序的逻辑函数 |
| `definePreInstallLogicFunction()` | 定义一个安装前逻辑函数(每个应用一个) |
| `definePostInstallLogicFunction()` | 定义一个安装后逻辑函数(每个应用一个) |
| `defineFrontComponent()` | 为自定义 UI 定义前端组件 |
| `defineRole()` | 配置角色权限和对象访问 |
| `defineField()` | 为现有对象扩展额外字段 |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
| `defineAgent()` | 使用系统提示词定义 AI 智能体 |
这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
### 定义对象
自定义对象同时描述工作空间中记录的架构与行为。 使用 `defineObject()` 以内置校验定义对象:
```typescript
// src/app/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 entity:add` 脚手架创建新对象,它会引导你完成命名、字段和关系。
<Note>
**基础字段会自动创建。** 当你定义自定义对象时,Twenty 会自动添加标准字段
例如 `id`、`name`、`createdAt`、`updatedAt`、`createdBy`、`updatedBy` 和 `deletedAt`。
你无需在 `fields` 数组中定义这些字段——只需添加你的自定义字段。
你可以通过在你的 `fields` 数组中定义一个同名字段来覆盖默认字段,
但不建议这样做。
</Note>
### 在现有对象上定义字段
使用 `defineField()` 向现有对象添加自定义字段——包括标准对象(如 `company`、`person`、`opportunity`)以及由其他应用定义的自定义对象。 每个字段位于其各自的文件中,并通过其 `universalIdentifier` 引用目标对象。
要引用标准对象,请从 `twenty-sdk` 导入 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`。 该常量为所有内置对象及其字段提供稳定的标识符:
```typescript
// src/fields/apollo-total-funding.field.ts
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk';
export default defineField({
universalIdentifier: 'c90ae72d-4ddf-4f22-882f-eef98c91e40e',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.CURRENCY,
name: 'apolloTotalFunding',
label: 'Total Funding',
description: 'Total funding raised by the company',
icon: 'IconCash',
});
```
关键点:
* `objectUniversalIdentifier` 告诉 Twenty 将该字段附加到哪个对象。 使用 `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.<objectName>.universalIdentifier` 用于标准对象。
* 每个字段都需要其自身稳定的 `universalIdentifier`、`name`、`type`、`label`,以及目标 `objectUniversalIdentifier`。
* 你可以使用 `yarn twenty entity:add` 脚手架创建新字段,并选择字段选项。
* `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS` 也以 `STANDARD_OBJECT` 的名称导出以方便使用——二者指向同一常量。
可用的标准对象包括:`attachment`、`blocklist`、`calendarChannel`、`calendarEvent`、`calendarEventParticipant`、`company`、`connectedAccount`、`dashboard`、`favorite`、`favoriteFolder`、`message`、`messageChannel`、`messageParticipant`、`messageThread`、`note`、`noteTarget`、`opportunity`、`person`、`task`、`taskTarget`、`timelineActivity`、`workflow`、`workflowAutomatedTrigger`、`workflowRun`、`workflowVersion` 和 `workspaceMember`。
每个标准对象还会暴露其字段标识符。 例如,要在角色权限中引用标准对象上的特定字段:
```typescript
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier
```
#### 现有对象上的关联字段
你还可以定义关联字段,将现有对象链接到你的自定义对象:
```typescript
// src/fields/people-on-call-recording.field.ts
import { defineField, FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
import { CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/call-recording';
import { CALL_RECORDING_ON_PERSON_ID } from 'src/fields/call-recording-on-person.field';
export default defineField({
universalIdentifier: '4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d',
objectUniversalIdentifier:
CALL_RECORDING_OBJECT_UNIVERSAL_IDENTIFIER,
type: FieldType.RELATION,
name: 'person',
label: 'Person',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
CALL_RECORDING_ON_PERSON_ID,
relationType: RelationType.MANY_TO_ONE,
});
```
### 应用配置(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)。
#### 角色和权限
应用可以定义角色,以封装对工作空间对象与操作的权限。 `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/app/createPostCard.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk';
import type { DatabaseEventPayload, ObjectRecordCreateEvent, CronPayload, RoutePayload } from 'twenty-sdk';
import { CoreApiClient, type Person } from 'twenty-client-sdk/core';
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 function:execute --preInstall
```
关键点:
* 安装前函数使用 `definePreInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装前函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `preInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的准备任务。
* 安装前函数不需要触发器——它们会在安装前由平台调用,或通过 `function:execute --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,
});
```
You can also manually execute the post-install function at any time using the CLI:
```bash filename="Terminal"
yarn twenty function:execute --postInstall
```
关键点:
* 安装后函数使用 `definePostInstallLogicFunction()` —— 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`isTool`)的专用变体。
* 处理器会接收一个 `InstallLogicFunctionPayload`,其包含 `{ previousVersion: string }` —— 即之前安装的应用版本(全新安装则为空字符串)。
* 每个应用仅允许一个安装后函数。 如果检测到多个,清单构建将报错。
* 在构建期间,函数的 `universalIdentifier` 会自动设置为应用清单上的 `postInstallLogicFunctionUniversalIdentifier` —— 你无需在 `defineApplication()` 中引用它。
* The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
* Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --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' }` |
| `请求体` | `object \| null` | 已解析的请求体(JSON) |
| `isBase64Encoded` | `布尔值` | 请求体是否为 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 entity: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 entity:add` 并选择添加新前端组件的选项。
* **手动**:创建一个新的 `.tsx` 文件,并使用 `defineFrontComponent()`,遵循相同的模式。
#### 前端组件可用位置
在 Twenty 中,前端组件可在两个位置进行渲染:
* **侧边栏** — 非无头的前端组件会在右侧侧边栏中打开。 当前端组件从命令菜单触发时,这是默认行为。
* **小部件(仪表盘和记录页面)** — 前端组件可以作为小部件嵌入页面布局中。 在配置仪表盘或记录页面布局时,用户可以添加前端组件小部件。
#### 无头与非无头
前端组件有两种由 `isHeadless` 选项控制的渲染模式:
**非无头(默认)** — 该组件会渲染可见的 UI。 从命令菜单触发时,它会在侧边栏中打开。 当 `isHeadless` 为 `false` 或被省略时,这是默认行为。
**无头** — 该组件会在后台以不可见的方式挂载。 它不会打开侧边栏。 无头组件旨在用于执行逻辑后自行卸载的操作——例如运行异步任务、导航到某个页面或显示确认模态框。 它们与下文介绍的 SDK Command 组件天然契合。
```typescript
export default defineFrontComponent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'my-action',
description: 'Runs an action without opening the side panel',
component: MyAction,
isHeadless: true,
command: {
universalIdentifier: 'b2c3d4e5-f6a7-8901-bcde-f12345678901',
label: 'Run my action',
},
});
```
#### 添加命令菜单项
要让前端组件作为一项出现在 Twenty 的命令菜单中,请在 `defineFrontComponent()` 中添加 `command` 属性。 当用户打开命令菜单(Cmd+K / Ctrl+K)时,该项会显示,并在点击时触发该前端组件。
`command` 对象接受以下字段:
| 字段 | 类型 | 描述 |
| --------------------------------------- | ------------------------------------- | -------------------------------------------------- |
| `universalIdentifier` | `string` (必填) | 命令菜单项的唯一 ID |
| `标签` | `string` (必填) | 在命令菜单中显示的标签 |
| `图标` | `string` (可选) | 图标名称(例如,`'IconSparkles'` |
| `isPinned` | `boolean` (可选) | 该命令是否固定在菜单顶部 |
| `availabilityType` | `'GLOBAL' \| 'RECORD_SELECTION'` (可选) | `GLOBAL` 会在任意位置显示该命令;`RECORD_SELECTION` 仅在记录上下文中显示 |
| `availabilityObjectUniversalIdentifier` | `string` (可选) | 将该命令限制为特定对象类型(例如,Person) |
以下是来自通话录音应用的示例,它添加了一个作用域为 Person 记录的命令:
```typescript
import { defineFrontComponent } from 'twenty-sdk';
export default defineFrontComponent({
universalIdentifier: 'c3d4e5f6-a7b8-9012-cdef-123456789012',
name: 'Summarize Person Call Recordings',
description: 'Generates a summary of call recordings for a person',
component: SummarizePersonRecordings,
command: {
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-234567890123',
label: 'Summarize call recordings',
icon: 'IconSparkles',
isPinned: false,
availabilityType: 'RECORD_SELECTION',
availabilityObjectUniversalIdentifier:
'20202020-e674-48e5-a542-72570eee7213',
},
});
```
当该命令同步后,它会出现在命令菜单中。 如果前端组件为非无头,侧边栏将打开并在其中渲染该组件。 如果为无头,该组件会在后台挂载并执行其逻辑。
#### SDK Command 组件
`twenty-sdk` 包提供了四个为无头前端组件设计的 Command 辅助组件。 每个组件都会在挂载时执行一个操作,通过显示 snackbar 通知来处理错误,并在完成后自动卸载该前端组件。
从 `twenty-sdk/command` 导入它们:
* **`Command`** — 通过 `execute` 属性运行异步回调。
* **`CommandLink`** — 导航到某个应用路径。 属性:`to`、`params`、`queryParams`、`options`。
* **`CommandModal`** — 打开一个确认模态框。 如果用户确认,则执行 `execute` 回调。 属性:`title`、`subtitle`、`execute`、`confirmButtonText`、`confirmButtonAccent`。
* **`CommandOpenSidePanelPage`** — 打开特定的侧边栏页面。 属性:`page`、`pageTitle`、`pageIcon`。
下面是一个完整示例:无头前端组件使用 `Command` 从命令菜单运行一个操作:
```typescript
// src/front-components/run-action.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { Command } from 'twenty-sdk/command';
import { CoreApiClient } from 'twenty-sdk/clients';
const RunAction = () => {
const execute = async () => {
const client = new CoreApiClient();
await client.mutation({
createTask: {
__args: { data: { title: 'Created by my app' } },
id: true,
},
});
};
return <Command execute={execute} />;
};
export default defineFrontComponent({
universalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
name: 'run-action',
description: 'Creates a task from the command menu',
component: RunAction,
isHeadless: true,
command: {
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
label: 'Run my action',
icon: 'IconPlayerPlay',
},
});
```
另一个示例:使用 `CommandModal` 在执行前请求确认:
```typescript
// src/front-components/delete-draft.tsx
import { defineFrontComponent } from 'twenty-sdk';
import { CommandModal } from 'twenty-sdk/command';
const DeleteDraft = () => {
const execute = async () => {
// perform the deletion
};
return (
<CommandModal
title="Delete draft?"
subtitle="This action cannot be undone."
execute={execute}
confirmButtonText="Delete"
confirmButtonAccent="danger"
/>
);
};
export default defineFrontComponent({
universalIdentifier: 'a7b8c9d0-e1f2-3456-abcd-567890123456',
name: 'delete-draft',
description: 'Deletes a draft with confirmation',
component: DeleteDraft,
isHeadless: true,
command: {
universalIdentifier: 'b8c9d0e1-f2a3-4567-bcde-678901234567',
label: 'Delete draft',
icon: 'IconTrash',
},
});
```
#### 执行上下文
每个前端组件都会接收一个执行上下文,提供其运行位置与方式的信息。 使用来自 `twenty-sdk` 的钩子来访问上下文值:
| 钩子 | 返回类型 | 描述 |
| ----------------------- | ---------------- | ------------------------------------------------------------- |
| `useFrontComponentId()` | `string` | 当前前端组件实例的唯一 ID |
| `useRecordId()` | `string \| null` | 当组件在记录上下文中运行时(例如记录页面小部件或作用域限定于某条记录的命令),当前记录的 ID。 否则返回 `null`。 |
| `useUserId()` | `string \| null` | 当前用户的 ID |
```typescript
import { useRecordId, useUserId } from 'twenty-sdk';
const MyWidget = () => {
const recordId = useRecordId();
const userId = useUserId();
return (
<div>
<p>Record: {recordId ?? 'none'}</p>
<p>User: {userId ?? 'anonymous'}</p>
</div>
);
};
```
该上下文是响应式的——如果周围的记录发生变化,钩子会自动返回更新后的值。
#### 宿主 API 函数
前端组件在隔离的沙箱中运行,但可以通过宿主提供的一组函数与 Twenty 的 UI 进行交互。 直接从 `twenty-sdk` 导入它们:
```typescript
import {
navigate,
closeSidePanel,
enqueueSnackbar,
unmountFrontComponent,
openSidePanelPage,
openCommandConfirmationModal,
} from 'twenty-sdk';
```
| 函数 | 签名 | 描述 |
| ------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `导航` | `(to, params?, queryParams?, options?) => Promise<void>` | 在 Twenty 内导航到一个类型化的应用路径 |
| `closeSidePanel` | `() => Promise<void>` | 关闭侧边栏 |
| `enqueueSnackbar` | `(params) => Promise<void>` | 显示一条 snackbar 通知。 参数:`message`、`variant``'error'`、`'success'`、`'info'`、`'warning'`),可选的 `duration`、`detailedMessage`、`dedupeKey` |
| `unmountFrontComponent` | `() => Promise<void>` | 卸载当前前端组件(供无头组件在执行后进行清理) |
| `openSidePanelPage` | `(params) => Promise<void>` | 在侧边栏中打开一个页面。 参数:`page`、`pageTitle`、`pageIcon`、`shouldResetSearchState` |
| `openCommandConfirmationModal` | `(params) => Promise<'confirm' \| 'cancel'>` | 显示一个确认模态框并等待用户的响应。 参数:`title`、`subtitle`、`confirmButtonText`、`confirmButtonAccent``'default'`、`'blue'`、`'danger'` |
下面是一个示例,使用宿主 API 在操作完成后显示一条 snackbar 并关闭侧边栏:
```typescript
import { defineFrontComponent, useRecordId } from 'twenty-sdk';
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk';
import { CoreApiClient } from 'twenty-sdk/clients';
const ArchiveRecord = () => {
const recordId = useRecordId();
const handleArchive = async () => {
const client = new CoreApiClient();
await client.mutation({
updateTask: {
__args: { id: recordId, data: { status: 'ARCHIVED' } },
id: true,
},
});
await enqueueSnackbar({
message: 'Record archived',
variant: 'success',
});
await closeSidePanel();
};
return (
<div style={{ padding: '20px' }}>
<p>Archive this record?</p>
<button onClick={handleArchive}>Archive</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'c9d0e1f2-a3b4-5678-cdef-789012345678',
name: 'archive-record',
description: 'Archives the current record',
component: ArchiveRecord,
});
```
### 技能
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
```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` is a unique identifier string for the skill (kebab-case recommended).
* `label` is the human-readable display name shown in the UI.
* `content` contains the skill instructions — this is the text the AI agent uses.
* `icon` (optional) sets the icon displayed in the UI.
* `description` (optional) provides additional context about the skill's purpose.
You can create new skills in two ways:
* **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
* **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### 代理
Agents 通过系统提示词定义可在你的工作区内运行的 AI 智能体。 使用 `defineAgent()` 以内置校验定义智能体:
```typescript
// src/agents/example-agent.ts
import { defineAgent } from 'twenty-sdk';
export default defineAgent({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-assistant',
label: 'Sales Assistant',
description: 'An AI agent that helps with sales tasks',
icon: 'IconRobot',
prompt: `You are a sales assistant. Help users with:
1. Researching prospects and companies
2. Drafting personalized outreach messages
3. Tracking follow-ups and next steps
4. Analyzing deal pipeline and suggesting actions`,
});
```
关键点:
* `name` 是该智能体的唯一标识字符串(推荐使用 kebab-case)。
* `label` 是在 UI 中显示的人类可读名称。
* `prompt` 包含系统提示词—这是定义智能体行为的指令文本。
* `icon`(可选)设置在 UI 中显示的图标。
* `description`(可选)提供有关智能体用途的更多上下文。
你可以通过两种方式创建新智能体:
* **脚手架生成**:运行 `yarn twenty entity:add` 并选择添加新智能体的选项。
* **手动**:创建一个新文件,并使用 `defineAgent()`,遵循相同的模式。
### 生成的类型化客户端
两个类型化客户端由 `yarn twenty dev` 自动生成(基于你的工作区架构),并存放在 `node_modules/twenty-sdk/clients`
* **`CoreApiClient`** — 查询 `/graphql` 端点以获取工作区数据
* **`MetadataApiClient`** — 查询 `/metadata` 端点以获取工作区配置并处理文件上传
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const client = new CoreApiClient();
const { me } = await client.query({ me: { id: true, displayName: true } });
const metadataClient = new MetadataApiClient();
const { currentWorkspace } = await metadataClient.query({ currentWorkspace: { id: true } });
```
每当你的对象或字段发生变化时,`yarn twenty dev` 都会自动重新生成 `CoreApiClient`。 `MetadataApiClient` 随 SDK 一并提供,已预构建。
#### 逻辑函数中的运行时凭据
当你的函数在 Twenty 上运行时,平台会在代码执行前将凭据作为环境变量注入:
* `TWENTY_API_URL`:你的应用所针对的 Twenty API 的基础 URL。
* `TWENTY_API_KEY`:作用域限定于你的应用默认函数角色的短期密钥。
备注:
* 你无需向生成的客户端传递 URL 或 API 密钥。 它会在运行时从 process.env 读取 `TWENTY_API_URL` 和 `TWENTY_API_KEY`。
* API 密钥的权限由 `application-config.ts` 中通过 `defaultRoleUniversalIdentifier` 引用的角色决定。 这是你的应用逻辑函数使用的默认角色。
* 应用可以定义角色以遵循最小权限原则。 仅授予函数所需的权限,然后将 `defaultRoleUniversalIdentifier` 指向该角色的通用标识符。
#### 上传文件
`MetadataApiClient` 包含一个 `uploadFile` 方法,用于将文件附加到你的工作区对象的文件类型字段。 由于标准 GraphQL 客户端不原生支持多部分文件上传,该客户端提供了一个专用方法,在底层实现了 [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 (defaults to 'application/octet-stream')
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
);
console.log(uploadedFile);
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
```
方法签名:
```typescript
uploadFile(
fileBuffer: Buffer,
filename: string,
contentType: string,
fieldMetadataUniversalIdentifier: string,
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
```
| 参数 | 类型 | 描述 |
| ---------------------------------- | -------- | ------------------------------------------------ |
| `fileBuffer` | `Buffer` | 原始文件内容 |
| `filename` | `string` | 文件名称(用于存储和显示) |
| `contentType` | `string` | 文件的 MIME 类型(如果省略,默认为 `application/octet-stream` |
| `fieldMetadataUniversalIdentifier` | `string` | 你的对象上文件类型字段的 `universalIdentifier` |
关键点:
* `uploadFile` 方法可在 `MetadataApiClient` 上使用,因为上传 mutation 由 `/metadata` 端点解析。
* 它使用该字段的 `universalIdentifier`(而不是其工作区特定的 ID),因此你的上传代码可以在安装了你的应用的任何工作区中使用——这与应用在其他地方引用字段的方式保持一致。
* 返回的 `url` 是一个签名 URL,你可以用它来访问已上传的文件。
### Hello World 示例
在[此处](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world)查看一个最小的端到端示例,展示对象、逻辑函数、前端组件和多种触发器:
## Building your app
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
```bash filename="Terminal"
# Build the app (output goes to .twenty/output/)
yarn twenty build
# Build and create a tarball (.tgz) for distribution
yarn twenty build --tarball
```
The build process:
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
4. **生成类型化的 API 客户端** — 对 GraphQL 架构进行自省,并生成带类型的 `CoreApiClient` 和 `MetadataApiClient` 客户端。
5. **运行 TypeScript 类型检查** — 运行 `tsc --noEmit` 以在发布前捕获类型错误。
6. **使用生成的客户端重新构建** — 执行第二次编译,以便包含生成的客户端类型。
7. **可选地创建一个 tar 包** — 如果传入 `--tarball`,则运行 `npm pack` 以创建用于分发的 `.tgz` 文件。
`.twenty/output/` 中的构建产物包含:
```text
.twenty/output/
├── manifest.json # Manifest with checksums for all built files
├── package.json # Copied from app root
├── yarn.lock # Copied from app root
├── src/
│ ├── logic-functions/ # Compiled .mjs logic function files
│ └── front-components/ # Compiled .mjs front component files
├── public/ # Static assets (if any)
└── my-app-1.0.0.tgz # Only with --tarball flag
```
| 选项 | 描述 |
| ----------- | ----------------------- |
| `[appPath]` | 应用目录的路径(默认为当前目录) |
| `--tarball` | 同时将输出打包为一个 `.tgz` tar 包 |
## 发布你的应用
使用 `app:publish` 分发你的应用 — 可以发布到 npm 注册表,或直接发布到 Twenty 服务器。
### 发布到 npm(默认)
```bash filename="Terminal"
# Publish to npm (requires npm login)
yarn twenty publish
# Publish with a dist-tag (e.g. beta, next)
yarn twenty publish --tag beta
```
这会构建应用,并在 `.twenty/output/` 目录下运行 `npm publish`。 发布后的软件包可由任何工作区从 Twenty 市场进行安装。
### 发布到 Twenty 服务器
```bash filename="Terminal"
# Publish directly to a Twenty server
yarn twenty publish --server https://app.twenty.com
```
这会以 tar 包方式构建应用,通过 `uploadAppTarball` GraphQL 变更将其上传到服务器,并在一步中触发安装。 这对于私有部署或针对特定服务器进行测试非常有用。
| 选项 | 描述 |
| ----------------- | ------------------------------------------ |
| `[appPath]` | 应用目录的路径(默认为当前目录) |
| `--server <url>` | 发布到 Twenty 服务器(而非 npm) |
| `--token <token>` | 目标服务器的身份验证令牌 |
| `--tag <tag>` | npm dist-tag(例如 `beta`、`next`)— 仅用于发布到 npm |
## 应用注册
在应用安装到工作区之前,必须先进行**注册**。 注册是一条元数据记录,用于描述应用的来源以及如何对其进行身份验证。 在大多数情况下,CLI 会自动处理这一流程。
### 来源类型
每个注册都有一个**来源类型**,用于决定安装期间如何解析应用的文件:
| 来源类型 | 文件的解析方式 | 典型用例 |
| --------- | -------------------------------- | --------------------- |
| `LOCAL` | 文件由 CLI 监听器实时同步——跳过安装步骤 | 使用 `app:dev` 进行开发 |
| `NPM` | 通过 `sourcePackage` 字段从 npm 注册表获取 | 在 npm 上发布的应用 |
| `TARBALL` | 从存储在服务器上的已上传 `.tgz` 文件中解压获得 | 使用 `--server` 发布的私有应用 |
### 注册如何进行
* **`app:dev`** — 第一次针对某个工作区运行开发模式时,会自动创建一个 `LOCAL` 注册。
* **`app:publish --server`** — 上传一个 tar 包并创建(或更新)一个 `TARBALL` 注册,然后安装应用。
* **npm 市场** — 当应用从 npm 注册表同步到 Twenty 市场目录时,会创建 `NPM` 注册。
* **GraphQL API** — 你也可以通过 `createApplicationRegistration` 变更以编程方式创建注册。
### 注册与安装
**注册** 与 **安装** 是两个独立的概念:
* **注册**`ApplicationRegistration`)是一条全局元数据记录,用于描述应用:其名称、来源类型、OAuth 凭据以及在市场中的上架状态。 它独立于任何工作区存在。
* **安装**`Application`)是一个按工作区划分的实例。 当用户安装一个应用时,Twenty 会根据注册的来源解析软件包,将构建生成的文件写入存储,并同步清单(创建对象、字段、逻辑函数等) 到该工作区中。
一个注册可以安装到多个工作区。 每个工作区都会获得应用文件和数据模型的独立副本。
### OAuth 凭据
每个注册都包含在创建时生成的 OAuth 凭据(`oAuthClientId` 和 `oAuthClientSecret`)。 应用使用这些凭据代表用户对 API 请求进行身份验证。 客户端密钥在创建时只会返回**一次**——请妥善保管。 你可以稍后通过 `rotateApplicationRegistrationClientSecret` 变更来轮换它。
## 手动设置(不使用脚手架)
虽然我们建议使用 `create-twenty-app` 以获得最佳的上手体验,但你也可以手动设置项目。 不要全局安装 CLI。 相反,请将 `twenty-sdk` 添加为本地依赖,并在你的 package.json 中配置一个脚本:
```bash filename="Terminal"
yarn add -D twenty-sdk
```
然后添加一个 `twenty` 脚本:
```json filename="package.json"
{
"scripts": {
"twenty": "twenty"
}
}
```
现在你可以通过 `yarn twenty <command>` 运行所有命令,例如 `yarn twenty dev`、`yarn twenty help` 等。
## 故障排除
* 身份验证错误:运行 `yarn twenty auth:login`,并确保你的 API 密钥具有所需权限。
* 无法连接到服务器:请验证 API URL,并确保 Twenty 服务器可达。
* 类型或客户端缺失/过期:重启 `yarn twenty dev` — 它会自动生成类型化客户端。
* 开发模式未同步:确保 `yarn twenty dev` 正在运行,并且你的环境不会忽略变更。
Discord 帮助频道:https://discord.com/channels/1130383047699738754/1130386664812982322