snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
---
title: 应用程序配置
description: 使用 defineApplication 声明应用的身份、默认角色、变量和应用市场元数据。
icon: rocket
---
每个应用必须且只能有一个 `defineApplication` 调用。 它声明:
* **应用的身份** — 通用标识符、显示名称、描述。
* **权限** — 其逻辑函数和前端组件在何种角色下运行。
* **变量**(可选)— 以环境变量形式暴露给代码的键值对。
* **安装前/安装后/卸载函数**(可选)— 参见 [逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions)。
```ts src/application-config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '39783023-bcac-41e3-b0d2-ff1944d8465d',
displayName: 'My Twenty App',
description: 'My first Twenty app',
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
description: 'Default recipient name for postcards',
value: 'Jane Doe',
isSecret: false,
},
},
});
```
备注:
* `universalIdentifier` 字段是你拥有的确定性 ID。 只需生成一次,并在多次同步过程中保持稳定不变。
* `applicationVariables` 会变成你的函数和前端组件可用的环境变量。 在逻辑函数(服务端)中,可以通过 `process.env.VARIABLE_NAME` 使用它们。 在前端组件中,使用 `twenty-sdk/front-component` 中的 `getApplicationVariable('VARIABLE_NAME')`。 标记为 `isSecret: true` 的变量只会注入到逻辑函数中。 前端组件只会接收非机密变量。
* 默认角色会根据使用 [`defineApplicationRole()`](/l/zh/developers/extend/apps/config/roles) 标记的角色文件自动检测——你不需要在 `defineApplication()` 中引用它。
* 在构建清单时会自动检测安装前、安装后和卸载函数——无需在 `defineApplication()` 中引用它们。
* 显式传递 `defaultRoleUniversalIdentifier` 仍然受支持以保持向后兼容性,但已弃用,推荐改用 `defineApplicationRole()`。
* `serverVariables` 是实例级的配置和机密信息(例如 API 密钥)。 与 `applicationVariables` 不同,它们不会在 manifest 中声明具体值——工作区运维人员会在应用设置中填写这些值,并且它们只有在被设置后才会被注入到逻辑函数中。
* 两种类型的变量都可以接受 `isDeprecated: true`。 使用它来停用变量而不是删除它:保留已声明的键可以保留存储的值(删除它会销毁操作员输入的值),并且变量仍然会被注入,所以你的代码仍然可以回退到它 — `process.env.NEW_API_KEY ?? process.env.API_KEY`。 一旦弃用变量没有任何值,它就会从应用的设置中消失,并且永远不会计入应用的配置检查中,因此 `isDeprecated` 的优先级高于 `isRequired`。
* 要在应用的 **Settings** 选项卡中渲染自定义配置界面(替换默认的变量配置部分),请在其独立文件中使用 [`defineSettingsFrontComponent()`](/l/zh/developers/extend/apps/layout/front-components#custom-settings-component) 声明一个前端组件。 每个应用只允许有一个。 系统管理的部分(自动升级、App URL、连接)将始终保持可见。
## 变量类型
`applicationVariables` 和 `serverVariables` 都接受一个可选的 `type`(且对于 `SELECT` / `MULTI_SELECT`,还可以接受一个 `options` 列表)。 支持的类型:`TEXT`(默认)、`BOOLEAN`、`NUMBER`、`NUMERIC`、`DATE`、`DATE_TIME`、`SELECT`、`MULTI_SELECT`、`ARRAY`、`RAW_JSON`、`RICH_TEXT`。
```ts src/application-config.ts
import { defineApplication, FieldType } from 'twenty-sdk/define';
export default defineApplication({
// ...identity, role...
applicationVariables: {
MAX_POSTCARDS: {
universalIdentifier: '5f4497e4-9030-4085-85eb-2c48b8d53713',
description: 'Maximum postcards per batch',
type: FieldType.NUMBER,
value: 10,
},
DEFAULT_REGION: {
universalIdentifier: '76c5c321-b6b6-46eb-b4fc-f9f04bb04227',
description: 'Default shipping region',
type: FieldType.SELECT,
options: [
{ label: 'Europe', value: 'eu' },
{ label: 'United States', value: 'us' },
],
value: 'eu',
},
},
});
```
`type` 只影响**展示和校验**——它会在工作区设置界面中选择匹配的输入控件(开关、数字字段、下拉框、日期选择器、JSON 编辑器等)。 并让构建过程校验你的配置(例如,`SELECT` / `MULTI_SELECT` 必须声明非空的 `options`)。 它**不会**改变该值到达你代码的方式。
值**始终以字符串注入**——这是环境变量固有的特性(`process.env.*` 只能是字符串)。 当你的逻辑函数运行时,执行器会在构建 `process.env` 时按声明的 `type` 序列化每个值,因此无论该值是如何设置的(清单默认值、设置界面或先前的版本),字符串格式都是一致的:
| 类型 | `process.env` 字符串 |
| ---------------------------------- | --------------------------------- |
| `TEXT`、`SELECT`、`DATE`、`DATE_TIME` | 原始值(`"eu"`、`"2026-01-01"`) |
| `BOOLEAN` | `"true"` / `"false"` |
| `NUMBER`、`NUMERIC` | 十进制字符串(`"10"`、`"2.5"`) |
| `MULTI_SELECT`、`ARRAY` | JSON 数组(`'["email","postcard"]'`) |
| `RAW_JSON`、`RICH_TEXT` | JSON 对象(`'{"retries":3}'`) |
将该字符串再解析回你所期望的类型:
```ts
const maxCards = Number(process.env.MAX_POSTCARDS); // "10" -> 10
const enabled = process.env.ENABLE_TRACKING === 'true'; // "true" -> true
const channels = JSON.parse(process.env.ENABLED_CHANNELS ?? '[]'); // '["email"]' -> ["email"]
const config = JSON.parse(process.env.PROVIDER_CONFIG ?? '{}'); // '{"retries":3}' -> { retries: 3 }
```
同样适用于通过 `getApplicationVariable('VARIABLE_NAME')` 读取值的前端组件——返回值是字符串;按需进行解析。
## 默认函数角色
使用 [`defineApplicationRole()`](/l/zh/developers/extend/apps/config/roles) 声明的角色控制应用的逻辑函数和前端组件可以访问的内容:
* 注入到您的逻辑函数中的运行时令牌来源于该角色。 以某个人的身份执行的调用会进一步受限于该人能够执行的操作,因此其权限永远不会超过两者中的任一方。 请参阅[调用使用谁的访问权限](/l/zh/developers/extend/apps/logic/logic-functions#whose-access-a-call-uses)。
* 类型化 API 客户端将受限于授予该角色的权限。
* 遵循最小权限原则:只声明你的函数所需的权限。
当你使用脚手架创建新应用时,CLI 会在 `src/roles/default-role.ts` 中创建一个入门角色文件。 完整参考请参见 [角色与权限](/l/zh/developers/extend/apps/config/roles)。
## 应用市场元数据
如果你计划[发布你的应用](/l/zh/developers/extend/apps/operations/publishing),这些可选字段将控制你的应用在应用市场中的展示:
| 字段 | 描述 |
| ------------------ | -------------------------------------------------------------- |
| `作者` | 作者或公司名称 |
| `类别` | 用于应用市场筛选的应用类别 |
| `logo` | 捆绑在`public/`中的应用徽标的路径(例如`public/logo.png`) |
| `galleryImages` | 在`public/`中捆绑的相册图像路径数组(例如`public/screenshot-1.png`) |
| `aboutDescription` | 用于“关于”选项卡的更长的 Markdown 描述。 如果省略,市场将使用该软件包在 npm 上的 `README.md`。 |
| `websiteUrl` | 你的网站链接 |
| `termsUrl` | 服务条款链接 |
| `emailSupport` | 支持电子邮件地址 |
| `issueReportUrl` | 问题跟踪器链接 |
<Note>
`logoUrl`和`screshots`被废弃的 `logo` 和 `GalleryImages` 的别名。 这些字段不支持外部绝对链接 (`http://` 或 `https://`) 。它们会在构建时被丢弃,并附有警告。 将图像捆绑在你的应用的 "public/" 文件夹中。
</Note>

View File

@@ -0,0 +1,188 @@
---
title: 安装钩子
description: 在安装、升级或卸载生命周期中运行逻辑——预置数据、备份记录、验证升级、清理外部资源。
icon: wrench
---
安装钩子是在安装、升级或卸载生命周期期间运行的特殊逻辑函数。 它们与常规的[逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions)共享相同的处理程序运行时,但使用自己的定义函数声明,并且存在于普通触发模型(HTTP、cron、数据库事件)之外。 安装钩子接收一个 `InstallPayload`(`{ previousVersion?: string; newVersion: string }`——在全新安装时 `previousVersion` 为 `undefined`);卸载钩子接收一个 `UninstallPayload`(`{ version?: string }`——要被移除的版本)。
每个应用每种钩子(预安装、后安装、卸载)最多只能定义一个。 如果检测到任一类型多于一个,清单构建将报错。
```
┌─────────────────────────────────────────────────────────────┐
│ install flow │
│ │
│ upload package → [pre-install] → metadata migration → │
│ generate SDK → [post-install] │
│ │
│ old schema visible new schema visible │
└─────────────────────────────────────────────────────────────┘
```
## 一览
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
| ---- | ------------------------------- | --------------------------------------------------------- |
| 运行 | 元数据迁移之前——**先前**的模式和数据仍然完好无损 | 迁移和 SDK 生成之后——**新的**模式已就位 |
| 执行 | 始终为同步;会阻塞安装 | 默认异步(排队,重试 3 次);可通过 `shouldRunSynchronously: true` 选择同步 |
| 失败时 | 安装在任何模式更改之前被**中止** | 异步:最多重试 3 次。 同步:调用方会收到 `POST_INSTALL_ERROR`(模式更改**不会**回滚) |
| 典型用途 | 备份或修复迁移会丢失的数据;通过抛出异常拒绝存在风险的升级 | 预填充默认数据、配置工作区、注册外部资源 |
**经验法则:** 默认使用 post-install。 仅当迁移本身具有破坏性,且你需要在其丢失之前拦截先前状态时,才使用安装前。
| 你想要... | 使用 |
| ------------------ | ------------------------------------------------ |
| 预填充数据、配置工作区、注册外部资源 | `post-install` |
| 不应阻塞安装响应的长时间运行任务 | `post-install`(默认异步模式,带工作线程重试) |
| 安装返回后调用方会立即依赖的快速设置 | `post-install`,配合 `shouldRunSynchronously: true` |
| 读取或备份即将被迁移丢失的数据 | `pre-install` |
| 拒绝会损坏现有数据的升级 | `pre-install`(从处理程序中抛出异常) |
| 在每次升级时执行对账 | 任一钩子配合 `shouldRunOnVersionUpgrade: true` |
## 两个钩子共享的行为
* 该配置等同于 `defineLogicFunction` 的配置减去触发器设置,再加上 `shouldRunOnVersionUpgrade`。
* **运行时机**:默认情况下,仅在全新安装时运行。 将 `shouldRunOnVersionUpgrade: true` 设为 true 以便在升级时也运行。 使用 `previousVersion` / `newVersion` 按升级路径分支处理。
* **幂等性很重要**:异步 post-install 可能会被重试,而且当开启 `shouldRunOnVersionUpgrade` 时,任一钩子都会在升级时重新运行。
* 会注入常规的逻辑函数环境(`APPLICATION_ID`、`APP_ACCESS_TOKEN`、`API_URL`),因此你可以使用应用的令牌调用 Twenty API。
* 该钩子会在构建时自动附加到应用清单上(`preInstallLogicFunction` / `postInstallLogicFunction`)——在 [`defineApplication()`](/l/zh/developers/extend/apps/config/application) 中无需额外引用。
* 默认的 `timeoutSeconds` 为 300,以便支持更长的设置任务,例如数据填充。
* **在开发模式下不会执行**:`yarn twenty dev` 会跳过安装流程并直接同步文件,因此钩子在其中不会运行。 改为手动触发它们:
```bash filename="Terminal"
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
```
<AccordionGroup>
<Accordion title="definePostInstallLogicFunction" description="在应用工作区元数据迁移之后运行">
在应用完成安装后运行:元数据已同步、SDK 客户端已生成、新模式可被查询。 示例——在全新安装时预填充一个默认记录:
```ts src/logic-functions/post-install.ts
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
if (previousVersion) return; // fresh installs only
const client = new CoreApiClient();
await client.mutation({
createPostCard: {
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
id: true,
},
});
};
export default definePostInstallLogicFunction({
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
name: 'post-install',
description: 'Seeds a welcome post card after install.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: false,
shouldRunSynchronously: false,
handler,
});
```
`shouldRunSynchronously` 标志控制执行模型:
* `false` *(默认)*——放入消息队列(`retryLimit: 3`)并由工作线程运行。 安装响应会在任务被放入队列后立即返回。 **用于长时间运行的任务**——例如预填充大型数据集、调用缓慢的第三方 API。
* `true`——在安装流程中内联执行。 安装请求会阻塞直至处理程序完成;抛出的错误会以 `POST_INSTALL_ERROR` 的形式暴露给调用方(不重试)。 **用于必须在返回响应前完成的快速任务。** 此时迁移已应用,因此失败不会回滚模式更改——只会将错误暴露出来。
</Accordion>
<Accordion title="definePreInstallLogicFunction" description="在应用工作区元数据迁移之前运行">
在元数据迁移之前、针对**先前**模式运行——适合在迁移会删除数据前对其进行备份,或拒绝存在风险的升级。 在执行之前,服务器会运行一次纯增量的“精简同步”,仅注册新版本的 pre-install 函数;当你的处理程序运行时,其他一切——上一版本的对象、字段和数据——都不会被触及。
安装前始终为**同步**,并会阻塞安装。 如果处理程序抛出异常,安装会在任何模式更改之前被中止——工作区将保持在上一版本且处于一致状态。 这是有意为之:安装前是你拒绝高风险升级的最后机会。
示例——在迁移删除旧字段之前复制该旧字段的值:
```ts src/logic-functions/pre-install.ts
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise<void> => {
// Only the 1.x → 2.x upgrade drops the legacy `notes` field.
if (!previousVersion?.startsWith('1.') || !newVersion.startsWith('2.')) {
return;
}
const client = new CoreApiClient();
const { postCards } = await client.query({
postCards: {
__args: { filter: { notes: { isNot: null } } },
edges: { node: { id: true, notes: true } },
},
});
// Copy legacy `notes` into `description` before the migration drops the
// column. If this fails, the upgrade aborts and the workspace stays on v1.
for (const { node } of postCards.edges) {
await client.mutation({
updatePostCard: {
__args: { id: node.id, data: { description: node.notes } },
id: true,
},
});
}
};
export default definePreInstallLogicFunction({
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'pre-install',
description: 'Backs up legacy notes into description before the v2 migration.',
timeoutSeconds: 300,
shouldRunOnVersionUpgrade: true,
handler,
});
```
</Accordion>
</AccordionGroup>
## 卸载钩子
`defineUninstallLogicFunction` 声明一个在用户卸载你的应用时运行的钩子。 它在应用的元数据、数据和代码被移除之前执行——一旦删除迁移运行完成,就不再有任何内容可供执行——因此你的处理程序仍然可以查询应用的对象和记录。 将其用于清理外部资源:取消预置 API 资源、删除剩余的机器人、撤销网络钩子。
备注:
* 该钩子是尽力执行:它同步运行,但失败会被记录,并且**绝不会阻止卸载**——清理操作绝不能导致应用无法被移除。
* 它接收 `UninstallPayload`(`{ version?: string }`——要被移除的版本)。
* 在回滚失败的全新安装时,它**不会**运行——该应用从未完成安装。
* 钩子无法在应用被删除后运行,因此依赖应用数据的外部清理(例如存储在记录中的机器人 ID)应放在这里,而不是放在外部计划任务中。
* 与安装钩子类似,它在开发模式下**不会执行**——请改为手动触发:
```bash filename="Terminal"
yarn twenty dev:function:exec --uninstall
```
```ts src/logic-functions/uninstall.ts
import { defineUninstallLogicFunction, type UninstallPayload } from 'twenty-sdk/define';
import { CoreApiClient } from 'twenty-client-sdk/core';
const handler = async (_payload: UninstallPayload): Promise<void> => {
const client = new CoreApiClient();
const { meetingBots } = await client.query({
meetingBots: { edges: { node: { id: true, externalBotId: true } } },
});
// Delete the provider-side bots so nothing keeps recording after uninstall.
for (const { node } of meetingBots.edges) {
await fetch(`https://api.recorder.example/bots/${node.externalBotId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.RECORDER_API_KEY}` },
});
}
};
export default defineUninstallLogicFunction({
universalIdentifier: 'b2c3d4e5-6789-01bc-def0-234567890abc',
name: 'uninstall',
description: 'Deletes remaining recorder bots when the app is uninstalled.',
timeoutSeconds: 300,
handler,
});
```

View File

@@ -0,0 +1,56 @@
---
title: 概览
description: 配置应用本身——它的身份、默认权限,以及在安装时运行的内容。
icon: screwdriver-wrench
---
Twenty 应用的 **配置层(config layer)** 用来向 *平台* 描述应用——它的身份、所持有的权限,以及在安装或升级期间运行的代码。 这些声明不会添加新的数据结构或运行时行为;它们告诉 Twenty *这个应用是谁*,以及 *如何完成设置*。
```text
┌────────────────────────────────────────────────────────┐
│ Application — identity, default role, variables, │
│ marketplace metadata │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Role — what the app's logic functions can read │ │
│ │ and write (referenced by Application) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
▼ (at install / upgrade time)
┌──────────────────────────────────┐
│ Pre-install hook │ before metadata migration
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Post-install hook │ after metadata migration
└──────────────────────────────────┘
│
▼ (at uninstall time)
┌──────────────────────────────────┐
│ Uninstall hook │ before app removal
└──────────────────────────────────┘
```
## 本节内容
<CardGroup cols={2}>
<Card title="应用程序配置" icon="rocket" href="/l/zh/developers/extend/apps/config/application">
`defineApplication` —— 身份、默认角色、变量、应用市场元数据。
</Card>
<Card title="角色和权限" icon="shield-halved" href="/l/zh/developers/extend/apps/config/roles">
`defineRole` —— 声明应用的逻辑函数可以读取和写入什么。
</Card>
<Card title="安装钩子" icon="wrench" href="/l/zh/developers/extend/apps/config/install-hooks">
`definePreInstallLogicFunction`、`definePostInstallLogicFunction` 和 `defineUninstallLogicFunction` —— 备份数据、写入默认值、验证升级、在卸载时清理。
</Card>
</CardGroup>
## 各部分之间的关系
* **Application** 是入口点。 每个应用都有且仅有一次 `defineApplication()` 调用,并将一个 **Role** 作为其默认角色。
* **Role** 控制应用的逻辑函数和前端组件可以读取和写入什么。 遵循最小权限原则:只授予代码实际需要的权限。
* **Install Hooks(安装钩子)** 在安装或升级过程中运行——预安装钩子在元数据迁移之前运行(因此可以拒绝存在风险的升级),后安装钩子在迁移之后运行(因此可以根据新模式写入默认数据)。 卸载钩子会在应用被移除之前立即运行,因此它可以在应用数据仍然可读时清理外部资源。
<Note>
安装钩子与 [logic function](/l/zh/developers/extend/apps/logic/logic-functions) 共用运行时——相同的处理器签名、相同的环境变量、相同的类型化 API 客户端——但它们使用各自的 define 函数声明,并且位于常规触发模型(HTTP、cron、数据库事件)之外。
</Note>

View File

@@ -0,0 +1,67 @@
---
title: 公共资源
description: 通过 public/ 文件夹将静态文件(图像、图标、字体)与应用一起发布。
icon: folder-open
---
应用根目录中的 `public/` 文件夹包含静态文件——图像、图标、字体,或应用在运行时所需的任何其他资源。 这些文件会在构建时自动包含、在开发模式下同步,并上传到服务器。
放置在 `public/` 中的文件:
* **公开可访问**——同步到服务器后,资源将通过公共 URL 提供服务。 访问它们无需身份验证。
* **在前端组件中可用**——使用资源 URL 在 React 组件中显示图像、图标或任何媒体。
* **在逻辑函数中可用**——在电子邮件、API 响应或任何服务端逻辑中引用资源 URL。
* **用于市场元数据**——`defineApplication()` 中的 `logo` 和 `galleryImages` 字段引用此文件夹中的文件(例如,`public/logo.png`)。 应用发布后,这些内容会显示在市场中。 对于这些字段,外部绝对 URL 会被忽略——请改为将图像打包到 `public/` 中。 每个图片文件大小不得超过 10 MB。
* **在开发模式下自动同步**——当在 `public/` 中添加、更新或删除文件时,会自动同步到服务器。 无需重启。
* **包含在构建中**——`yarn twenty dev:build` 会将所有公共资源打包到分发产物中。
## 使用 `getPublicAssetUrl` 访问公共资源
使用来自 `twenty-sdk` 的 `getPublicAssetUrl` 辅助函数获取 `public/` 目录中文件的完整 URL。 它可在 **逻辑函数** 和 **前端组件** 中使用。
**在逻辑函数中:**
```ts src/logic-functions/send-invoice.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const handler = async (): Promise<any> => {
const logoUrl = getPublicAssetUrl('logo.png');
const invoiceUrl = getPublicAssetUrl('templates/invoice.png');
// Fetch the file content (no auth required — public endpoint)
const response = await fetch(invoiceUrl);
const buffer = await response.arrayBuffer();
return { logoUrl, size: buffer.byteLength };
};
export default defineLogicFunction({
universalIdentifier: 'a1b2c3d4-...',
name: 'send-invoice',
description: 'Sends an invoice with the app logo',
timeoutSeconds: 10,
handler,
});
```
**在前端组件中:**
```tsx src/front-components/company-card.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { getPublicAssetUrl } from 'twenty-sdk/utils';
const CompanyCard = () => {
const logoUrl = getPublicAssetUrl('logo.png');
return <img src={logoUrl} alt="App logo" />;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'company-card',
component: CompanyCard,
});
```
`path` 参数是相对于应用的 `public/` 文件夹的。 `getPublicAssetUrl('logo.png')` 和 `getPublicAssetUrl('public/logo.png')` 均解析为相同的 URL——如果存在,`public/` 前缀会被自动移除。

View File

@@ -0,0 +1,203 @@
---
title: 角色和权限
description: 声明你的应用的逻辑函数和前端组件可以读取和写入哪些对象和字段。
icon: shield-halved
---
**角色(role)** 是一组权限:应用可以读取或写入哪些对象,可以查看哪些字段,以及可以使用哪些平台级能力。 每个应用的逻辑函数和前端组件都会继承使用 `defineApplicationRole()` 标记的角色的权限(参见下文的 [默认函数角色](#the-default-function-role))。
```ts src/roles/restricted-company-role.ts
import {
defineRole,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
SystemPermissionFlag,
} from 'twenty-sdk/define';
export default defineRole({
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
label: 'My new role',
description: 'A role that can be used in your workspace',
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
fieldPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
.universalIdentifier,
canReadFieldValue: false,
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
});
```
## 行级安全
对象和字段权限决定某个角色可以访问_哪些对象和字段_。 **行级
权限谓词**更进一步,决定某个角色可以查看和操作_哪些记录_——例如,一个自助式角色,其中每个外部用户只能看到自己的记录。
在角色上使用 `rowLevelPermissionPredicates` 来声明谓词。 与清单的其余部分类似,
每个谓词都有自己的 `universalIdentifier`,并引用一个对象和一个字段(通过它们的 `universalIdentifier`)、一个 `operand`,以及(可选的)在查询时注入其值的 workspaceMember 字段——这样你就可以表达:“记录的所有者关系**是**当前的工作区成员”:
```ts src/roles/partner-role.ts
import {
defineRole,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
export default defineRole({
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
label: 'Partner',
description: 'External partner — sees only its own records',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier,
},
],
});
```
由于谓词随清单一起提供,它们会在每次安装和升级时与该角色一起创建、更新和删除——不需要单独的安装后步骤来保持同步。
### 将谓词与组组合使用
默认情况下,角色的谓词通过 `AND` 进行组合。 要将其中一些谓词通过 `OR` 进行组合(或嵌套逻辑),请声明一个 `rowLevelPermissionPredicateGroups` 条目,并通过 `predicateGroupUniversalIdentifier` 将每个谓词指向该条目。 此角色允许合作伙伴查看其**拥有**或其是**联系人**的商机:
```ts src/roles/partner-opportunities-role.ts
import {
defineRole,
RowLevelPermissionPredicateGroupLogicalOperator,
RowLevelPermissionPredicateOperand,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
const CURRENT_MEMBER =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
.universalIdentifier;
export default defineRole({
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
label: 'Partner (opportunities)',
canBeAssignedToUsers: true,
objectPermissions: [
{
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
canReadObjectRecords: true,
},
],
rowLevelPermissionPredicateGroups: [
{
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
},
],
rowLevelPermissionPredicates: [
{
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
{
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
fieldUniversalIdentifier:
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
operand: RowLevelPermissionPredicateOperand.IS,
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
},
],
});
```
备注:
* 为每个谓词和组分配一个稳定的 `universalIdentifier`(任意 uuid)——它在各次升级中作为实体的键,谓词通过它来引用组。
* 谓词可以引用由你的应用拥有的对象和字段,或由 Twenty 的标准对象拥有的对象和字段。
* 行级安全会在包含该功能的套餐中的工作区上强制执行;在其他套餐中谓词仍会同步,只是不会被强制执行。
## 默认函数角色
当你使用脚手架创建新应用时,CLI 会创建一个使用 `defineApplicationRole()` 声明的默认角色文件:
```ts src/roles/default-role.ts
import { defineApplicationRole } from 'twenty-sdk/define';
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
'b648f87b-1d26-4961-b974-0908fd991061';
export default defineApplicationRole({
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
label: 'Default function role',
description: 'Default role for function Twenty client',
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
objectPermissions: [],
fieldPermissions: [],
permissionFlagUniversalIdentifiers: [],
});
```
`defineApplicationRole()` 是 `defineRole()` 的一个轻量包装,它会标记在应用安装时用作应用默认角色的**该**角色。 验证与 `defineRole` 完全相同,但构建流水线会自动将其 `universalIdentifier` 注入到应用清单的 `defaultRoleUniversalIdentifier` 中——因此你无需在 [`defineApplication`](/l/zh/developers/extend/apps/config/application) 中自行引用它。
备注:
* 每个应用只允许有且仅有 **一个** `defineApplicationRole(...)`——如果发现多于一个,清单构建将失败。
* 对于你的应用随附的任何**额外**角色,请使用 `defineRole()`(而不是 `defineApplicationRole()`)。
* 在 `defineApplication()` 上显式设置 `defaultRoleUniversalIdentifier` 仍然受支持以保持向后兼容性,但已经弃用,推荐使用 `defineApplicationRole()`。
## 最佳实践
* 从脚手架生成的角色开始,然后逐步收紧它的权限——默认角色授予广泛的读取访问权限,而这在生产环境中很少是你想要的。
* 将 `objectPermissions` 和 `fieldPermissions` 替换为你的函数实际需要的精确对象和字段。
* `permissionFlagUniversalIdentifiers` 控制对平台级能力的访问。 尽量保持最小化。
* 查看一个工作示例:[[`exampes/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts)