i18n - docs translations (#22715)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22715?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
a0cf4cc9e1
commit
ebee7d71b9
@@ -4,7 +4,7 @@ description: 在安装之前或之后运行逻辑——预置数据、备份记
|
||||
icon: wrench
|
||||
---
|
||||
|
||||
安装钩子是在安装或升级生命周期期间运行的特殊逻辑函数。 它们与常规的[逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions)共享相同的处理程序运行时,并接收 `InstallPayload`,但它们使用自己的定义函数声明——`definePostInstallLogicFunction()` 和 `definePreInstallLogicFunction()`——并且存在于普通触发模型(HTTP、cron、数据库事件)之外。
|
||||
安装钩子是在安装或升级生命周期期间运行的特殊逻辑函数。 它们与常规的[逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions)共享相同的处理程序运行时,并接收一个 `InstallPayload`(`{ previousVersion?: string; newVersion: string }`——在全新安装时 `previousVersion` 为 `undefined`),但它们使用自己的 define 函数声明,并且存在于普通触发模型(HTTP、cron、数据库事件)之外。
|
||||
|
||||
每个应用**最多只能定义一个安装前函数**和**最多一个安装后函数**。 如果检测到任一类型多于一个,清单构建将报错。
|
||||
|
||||
@@ -19,111 +19,59 @@ icon: wrench
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="在应用工作区元数据迁移之后运行">
|
||||
## 一览
|
||||
|
||||
安装后函数会在你的应用完成安装到某个工作区后自动运行。 服务器会在应用的元数据已同步并已生成 SDK 客户端**之后**执行它,因此工作区已完全可用,且新架构已就绪。 常见用例包括预置默认数据、创建初始记录、配置工作区设置,或在第三方服务上预配资源。
|
||||
| | `definePreInstallLogicFunction` | `definePostInstallLogicFunction` |
|
||||
| ---- | ------------------------------- | --------------------------------------------------------- |
|
||||
| 运行 | 元数据迁移之前——**先前**的模式和数据仍然完好无损 | 迁移和 SDK 生成之后——**新的**模式已就位 |
|
||||
| 执行 | 始终为同步;会阻塞安装 | 默认异步(排队,重试 3 次);可通过 `shouldRunSynchronously: true` 选择同步 |
|
||||
| 失败时 | 安装在任何模式更改之前被**中止** | 异步:最多重试 3 次。 同步:调用方会收到 `POST_INSTALL_ERROR`(模式更改**不会**回滚) |
|
||||
| 典型用途 | 备份或修复迁移会丢失的数据;通过抛出异常拒绝存在风险的升级 | 预填充默认数据、配置工作区、注册外部资源 |
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
**经验法则:** 默认使用 post-install。 仅当迁移本身具有破坏性,且你需要在其丢失之前拦截先前状态时,才使用安装前。
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
| 你想要... | 使用 |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| 预填充数据、配置工作区、注册外部资源 | `post-install` |
|
||||
| 不应阻塞安装响应的长时间运行任务 | `post-install`(默认异步模式,带工作线程重试) |
|
||||
| 安装返回后调用方会立即依赖的快速设置 | `post-install`,配合 `shouldRunSynchronously: true` |
|
||||
| 读取或备份即将被迁移丢失的数据 | `pre-install` |
|
||||
| 拒绝会损坏现有数据的升级 | `pre-install`(从处理程序中抛出异常) |
|
||||
| 在每次升级时执行对账 | 任一钩子配合 `shouldRunOnVersionUpgrade: true` |
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f7a2b9c1-3d4e-5678-abcd-ef9876543210',
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
## 两个钩子共享的行为
|
||||
|
||||
你也可以随时使用 CLI 手动执行安装后函数:
|
||||
* 该配置等同于 `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
|
||||
```
|
||||
|
||||
关键点:
|
||||
* 安装后函数使用 `definePostInstallLogicFunction()` — 这是一个省略触发器设置(`cronTriggerSettings`、`databaseEventTriggerSettings`、`httpRouteTriggerSettings`、`toolTriggerSettings`、`workflowActionTriggerSettings`)的专用变体。
|
||||
* 处理程序会接收一个 `InstallPayload`,其为 `{ previousVersion?: string; newVersion: string }` —— `newVersion` 是正在安装的版本,而 `previousVersion` 是先前已安装的版本(在全新安装时为 `undefined`)。 使用这些值来区分全新安装与升级,并运行特定版本的迁移逻辑。
|
||||
* **钩子何时运行**:默认情况下,仅在全新安装时运行。 如果还希望在应用从旧版本升级时运行,请传入 `shouldRunOnVersionUpgrade: true`。 若省略,该标志默认为 `false`,升级将跳过该钩子。
|
||||
* **执行模型 — 默认异步,可选择同步**:`shouldRunSynchronously` 标志控制安装后*如何*执行。
|
||||
* `shouldRunSynchronously: false` *(默认)* — 该钩子会**加入消息队列**,设置 `retryLimit: 3`,并在工作线程中异步运行。 作业一入列,安装响应即返回,因此缓慢或失败的处理程序不会阻塞调用方。 工作线程最多会重试三次。 **将其用于长时间运行的作业**——预填充大型数据集、调用缓慢的第三方 API、预配外部资源,以及任何可能超出合理 HTTP 响应窗口的任务。
|
||||
* `shouldRunSynchronously: true` — 该钩子会在安装流程中**内联执行**(与安装前使用相同的执行器)。 安装请求将阻塞直至处理程序完成;若抛出异常,安装调用方将收到 `POST_INSTALL_ERROR`。 不进行自动重试。 **用于需要在响应前完成的快速工作**——例如向用户返回验证错误,或进行安装调用返回后客户端将立即依赖的快速设置。 请注意,运行安装后时,元数据迁移已应用完成,因此同步模式下的失败**不会**回滚架构更改——它只会暴露错误。
|
||||
* 确保你的处理程序是幂等的。 在异步模式下,队列最多可重试三次;在任一模式下,当 `shouldRunOnVersionUpgrade: true` 时,该钩子在升级时可能再次运行。
|
||||
* 在处理程序内可使用环境变量 `APPLICATION_ID`、`APP_ACCESS_TOKEN` 和 `API_URL`(与其他逻辑函数相同),因此你可以使用作用域限定到你应用的应用访问令牌调用 Twenty API。
|
||||
* 每个应用仅允许一个安装后函数。 如果检测到多个,清单构建将报错。
|
||||
* 构建期间,函数的 `universalIdentifier`、`shouldRunOnVersionUpgrade` 和 `shouldRunSynchronously` 会自动附加到应用清单的 `postInstallLogicFunction` 字段下——你无需在 [`defineApplication()`](/l/zh/developers/extend/apps/config/application) 中引用它们。
|
||||
* 默认超时时间设置为 300 秒(5 分钟),以便支持更长的设置任务,如数据填充。
|
||||
* **开发模式下不执行**:当应用在本地注册(通过 `yarn twenty dev`)时,服务器会完全跳过安装流程,并通过 CLI 监视器直接同步文件——因此无论 `shouldRunSynchronously` 如何,安装后在开发模式下都不会运行。 使用 `yarn twenty dev:function:exec --postInstall` 在运行中的工作区上手动触发它。
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="definePreInstallLogicFunction" description="在应用工作区元数据迁移之前运行">
|
||||
|
||||
安装前函数会在安装期间自动运行,**在应用工作区元数据迁移之前**。 它与安装后共享相同的负载结构(`InstallPayload`),但在安装流程中位置更早,因此可以准备即将到来的迁移所依赖的状态——典型用例如备份数据、验证与新架构的兼容性,或归档即将被重构或删除的记录。
|
||||
|
||||
```ts src/logic-functions/pre-install.ts
|
||||
import { definePreInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
|
||||
const handler = async (payload: InstallPayload): Promise<void> => {
|
||||
console.log('Pre install logic function executed successfully!', payload.previousVersion);
|
||||
};
|
||||
|
||||
export default definePreInstallLogicFunction({
|
||||
universalIdentifier: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
|
||||
name: 'pre-install',
|
||||
description: 'Runs before installation to prepare the application.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: true,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
你也可以随时使用 CLI 手动执行安装前函数:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
关键点:
|
||||
* 安装前函数使用 `definePreInstallLogicFunction()`——与安装后相同的专用配置,只是附加到不同的生命周期阶段。
|
||||
* 安装前和安装后处理程序接收相同的 `InstallPayload` 类型:`{ previousVersion?: string; newVersion: string }`。 导入一次,可在两个钩子中复用。
|
||||
* **钩子何时运行**:位于工作区元数据迁移(`synchronizeFromManifest`)之前。 在执行之前,服务器会运行一次纯增量的“精简同步”,将**新**版本的安装前函数注册到工作区元数据中——不会触及其他任何内容——然后再执行它。 由于此次同步仅为增量操作,当你的处理程序运行时,上一版本的对象、字段和数据仍完好无损:你可以安全地读取并备份迁移前的状态。
|
||||
* **执行模型**:安装前以**同步**方式执行,并且**会阻塞安装**。 如果处理程序抛出异常,安装会在任何架构更改应用之前被中止——工作区将保持在上一版本且处于一致状态。 这是有意为之:安装前是你拒绝高风险升级的最后机会。
|
||||
* 与安装后相同,每个应用仅允许一个安装前函数。 在构建期间,它会自动附加到应用清单的 `preInstallLogicFunction` 下。
|
||||
* **开发模式下不执行**:与安装后相同——对于本地注册的应用将完全跳过安装流程,因此在 `yarn twenty dev` 下不会运行安装前。 使用 `yarn twenty dev:function:exec --preInstall` 手动触发它。
|
||||
<AccordionGroup>
|
||||
<Accordion title="definePostInstallLogicFunction" description="在应用工作区元数据迁移之后运行">
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="安装前 vs 安装后:何时使用哪一个" description="选择合适的安装钩子">
|
||||
|
||||
两个钩子都属于同一安装流程,并接收相同的 `InstallPayload`。 区别在于它们相对于工作区元数据迁移**何时**运行,这会影响它们可以安全访问的数据范围。
|
||||
|
||||
安装前始终为**同步**(会阻塞安装并可中止它)。 安装后**默认异步**——在工作线程中入列并自动重试——但可通过 `shouldRunSynchronously: true` 选择同步执行。 关于各模式的使用场景,请参见上方的 `definePostInstallLogicFunction` 折叠面板。
|
||||
|
||||
**对于需要新架构已存在的任何事项,请使用 `post-install`。** 这是最常见的情况:
|
||||
|
||||
* 针对新添加的对象和字段预填充默认数据(创建初始记录、默认视图、演示内容)。
|
||||
* 在应用已有凭据的前提下,向第三方服务注册 Webhook。
|
||||
* 调用你自己的 API 完成依赖已同步元数据的设置。
|
||||
* 用于在每次升级时对状态进行对账的幂等“确保其存在”逻辑——结合 `shouldRunOnVersionUpgrade: true` 使用。
|
||||
|
||||
示例——在安装后预填充一个默认的 `PostCard` 记录:
|
||||
在应用完成安装后运行:元数据已同步、SDK 客户端已生成、新模式可被查询。 示例——在全新安装时预填充一个默认记录:
|
||||
|
||||
```ts src/logic-functions/post-install.ts
|
||||
import { definePostInstallLogicFunction, type InstallPayload } from 'twenty-sdk/define';
|
||||
import { createClient } from './generated/client';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async ({ previousVersion }: InstallPayload): Promise<void> => {
|
||||
if (previousVersion) return; // fresh installs only
|
||||
|
||||
const client = createClient();
|
||||
await client.postCard.create({
|
||||
data: { title: 'Welcome to Postcard', content: 'Your first card!' },
|
||||
const client = new CoreApiClient();
|
||||
await client.mutation({
|
||||
createPostCard: {
|
||||
__args: { data: { name: 'Welcome to Postcard', content: 'Your first card!' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,22 +81,28 @@ export default definePostInstallLogicFunction({
|
||||
description: 'Seeds a welcome post card after install.',
|
||||
timeoutSeconds: 300,
|
||||
shouldRunOnVersionUpgrade: false,
|
||||
shouldRunSynchronously: false,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
**当迁移可能破坏或损坏现有数据时,请使用 `pre-install`。** 由于安装前在*先前*架构上运行,且其失败会回滚升级,因此凡是有风险的操作都应放在这里:
|
||||
`shouldRunSynchronously` 标志控制执行模型:
|
||||
|
||||
* **备份即将被删除或重构的数据**——例如,你在 v2 中移除某个字段,需要在迁移运行前将其值复制到另一个字段或导出到存储中。
|
||||
* **归档会被新约束判为无效的记录**——例如某个字段将变为 `NOT NULL`,你需要先删除或修正具有空值的行。
|
||||
* **验证兼容性;若当前数据无法干净迁移则拒绝升级**——从处理程序中抛出异常,安装将中止且不会应用任何更改。 这比在迁移中途才发现不兼容要更安全。
|
||||
* **在会导致关联丢失的架构更改之前**对数据进行重命名或重新设置键。
|
||||
* `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 { createClient } from './generated/client';
|
||||
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.
|
||||
@@ -156,24 +110,24 @@ const handler = async ({ previousVersion, newVersion }: InstallPayload): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
const legacyRecords = await client.postCard.findMany({
|
||||
where: { notes: { isNotNull: true } },
|
||||
const client = new CoreApiClient();
|
||||
const { postCards } = await client.query({
|
||||
postCards: {
|
||||
__args: { filter: { notes: { isNot: null } } },
|
||||
edges: { node: { id: true, notes: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (legacyRecords.length === 0) return;
|
||||
|
||||
// Copy legacy `notes` into the new `description` field before the migration
|
||||
// drops the `notes` column. If this fails, the upgrade is aborted and the
|
||||
// workspace stays on v1 with all data intact.
|
||||
await Promise.all(
|
||||
legacyRecords.map((record) =>
|
||||
client.postCard.update({
|
||||
where: { id: record.id },
|
||||
data: { description: record.notes },
|
||||
}),
|
||||
),
|
||||
);
|
||||
// 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({
|
||||
@@ -186,21 +140,5 @@ export default definePreInstallLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
**经验法则:**
|
||||
|
||||
| 你想要... | 使用 |
|
||||
| ----------------------- | ------------------------------------------------------------- |
|
||||
| 预填充默认数据、配置工作区、注册外部资源 | `post-install` |
|
||||
| 运行不应阻塞安装响应的长时间预填充或第三方调用 | `post-install` (默认 — `shouldRunSynchronously: false`,由工作线程重试) |
|
||||
| 运行安装调用返回后调用方将立即依赖的快速设置 | `post-install`,配合 `shouldRunSynchronously: true` |
|
||||
| 读取或备份即将被迁移丢失的数据 | `pre-install` |
|
||||
| 拒绝会损坏现有数据的升级 | `pre-install`(从处理程序中抛出异常) |
|
||||
| 在每次升级时执行对账 | `post-install` 配合 `shouldRunOnVersionUpgrade: true` |
|
||||
| 仅在首次安装时执行一次性设置 | `post-install` 配合 `shouldRunOnVersionUpgrade: false`(默认) |
|
||||
|
||||
<Note>
|
||||
如有不确定,默认选择**安装后(post-install)**。 仅当迁移本身具有破坏性,且你需要在其丢失之前拦截先前状态时,才使用安装前。
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -86,6 +86,22 @@ export default defineObject({
|
||||
**基础字段会自动添加。** 当你定义自定义对象时,Twenty 会为你创建标准字段,例如 `id`、`name`、`createdAt`、`updatedAt`、`createdBy`、`updatedBy` 和 `deletedAt`。 你无需在 `fields` 数组中声明这些字段——只需声明你的自定义字段。 你可以通过声明一个同名字段来覆盖默认字段,但这么做通常并不是一个好主意。
|
||||
</Note>
|
||||
|
||||
## 字段类型
|
||||
|
||||
从 `twenty-sdk/define` 导出的完整 `FieldType` 值集合:
|
||||
|
||||
| 类别 | 类型 |
|
||||
| ------ | ----------------------------------------------------------------------------------------------------------- |
|
||||
| 文本 | `TEXT`、`RICH_TEXT`、`ARRAY`(字符串数组)、`RAW_JSON` |
|
||||
| 数值 | `NUMBER`(`universalSettings.dataType`:`'float'` / `'int'` / `'bigint'`)、`NUMERIC`(任意精度)、`RATING`、`POSITION` |
|
||||
| 日期 | `DATE`, `DATE_TIME` |
|
||||
| 选项 | `BOOLEAN`、`SELECT`、`MULTI_SELECT` |
|
||||
| 复合 | `FULL_NAME`、`ADDRESS`、`EMAILS`、`PHONES`、`LINKS`、`CURRENCY`、`ACTOR`、`FILES` |
|
||||
| 标识符和关联 | `UUID`、`RELATION`、`MORPH_RELATION`(参见 [Relations](/l/zh/developers/extend/apps/data/relations)) |
|
||||
| 系统 | `TS_VECTOR`(全文搜索向量,由服务器管理) |
|
||||
|
||||
复合类型会存储多个子字段(例如,`FULL_NAME` = 名 + 姓;`CURRENCY` = `amountMicros` + `currencyCode`)。 `SELECT` 和 `MULTI_SELECT` 需要一个如上示例所示的 `options` 数组。
|
||||
|
||||
## 默认值
|
||||
|
||||
字面量字符串默认值必须在字符串**内部**用单引号包裹——应写成 `defaultValue: "'Draft'"`,而不是 `defaultValue: "Draft"`。 这就是上面的 `status` 字段使用 `` `'${PostCardStatus.DRAFT}'` `` 的原因。
|
||||
|
||||
+32
-16
@@ -14,26 +14,39 @@ my-twenty-app/
|
||||
default-role.ts # Permissions for logic functions
|
||||
constants/
|
||||
universal-identifiers.ts # Auto-generated UUIDs and metadata
|
||||
front-components/
|
||||
main-page.tsx # Welcome page component
|
||||
navigation-menu-items/
|
||||
main-page.navigation-menu-item.ts # Sidebar entry for the welcome page
|
||||
page-layouts/
|
||||
main-page.page-layout.ts # Standalone page hosting the component
|
||||
__tests__/
|
||||
setup-test.ts
|
||||
app-install.integration-test.ts
|
||||
.github/workflows/ci.yml # GitHub Actions
|
||||
public/ # Static assets
|
||||
vitest.config.ts # Test runner config
|
||||
application-config.test.ts # Unit test
|
||||
global-setup.ts # Integration test setup (sync + uninstall)
|
||||
schema.integration-test.ts # Integration test against a live server
|
||||
.github/workflows/
|
||||
ci.yml # Lint, typecheck, unit + integration tests
|
||||
cd.yml # Deploy + install on push to main
|
||||
public/
|
||||
logo.svg # Static assets
|
||||
vitest.config.ts # Integration test runner config
|
||||
vitest.unit.config.ts # Unit test runner config
|
||||
tsconfig.json, tsconfig.spec.json
|
||||
.nvmrc, .yarnrc.yml, .oxlintrc.json
|
||||
README.md, LLMS.md
|
||||
README.md, AGENTS.md, CLAUDE.md
|
||||
```
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 / 文件夹 | 目的 |
|
||||
| ---------------------------------------- | ------------------------- |
|
||||
| `src/application-config.ts` | **必需。** 应用的主配置文件。 |
|
||||
| `src/default-role.ts` | 默认角色,用于控制你的逻辑函数可访问的内容。 |
|
||||
| `src/constants/universal-identifiers.ts` | 自动生成的 UUID 和元数据(显示名称、描述)。 |
|
||||
| `src/__tests__/` | 集成测试(设置 + 示例测试)。 |
|
||||
| `public/` | 随应用一起提供的静态资源(图像、字体)。 |
|
||||
| 文件 / 文件夹 | 目的 |
|
||||
| -------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `src/application-config.ts` | **必需。** 应用的主配置文件。 |
|
||||
| `src/default-role.ts` | 默认角色,用于控制你的逻辑函数可访问的内容。 |
|
||||
| `src/constants/universal-identifiers.ts` | 自动生成的 UUID 和元数据(显示名称、描述)。 |
|
||||
| `src/front-components/`, `src/navigation-menu-items/`, `src/page-layouts/` | 一个入门欢迎页面:由独立页面布局渲染的前端组件,可从侧边栏访问。 |
|
||||
| `src/__tests__/` | 一个单元测试加一个集成测试(带有其全局设置),用于将应用与真实服务器进行同步。 |
|
||||
| `public/` | 随应用一起提供的静态资源(图像、字体)。 |
|
||||
| `AGENTS.md` / `CLAUDE.md` | 为在该应用上工作的 AI 编码代理提供指导。 |
|
||||
|
||||
<Note>
|
||||
**文件组织由你决定。** 上述文件夹只是约定——SDK 通过对 `export default defineEntity(...)` 调用进行 AST 分析来检测实体,而不受文件所在位置影响。
|
||||
@@ -47,15 +60,18 @@ my-twenty-app/
|
||||
{
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"twenty-client-sdk": "^2.13.0",
|
||||
"twenty-sdk": "^2.13.0"
|
||||
"twenty-client-sdk": "2.20.0",
|
||||
"twenty-sdk": "2.20.0",
|
||||
"twenty-ui": "1.0.0-alpha.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
脚手架工具将 `twenty-sdk` 和 `twenty-client-sdk` 固定为与自身相同的版本——升级时请保持这两者同步。
|
||||
|
||||
* **`twenty-sdk`** 提供 `twenty` CLI 以及构建/脚手架工具。 它只在开发和构建阶段运行,且永远不会在已发布应用的运行时环境中被导入。
|
||||
* **`twenty-client-sdk`** 会被你的应用代码(`CoreApiClient`、`MetadataApiClient`、`RestApiClient`)导入,但 Twenty 会在运行时提供它——逻辑函数从生成的 SDK 层获取它,前端组件则从服务器提供的模块中解析它。 你安装的副本仅用于类型检查和部署时的构建,因此不需要被打包进已部署的 bundle 中。
|
||||
|
||||
把任意一个软件包放在 `dependencies` 下,都会把它拉入已安装应用的运行时 bundle 中,在那里只是累赘。 当任一软件包仍然列在 `dependencies` 下时,`twenty build` 会发出警告。
|
||||
把任意一个软件包放在 `dependencies` 下,都会把它拉入已安装应用的运行时 bundle 中,在那里只是累赘。 当任一软件包仍然列在 `dependencies` 下时,`twenty dev:build` 会发出警告。
|
||||
|
||||
像往常一样,把你的应用自身的运行时依赖项(逻辑函数在运行时实际导入的库)添加到 `dependencies` 下。
|
||||
|
||||
@@ -6,17 +6,17 @@ description: 几分钟内创建你的第一个 Twenty 应用。
|
||||
|
||||
## 先决条件
|
||||
|
||||
* **Node.js 24+** — [在此下载](https://nodejs.org/)
|
||||
* **Node.js 24.5+** — [在此下载](https://nodejs.org/)
|
||||
* **Yarn 4** — 通过 Corepack 随 Node.js 提供。 启用它:`corepack enable`
|
||||
* **Docker** — [在此下载](https://www.docker.com/products/docker-desktop/)。 运行本地 Twenty 服务器所需。 如果你已经在其他地方运行了 Twenty,请跳过。
|
||||
|
||||
构建一个 Twenty 应用包含三个阶段。 脚手架工具将它们合并为一个理想路径的命令,但每个阶段都是独立的概念——当出现问题时,知道自己处于哪个阶段可以指明需要修复什么。
|
||||
|
||||
| 阶段 | 你要做什么 | 工具 | 结果 |
|
||||
| ------------ | -------------------- | ----------------------------- | -------------------- |
|
||||
| **1. 脚手架** | 生成应用的源代码 | `npx create-twenty-app` | 磁盘上的一个 TypeScript 项目 |
|
||||
| **2. 运行服务器** | 启动一个 Twenty 服务器以进行同步 | Docker + `yarn twenty server` | 一个正在运行的 Twenty 实例 |
|
||||
| **3. 同步** | 将你的代码实时同步到服务器 | `yarn twenty dev` | 你的更改会出现在 UI 中 |
|
||||
| 阶段 | 你要做什么 | 工具 | 结果 |
|
||||
| ------------ | -------------------- | ----------------------------------- | -------------------- |
|
||||
| **1. 脚手架** | 生成应用的源代码 | `npx create-twenty-app` | 磁盘上的一个 TypeScript 项目 |
|
||||
| **2. 运行服务器** | 启动一个 Twenty 服务器以进行同步 | Docker + `yarn twenty docker:start` | 一个正在运行的 Twenty 实例 |
|
||||
| **3. 同步** | 将你的代码实时同步到服务器 | `yarn twenty dev` | 你的更改会出现在 UI 中 |
|
||||
|
||||
---
|
||||
|
||||
@@ -28,7 +28,7 @@ description: 几分钟内创建你的第一个 Twenty 应用。
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
系统会提示你输入名称和描述——按下 **Enter** 采用默认值。 这将在 `my-twenty-app/` 中生成一个 TypeScript 项目,包含一个入门版的 `application-config.ts`、一个默认角色、一个 CI 工作流,以及一个集成测试。
|
||||
脚手架是非交互式的:目录名称会成为应用名称。 传递 `--display-name` 和 `--description` 来自定义生成的元数据(你也可以稍后在 `src/constants/universal-identifiers.ts` 中进行编辑)。 这将在 `my-twenty-app/` 中生成一个 TypeScript 项目,包含一个入门版的 `application-config.ts`、一个默认角色、CI/CD 工作流,以及一个集成测试。
|
||||
|
||||
**完成此阶段后:** 你的机器上已有该应用的源代码。 它还未运行——那是第 2 阶段的内容。
|
||||
|
||||
@@ -38,28 +38,14 @@ npx create-twenty-app@latest my-twenty-app
|
||||
|
||||
你的应用需要一个 Twenty 服务器来进行同步。 该服务器是一个完整的 Twenty 实例——包含 UI、GraphQL API、PostgreSQL——在本地的 Docker 中运行。 你的本地代码会将其定义上传到该服务器,从而使其显示在 UI 中。
|
||||
|
||||
脚手架工具会为你提供启动它的选项:
|
||||
脚手架会为你启动一个:在 Docker 正在运行的情况下,它会拉取 `twentycrm/twenty-app-dev` 镜像,在端口 `2020` 上启动,并将 CLI 认证到预置的演示工作区(`tim@apple.dev`)——无需登录。
|
||||
|
||||
> **是否要设置本地 Twenty 实例?**
|
||||
|
||||
* **是(推荐)** — 将拉取 `twentycrm/twenty-app-dev` Docker 镜像,并在端口 `2020` 上启动它。 请先确保 Docker 正在运行。
|
||||
* **否** — 如果你已经有一个想要连接的 Twenty 服务器,请选择此项。 你可以稍后通过 `yarn twenty remote:add` 将其连接起来。
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="是否启动本地实例?" />
|
||||
</div>
|
||||
|
||||
服务器启动后,浏览器会打开登录页面。 使用预置的演示账户:
|
||||
|
||||
* **邮箱:** `tim@apple.dev`
|
||||
* **密码:** `tim@apple.dev`
|
||||
若要改为连接到现有的 Twenty 服务器,请传入 `--url \<your-server-url>`。 远程服务器通过 OAuth 进行身份验证:会打开一个浏览器窗口,你可以登录并点击 **Authorize**,从而授予 CLI 访问你工作区的权限。 (你也可以在本地选择使用 OAuth,方式是添加 `--authentication-method oauth` —— 使用 `tim@apple.dev` / `tim@apple.dev` 登录。)
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty 登录界面" />
|
||||
</div>
|
||||
|
||||
在下一屏点击 **Authorize** —— 这将授予 CLI 访问你工作区的权限。
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI 授权界面" />
|
||||
</div>
|
||||
@@ -117,28 +103,32 @@ yarn twenty dev
|
||||
|
||||
### 用于 CI 和脚本的一次性同步
|
||||
|
||||
传入 `--once` 以执行一次构建与同步后退出——相同的流水线,无文件监视器:
|
||||
使用 `plan` 和 `apply` 在无监视器的情况下各运行一次相同的流水线:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once
|
||||
yarn twenty plan # preview the metadata changes without applying them
|
||||
yarn twenty apply # show the plan, then apply it
|
||||
```
|
||||
|
||||
| 命令 | 行为 | 适用场景 |
|
||||
| ---------------------------------- | -------------------------------- | ------------------------------- |
|
||||
| `yarn twenty dev` | 监视并在每次更改时重新同步。 持续运行,直到你将其停止。 | 交互式本地开发。 |
|
||||
| `yarn twenty dev --once` | 单次构建与同步,成功时以 `0` 退出,失败时以 `1` 退出。 | CI、pre-commit 钩子、AI 智能体、脚本化工作流。 |
|
||||
| `yarn twenty dev --once --dry-run` | 构建并打印元数据更改,**但不会应用这些更改**。 | 在提交同步之前检查它会更改哪些内容。 |
|
||||
| 命令 | 行为 | 适用场景 |
|
||||
| ------------------- | ------------------------------------------------------------------ | ------------------------------- |
|
||||
| `yarn twenty dev` | 监视并在每次更改时重新同步。 持续运行,直到你将其停止。 | 交互式本地开发。 |
|
||||
| `yarn twenty apply` | 单次构建与同步,成功时以 `0` 退出,失败时以 `1` 退出。 在执行破坏性变更前会要求确认(传入 `--force` 可跳过)。 | CI、pre-commit 钩子、AI 智能体、脚本化工作流。 |
|
||||
| `yarn twenty plan` | 构建并打印元数据更改,**但不会应用这些更改**。 | 在提交同步之前检查它会更改哪些内容。 |
|
||||
|
||||
两种模式都需要经过身份验证的远程仓库。 有关 `--dry-run` 的更多信息,请参见 [同步与恢复](/l/zh/developers/extend/apps/operations/sync-and-recovery#previewing-changes-dry-run)。
|
||||
所有模式都需要经过身份验证的远程服务器。 有关 `plan` 的更多信息,请参见 [同步与恢复](/l/zh/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan)。
|
||||
|
||||
<Note>
|
||||
`yarn twenty dev --once` 和 `yarn twenty dev --once --dry-run` 是 `yarn twenty apply` 和 `yarn twenty plan` 的已弃用别名。
|
||||
</Note>
|
||||
|
||||
### 开发模式选项
|
||||
|
||||
| 标志 | 描述 |
|
||||
| ------------------------------------- | -------------------------------------------- |
|
||||
| `--once` | 仅构建并同步一次,然后退出。 |
|
||||
| `--dry-run` | 使用 `--once` 时,可在不应用元数据更改的情况下预览这些更改。 不写入任何内容。 |
|
||||
| `--debounceMs \<ms>` | 以毫秒为单位设置文件更改的防抖延迟(默认值:`2000`)。 |
|
||||
| `--verbose` / `--debug` | 显示详细的构建日志、同步请求和错误跟踪。 |
|
||||
| 标志 | 描述 |
|
||||
| ------------------------------------- | ------------------------------ |
|
||||
| `--force` | 在不经确认的情况下应用破坏性变更(删除)。 |
|
||||
| `--debounceMs \<ms>` | 以毫秒为单位设置文件更改的防抖延迟(默认值:`1000`)。 |
|
||||
| `--verbose` / `--debug` | 显示详细的构建日志、同步请求和错误跟踪。 |
|
||||
|
||||
## 你可以构建的内容
|
||||
|
||||
|
||||
@@ -22,18 +22,22 @@ yarn twenty dev:add frontComponent
|
||||
|
||||
## 可用的实体类型
|
||||
|
||||
| 实体类型 | 命令 | 生成的文件 |
|
||||
| ----- | ---------------------------------------- | ------------------------------------------------------- |
|
||||
| 对象 | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
|
||||
| 字段 | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
|
||||
| 逻辑函数 | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| 前端组件 | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| 角色 | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
|
||||
| 技能 | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
|
||||
| 代理 | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
|
||||
| 视图 | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
|
||||
| 导航菜单项 | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| 页面布局 | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
| 实体类型 | 命令 | 生成的文件 |
|
||||
| ------- | ---------------------------------------- | ------------------------------------------------------- |
|
||||
| 对象 | `yarn twenty dev:add object` | `src/objects/\<name>.ts` |
|
||||
| 字段 | `yarn twenty dev:add field` | `src/fields/\<name>.ts` |
|
||||
| 逻辑函数 | `yarn twenty dev:add logicFunction` | `src/logic-functions/\<name>.ts` |
|
||||
| 前端组件 | `yarn twenty dev:add frontComponent` | `src/front-components/\<name>.tsx` |
|
||||
| 角色 | `yarn twenty dev:add role` | `src/roles/\<name>.ts` |
|
||||
| 技能 | `yarn twenty dev:add skill` | `src/skills/\<name>.ts` |
|
||||
| 代理 | `yarn twenty dev:add agent` | `src/agents/\<name>.ts` |
|
||||
| 视图 | `yarn twenty dev:add view` | `src/views/\<name>.ts` |
|
||||
| 导航菜单项 | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/\<name>.ts` |
|
||||
| 页面布局 | `yarn twenty dev:add pageLayout` | `src/page-layouts/\<name>.ts` |
|
||||
| 页面布局选项卡 | `yarn twenty dev:add pageLayoutTab` | `src/page-layout-tabs/\<name>.ts` |
|
||||
| 命令菜单项 | `yarn twenty dev:add commandMenuItem` | `src/command-menu-items/\<name>.ts` |
|
||||
| 视图字段 | `yarn twenty dev:add viewField` | `src/view-fields/\<name>.ts` |
|
||||
| 连接提供方 | `yarn twenty dev:add connectionProvider` | `src/connection-providers/\<name>.ts` |
|
||||
|
||||
## 脚手架生成的内容
|
||||
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@ icon: wrench
|
||||
---
|
||||
|
||||
* **Docker 错误** — 在运行 `yarn twenty docker:start` 之前,请确保 Docker Desktop(或守护进程)已在运行。 错误消息会显示适用于你的操作系统的正确启动命令。
|
||||
* **Node 版本不正确** — 需要 24+。 使用 `node -v` 检查。
|
||||
* **错误的 Node 版本** — 需要 24.5+(`engines.node: ^24.5.0`)。 使用 `node -v` 检查。
|
||||
* **缺少 Yarn 4** — 运行 `corepack enable`。
|
||||
* **依赖损坏** — `rm -rf node_modules && yarn install`。
|
||||
* **`twenty-sdk` 升级到 v2.8.0 后出现错误** — 在 v2.8.0 中,它从 `dependencies` 移动到了 `devDependencies`。 请参阅[项目结构 → 依赖项](/l/zh/developers/extend/apps/getting-started/project-structure#dependencies)。
|
||||
* **`twenty build` 会对 `dependencies` 下的 `twenty-client-sdk` 发出警告** — 它由 Twenty 在运行时提供,因此应与 `twenty-sdk` 一起移动到 `devDependencies` 中。 请参阅[项目结构 → 依赖项](/l/zh/developers/extend/apps/getting-started/project-structure#dependencies)。
|
||||
* **`twenty dev:build` 会对 `dependencies` 下的 `twenty-client-sdk` 发出警告** — 它由 Twenty 在运行时提供,因此应与 `twenty-sdk` 一起移动到 `devDependencies` 中。 请参阅[项目结构 → 依赖项](/l/zh/developers/extend/apps/getting-started/project-structure#dependencies)。
|
||||
|
||||
卡住了吗? 在 [Twenty 的 Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) 上提问。
|
||||
|
||||
@@ -13,7 +13,6 @@ export default defineCommandMenuItem({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
label: 'Open Dashboard',
|
||||
shortLabel: 'Dashboard',
|
||||
icon: 'IconLayoutDashboard',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
@@ -22,51 +21,23 @@ export default defineCommandMenuItem({
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 字段 | 必填 | 描述 |
|
||||
| --------------------------------------- | -- | -------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | 是 | 该命令的稳定唯一 ID |
|
||||
| `label` | 是 | 在命令菜单(Cmd+K)中显示的完整标签 |
|
||||
| `frontComponentUniversalIdentifier` | 是 | 此命令打开的前端组件的 `universalIdentifier` |
|
||||
| `shortLabel` | 否 | 固定的快速操作按钮上显示的较短标签 |
|
||||
| `icon` | 否 | 显示在标签旁边的图标名称(例如 `'IconBolt'`、`'IconSend'`) |
|
||||
| `isPinned` | 否 | 为 `true` 时,会将该命令显示为页面右上角的快速操作按钮 |
|
||||
| `availabilityType` | 否 | 控制命令出现的位置:'GLOBAL'(始终可用)、'RECORD_SELECTION'(仅在选择了记录时),或 'FALLBACK'(当没有其他命令匹配时显示) |
|
||||
| `availabilityObjectUniversalIdentifier` | 否 | 将该命令限制在特定对象类型的页面上(例如仅在 Company 记录上) |
|
||||
| `conditionalAvailabilityExpression` | 否 | 用于动态控制可见性的布尔表达式(见下文) |
|
||||
| 字段 | 必填 | 描述 |
|
||||
| --------------------------------------- | -- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | 是 | 该命令的稳定唯一 ID |
|
||||
| `label` | 是 | 在命令菜单(Cmd+K)中显示的完整标签 |
|
||||
| `frontComponentUniversalIdentifier` | 是 | 此命令打开的前端组件的 `universalIdentifier` |
|
||||
| `shortLabel` | 否 | 固定的快速操作按钮上显示的较短标签 |
|
||||
| `icon` | 否 | **已弃用** — 将被应用程序图标忽略;如果设置了该值,构建时会发出警告 |
|
||||
| `isPinned` | 否 | 为 `true` 时,会将该命令显示为页面右上角的快速操作按钮 |
|
||||
| `availabilityType` | 否 | 控制命令出现的位置:`'GLOBAL'`(始终可用)、`'GLOBAL_OBJECT_CONTEXT'`(仅在具有对象上下文的页面上——索引页和记录页)、`'RECORD_SELECTION'`(仅在选择了记录时),或 `'FALLBACK'`(当没有其他命令匹配时显示) |
|
||||
| `availabilityObjectUniversalIdentifier` | 否 | 将该命令限制在特定对象类型的页面上(例如仅在 Company 记录上) |
|
||||
| `conditionalAvailabilityExpression` | 否 | 用于动态控制可见性的布尔表达式(见下文) |
|
||||
|
||||
## 无头命令
|
||||
|
||||
与[无头前端组件](/l/zh/developers/extend/apps/layout/front-components#headless-vs-non-headless)配对的命令菜单项,是交付一键操作(运行代码、导航,或确认并执行)的惯用方式。 Front Components 页面介绍了处理“执行操作并卸载”模式的 [SDK Command 组件](/l/zh/developers/extend/apps/layout/front-components#sdk-command-components)(`Command`、`CommandLink`、`CommandModal`、`CommandOpenSidePanelPage`)。
|
||||
|
||||
一个典型的流程:
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
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,
|
||||
});
|
||||
```
|
||||
一个典型流程:一个 headless 组件会渲染 `<Command execute={...} />`(参见[完整示例](/l/zh/developers/extend/apps/layout/front-components#sdk-command-components)),并且命令菜单项会指向它:
|
||||
|
||||
```ts src/command-menu-items/run-action.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
@@ -74,7 +45,6 @@ import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -49,14 +49,13 @@ export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd4e5f6a7-b8c9-0123-defa-456789012345',
|
||||
shortLabel: 'Hello',
|
||||
label: 'Hello World',
|
||||
icon: 'IconBolt',
|
||||
isPinned: true,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier: '74c526eb-cb68-4cf7-b05c-0dd8c288d948',
|
||||
});
|
||||
```
|
||||
|
||||
使用 `yarn twenty dev` 同步后(或单次运行 `yarn twenty dev --once`),快速操作会出现在页面右上角:
|
||||
使用 `yarn twenty dev` 同步后(或单次运行 `yarn twenty apply`),快速操作会出现在页面右上角:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="右上角的快速操作按钮" />
|
||||
@@ -88,11 +87,11 @@ export default defineCommandMenuItem({
|
||||
|
||||
```tsx src/front-components/sync-tracker.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useSelectedRecordIds, enqueueSnackbar } from 'twenty-sdk/front-component';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SyncTracker = () => {
|
||||
const recordId = useRecordId();
|
||||
const [recordId] = useSelectedRecordIds();
|
||||
|
||||
useEffect(() => {
|
||||
enqueueSnackbar({ message: `Tracking record ${recordId}`, variant: 'info' });
|
||||
@@ -116,7 +115,7 @@ export default defineFrontComponent({
|
||||
|
||||
`twenty-sdk` 包提供了四个为无头前端组件设计的 Command 辅助组件。 每个组件都会在挂载时执行一个操作,通过显示 snackbar 通知来处理错误,并在完成后自动卸载该前端组件。
|
||||
|
||||
从 `twenty-sdk/command` 导入它们:
|
||||
从 `twenty-sdk/front-component` 导入它们:
|
||||
|
||||
* **`Command`** — 通过 `execute` 属性运行异步回调。
|
||||
* **`CommandLink`** — 导航到某个应用路径。 属性:`to`、`params`、`queryParams`、`options`。
|
||||
@@ -127,8 +126,8 @@ export default defineFrontComponent({
|
||||
|
||||
```tsx src/front-components/run-action.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { Command } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const RunAction = () => {
|
||||
const execute = async () => {
|
||||
@@ -160,7 +159,6 @@ import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
|
||||
label: 'Run my action',
|
||||
icon: 'IconPlayerPlay',
|
||||
frontComponentUniversalIdentifier: 'e5f6a7b8-c9d0-1234-efab-345678901234',
|
||||
});
|
||||
```
|
||||
@@ -169,7 +167,7 @@ export default defineCommandMenuItem({
|
||||
|
||||
```tsx src/front-components/delete-draft.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { CommandModal } from 'twenty-sdk/command';
|
||||
import { CommandModal } from 'twenty-sdk/front-component';
|
||||
|
||||
const DeleteDraft = () => {
|
||||
const execute = async () => {
|
||||
@@ -202,7 +200,7 @@ export default defineFrontComponent({
|
||||
|
||||
使用 `httpRouteTriggerSettings` 声明的逻辑函数,可以通过其路由路径在 HTTP 上进行访问。 Twenty 会将提供你函数服务的基础 URL 作为 `TWENTY_FUNCTIONS_URL` 注入到 worker 中,同时注入用于对调用进行身份验证的 `TWENTY_APP_ACCESS_TOKEN`。 目前还没有用于调用你自定义函数的专用 SDK 客户端,因此请使用普通的 `fetch` 来调用它们:
|
||||
|
||||
> **在 Twenty Cloud 上,HTTP 触发的逻辑函数通过每个工作区的专用域名提供服务**,域名为 `https://\<your-workspace-subdomain>.twenty.com\<path>`——这正是 `TWENTY_FUNCTIONS_URL` 所解析到的地址。 对于外部调用方,请从函数的 **HTTP trigger** 设置或应用的 **Settings** 选项卡中复制准确的 URL。
|
||||
> **在 Twenty Cloud 上,HTTP 触发的逻辑函数通过每个工作区的专用域名提供服务**,域名为 `https://\<your-workspace-subdomain>.withtwenty.com\<path>`——这正是 `TWENTY_FUNCTIONS_URL` 所解析到的地址。 对于外部调用方,请从函数的 **HTTP trigger** 设置或应用的 **Settings** 选项卡中复制准确的 URL。
|
||||
|
||||
<Warning>
|
||||
旧版的 `/s/` 函数路由已被**弃用**,并将于 **2026-07-24 停用**。 请改用上面的 `TWENTY_FUNCTIONS_URL`,并在该日期之前迁移所有硬编码的 `/s/` URL。 `/s/` 路由在自托管场景下仍可用。
|
||||
@@ -212,7 +210,7 @@ export default defineFrontComponent({
|
||||
|
||||
```tsx src/front-components/sync-prs.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { Command } from 'twenty-sdk/command';
|
||||
import { Command } from 'twenty-sdk/front-component';
|
||||
|
||||
const SyncPrs = () => {
|
||||
const execute = async () => {
|
||||
@@ -316,13 +314,13 @@ try {
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
useUserId,
|
||||
useRecordId,
|
||||
useSelectedRecordIds,
|
||||
useFrontComponentId,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
const RecordInfo = () => {
|
||||
const userId = useUserId();
|
||||
const recordId = useRecordId();
|
||||
const [recordId] = useSelectedRecordIds();
|
||||
const componentId = useFrontComponentId();
|
||||
|
||||
return (
|
||||
@@ -405,12 +403,11 @@ export default defineFrontComponent({
|
||||
|
||||
```tsx src/front-components/archive-record.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useRecordId } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { enqueueSnackbar, closeSidePanel, useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const ArchiveRecord = () => {
|
||||
const recordId = useRecordId();
|
||||
const [recordId] = useSelectedRecordIds();
|
||||
|
||||
const handleArchive = async () => {
|
||||
const client = new CoreApiClient();
|
||||
@@ -451,10 +448,10 @@ export default defineFrontComponent({
|
||||
使用 `useSelectedRecordIds()` 来处理多个已选记录。 这对于批量操作很有用:
|
||||
|
||||
```tsx src/front-components/bulk-export.tsx
|
||||
import { defineFrontComponent, numberOfSelectedRecords } from 'twenty-sdk/define';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
import { enqueueSnackbar, closeSidePanel } from 'twenty-sdk/front-component';
|
||||
import { CoreApiClient } from 'twenty-sdk/clients';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const BulkExport = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
@@ -492,12 +489,19 @@ export default defineFrontComponent({
|
||||
name: 'bulk-export',
|
||||
description: 'Export selected records',
|
||||
component: BulkExport,
|
||||
command: {
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
conditionalAvailabilityExpression: numberOfSelectedRecords > 0,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
通过仅限记录选择的[命令菜单项](/l/zh/developers/extend/apps/layout/command-menu-items)将其呈现出来:
|
||||
|
||||
```ts src/command-menu-items/bulk-export.command-menu-item.ts
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678902',
|
||||
label: 'Bulk Export',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
frontComponentUniversalIdentifier: 'd0e1f2a3-b4c5-6789-defa-012345678901',
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export default defineNavigationMenuItem({
|
||||
|
||||
* `position` 控制在侧边栏中的排序。
|
||||
|
||||
* 该枚举还包含 `NavigationMenuItemType.RECORD`,在内部用于用户创建的记录收藏——在应用 manifest 中不可用(没有用于引用记录的字段)。
|
||||
|
||||
* `icon` 和 `color` 是可选的,用于自定义条目的外观。
|
||||
|
||||
* `folderUniversalIdentifier` 也可用于任意条目,将其嵌套到一个 `FOLDER` 类型的父级中。
|
||||
|
||||
@@ -33,17 +33,32 @@ export default defineView({
|
||||
## 关键点
|
||||
|
||||
* `objectUniversalIdentifier` 指定此视图适用于哪个对象。 它可以是你定义的自定义对象,也可以是标准的 Twenty 对象。
|
||||
* `key` 决定视图类型——`ViewKey.INDEX` 是该对象的主列表视图。
|
||||
* `key: ViewKey.INDEX` 将该视图标记为对象的主列表视图(`OBJECT` 导航项打开的那个)。
|
||||
* `fields` 控制显示哪些列以及它们的顺序。 每个字段引用一个 `fieldMetadataUniversalIdentifier`。
|
||||
* 你还可以声明 `filters`、`filterGroups`、`groups` 和 `fieldGroups` 以进行更高级的配置。
|
||||
* 你还可以声明 `filters`、`filterGroups`、`sorts`、`groups` 和 `fieldGroups` 以进行更高级的配置。
|
||||
* 当同一对象存在多个视图时,`position` 控制其排序。
|
||||
|
||||
## 可选属性
|
||||
|
||||
| 属性 | 值 | 描述 |
|
||||
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `type` | `ViewType.TABLE`(默认),`ViewType.KANBAN`,`ViewType.CALENDAR` | 记录的排布方式。 (`FIELDS_WIDGET` / `TABLE_WIDGET` 也存在,但由页面布局小部件在内部使用。) |
|
||||
| `visibility` | `ViewVisibility.WORKSPACE`(默认),`ViewVisibility.UNLISTED` | 视图是对整个工作区可见,还是在选择器中隐藏。 |
|
||||
| `openRecordIn` | `ViewOpenRecordIn.SIDE_PANEL`(默认),`ViewOpenRecordIn.RECORD_PAGE` | 点击记录时在何处打开该记录。 |
|
||||
| `排序` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | 默认排序顺序。 |
|
||||
| `isCompact` | `boolean` | 紧凑的行显示。 |
|
||||
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | 按字段对记录进行分组(例如看板列)。 |
|
||||
| `kanbanAggregateOperation`, `kanbanAggregateOperationFieldMetadataUniversalIdentifier`, `kanbanColumnWidth` | `AggregateOperations.*` | 看板列聚合和列宽设置。 |
|
||||
| `calendarLayout`, `calendarFieldMetadataUniversalIdentifier` | `ViewCalendarLayout.DAY` / `WEEK` / `MONTH` | 日历视图:布局以及用于定位记录的日期字段。 |
|
||||
|
||||
上述所有枚举都从 `twenty-sdk/define` 导出。
|
||||
|
||||
## 过滤器
|
||||
|
||||
视图可以附带预先应用的过滤器。 每个过滤器有三个坐标:被筛选的**字段**、**运算符**(如何比较)以及**值**(与之比较的内容)。 这三者必须全部对齐——在同步时,使用不适用于字段类型的运算符将会被拒绝。
|
||||
|
||||
```ts
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { ViewFilterOperand } from 'twenty-sdk/define';
|
||||
|
||||
filters: [
|
||||
{
|
||||
|
||||
@@ -51,8 +51,12 @@ export default defineLogicFunction({
|
||||
```
|
||||
|
||||
可用的触发器类型:
|
||||
* **httpRoute**:在 **`/s/` 端点**下通过 HTTP 路径和方法公开你的函数:
|
||||
> 例如 `path: '/post-card/create'` 可在 `https://your-twenty-server.com/s/post-card/create` 调用
|
||||
* **httpRoute**:在你工作区的 HTTP 路径和方法上显示你的函数。**函数基础的 URL** ——`TWENTY_FUNCTIONS_URL` (在20个云上) a 专用工作区:
|
||||
> 例如 `path: '/post-card/create'` 可在 `https://your-workspace.withtwenty.com/post-card/create` 调用
|
||||
|
||||
<Warning>
|
||||
旧的 `/s/` 前缀路由 (`https://your-twentserver.com/s/post-card/create`) **在 20 Cloud** 上被废弃,并将在 **2026-07-24**被停用。 它仍可用于不配置一个孤立函数域的自托管和本地实例——在设置时使用 `TWENTY_FUNCTIONS_URL` 。 然后回到\<server-url>/s/\<path>。
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
要从(无头)前端组件调用由路由触发的逻辑函数,请参见[调用逻辑函数](/l/zh/developers/extend/apps/layout/front-components#calling-a-logic-function)。
|
||||
|
||||
@@ -40,13 +40,13 @@ Twenty 应用的 **逻辑层** 是实际*运行*的代码——用于响应 HTTP
|
||||
|
||||
逻辑函数选择一个或多个触发器——下面的每一项都是 `defineLogicFunction()` 上的一个独立字段:
|
||||
|
||||
| 触发器 | 触发时机 | 设置 |
|
||||
| ----------- | --------------------------------------- | ------------------------------- |
|
||||
| **HTTP 路由** | 请求命中你的 `/s/\<path>` 端点 | `httpRouteTriggerSettings` |
|
||||
| **Cron** | 匹配到一个 CRON 表达式时 | `cronTriggerSettings` |
|
||||
| **数据库事件** | 当工作区记录被创建、更新或删除时 | `databaseEventTriggerSettings` |
|
||||
| **AI 工具** | 某个 Twenty AI 功能决定调用你的函数时 | `toolTriggerSettings` |
|
||||
| **工作流动作** | 当工作流步骤调用你的函数时 | `workflowActionTriggerSettings` |
|
||||
| 触发器 | 触发时机 | 设置 |
|
||||
| ----------- | ------------------------ | ------------------------------- |
|
||||
| **HTTP 路由** | 一个请求点击你的公開的 URL | `httpRouteTriggerSettings` |
|
||||
| **Cron** | 匹配到一个 CRON 表达式时 | `cronTriggerSettings` |
|
||||
| **数据库事件** | 当工作区记录被创建、更新或删除时 | `databaseEventTriggerSettings` |
|
||||
| **AI 工具** | 某个 Twenty AI 功能决定调用你的函数时 | `toolTriggerSettings` |
|
||||
| **工作流动作** | 当工作流步骤调用你的函数时 | `workflowActionTriggerSettings` |
|
||||
|
||||
函数在隔离的 Node.js 进程沙箱中运行,并通过限定在 [`defineApplication()`](/l/zh/developers/extend/apps/config/application) 上声明角色范围内的类型化 API 客户端访问工作区。
|
||||
|
||||
|
||||
@@ -4,7 +4,25 @@ description: yarn twenty 命令可用于执行函数、流式传输日志、管
|
||||
icon: terminal
|
||||
---
|
||||
|
||||
除了 `dev`、`dev:build`、`dev:add` 和 `dev:typecheck` 外,`yarn twenty` CLI 还提供了用于执行函数、查看日志和管理应用安装的命令。
|
||||
`yarn twenty` CLI 是与你的所有应用相关内容进行交互的接口。 完整命令列表:
|
||||
|
||||
| 命令 | 作用 | 记录于 |
|
||||
| ----------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- |
|
||||
| `dev` | 监视源文件并实时同步更改 | [快速开始](/l/zh/developers/extend/apps/getting-started/quick-start) |
|
||||
| `计划` | 在不应用变更的情况下预览元数据更改 | [同步与恢复](/l/zh/developers/extend/apps/operations/sync-and-recovery#previewing-changes-plan) |
|
||||
| `应用` | 在显示计划后应用元数据更改 | [同步与恢复](/l/zh/developers/extend/apps/operations/sync-and-recovery) |
|
||||
| `dev:build` | 编译应用并生成 API 客户端(使用 `--tarball` 打包为 `.tgz`) | [发布](/l/zh/developers/extend/apps/operations/publishing) |
|
||||
| `dev:typecheck` | 运行 TypeScript 类型检查 | [测试](/l/zh/developers/extend/apps/operations/testing) |
|
||||
| `dev:add` | 搭建一个新的实体脚手架 | [脚手架](/l/zh/developers/extend/apps/getting-started/scaffolding) |
|
||||
| `dev:generate-client` | 重新生成类型化 API 客户端 | 本页面 |
|
||||
| `dev:function:exec` / `dev:function:logs` | 执行函数并流式输出其日志 | 本页面 |
|
||||
| `dev:translations-extract` | 将可翻译字符串提取到 `locales/` 目录中的目录文件中 | [翻译](/l/zh/developers/extend/apps/translations/overview) |
|
||||
| `dev:catalog-sync` | 触发一次市场目录同步 | [发布](/l/zh/developers/extend/apps/operations/publishing#how-marketplace-discovery-works) |
|
||||
| `app:publish` / `app:install` / `app:uninstall` | 发布生命周期 | [发布](/l/zh/developers/extend/apps/operations/publishing) 和本页面 |
|
||||
| `docker:*` | 管理本地 Twenty 服务器容器 | [本地服务器](/l/zh/developers/extend/apps/getting-started/local-server) |
|
||||
| `remote:*` | 管理服务器连接 | 本页面 |
|
||||
|
||||
每个命令都接受 `-r, --remote \<name>` 参数,以便针对特定远程目标而不是默认远程。
|
||||
|
||||
## 执行函数(`yarn twenty dev:function:exec`)
|
||||
|
||||
@@ -20,8 +38,9 @@ yarn twenty dev:function:exec -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
# Pass a JSON payload
|
||||
yarn twenty dev:function:exec -n create-new-post-card -p '{"name": "Hello"}'
|
||||
|
||||
# Execute the post-install function
|
||||
# Execute the install hooks
|
||||
yarn twenty dev:function:exec --postInstall
|
||||
yarn twenty dev:function:exec --preInstall
|
||||
```
|
||||
|
||||
## 查看函数日志(`yarn twenty dev:function:logs`)
|
||||
@@ -100,6 +119,12 @@ yarn twenty remote:list
|
||||
|
||||
# Set the active remote
|
||||
yarn twenty remote:use <name>
|
||||
|
||||
# Check that the active remote's authentication is still valid
|
||||
yarn twenty remote:status
|
||||
|
||||
# Remove a remote
|
||||
yarn twenty remote:remove <name>
|
||||
```
|
||||
|
||||
你的凭据存储在 `~/.twenty/config.json` 中。
|
||||
|
||||
@@ -229,7 +229,7 @@ yarn twenty dev:catalog-sync
|
||||
# yarn twenty dev:catalog-sync --remote production
|
||||
```
|
||||
|
||||
市场中显示的元数据来自你的 `defineApplication()` 配置——例如 `displayName`、`description`、`author`、`category`、`logoUrl`、`screenshots`、`aboutDescription`、`websiteUrl` 和 `termsUrl` 等字段。
|
||||
市场中显示的元数据来自你的 `defineApplication()` 配置——参见上面的 [Marketplace 元数据](#marketplace-metadata)。
|
||||
|
||||
<Note>
|
||||
如果您的应用未在 `defineApplication()` 中定义 `aboutDescription`,市场将自动使用 npm 上您的软件包的 `README.md` 作为关于页面内容。 这意味着您可以为 npm 和 Twenty 市场维护同一个 README。 如果您希望在市场中使用不同的描述,请显式设置 `aboutDescription`。
|
||||
|
||||
@@ -15,33 +15,44 @@ icon: compass
|
||||
| 如果你想要…… | 命令 | 备注 |
|
||||
| ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| 使用实时同步进行本地迭代 | `yarn twenty dev` | 监视你的文件,并在每次更改时执行同步。 |
|
||||
| 同步一次后退出(CI、脚本、钩子) | `yarn twenty dev --once` | 执行一次构建和同步,然后退出。 |
|
||||
| 在**不应用变更**的前提下预览更改 | `yarn twenty dev --once --dry-run` | 计算并打印 diff;不写入任何内容。 |
|
||||
| 同步一次后退出(CI、脚本、钩子) | `yarn twenty apply` | 执行一次构建和同步,然后退出。 添加 `--force` 以跳过破坏性变更确认。 |
|
||||
| 在**不应用变更**的前提下预览更改 | `yarn twenty plan` | 计算并打印 diff;不写入任何内容。 |
|
||||
| 从工作区中移除该应用 | `yarn twenty app:uninstall` | 添加 `--yes` 以跳过提示。 |
|
||||
| 将 tar 包发送到服务器 | `yarn twenty app:publish --private` | 需要一个**严格更高的** `package.json` 版本——参见 [Publishing](/l/zh/developers/extend/apps/operations/publishing)。 |
|
||||
| 发布到应用市场(npm) | `yarn twenty app:publish` | — |
|
||||
| 安装 / 升级已部署的版本 | `yarn twenty app:install` | 安装当前已部署的版本。 |
|
||||
| 清空本地服务器并重新开始 | `yarn twenty docker:reset` | 删除**所有**本地数据——最后的手段。 |
|
||||
|
||||
<Note>
|
||||
`yarn twenty dev --once` 和 `yarn twenty dev --once --dry-run` 仍然可以作为已弃用的别名使用,对应 `yarn twenty apply` 和 `yarn twenty plan`。
|
||||
</Note>
|
||||
|
||||
### 本地同步不需要提升版本号
|
||||
|
||||
严格递增的 `version` 规则(在 deploy 时为 `VERSION_ALREADY_EXISTS`,在 install 时为 `APP_ALREADY_INSTALLED` / `CANNOT_DOWNGRADE_APPLICATION`)适用于 **`app:publish` / `app:install`**——即发布路径。 `yarn twenty dev` 就地同步你的 manifest,且从不要求修改版本,因此你无需为了迭代去改动 `package.json`。 如果你发现自己为了测试本地改动而不断提升版本号,说明你在想要使用开发循环时却走了发布路径。
|
||||
|
||||
## 阅读同步输出
|
||||
|
||||
每次同步都会打印它实际应用的(或在使用 `--dry-run` 时将要应用的)元数据更改:
|
||||
每次同步都会打印它实际应用的(或在使用 `plan` 时将要应用的)元数据更改,Terraform 风格——每个实体一个区块,包含其属性,然后是一行总结:
|
||||
|
||||
```text filename="Terminal"
|
||||
Metadata changes: 2 created, 1 updated, 1 deleted
|
||||
created objectMetadata rocket
|
||||
created fieldMetadata timelineActivities
|
||||
updated fieldMetadata launchedAt
|
||||
deleted pageLayout legacyTab
|
||||
✓ Synced
|
||||
# objectMetadata "rocket" will be created
|
||||
+ icon = "IconRocket"
|
||||
+ labelSingular = "Rocket"
|
||||
+ ...
|
||||
|
||||
# fieldMetadata "launchedAt" will be updated
|
||||
~ isNullable = false -> true
|
||||
|
||||
Plan: 2 to add, 1 to change, 1 to destroy.
|
||||
|
||||
✓ Synced My App (4 files)
|
||||
```
|
||||
|
||||
这是你的首要诊断工具:它会准确告诉你哪些对象、字段和布局发生了变化,这样你就可以在查看 UI 之前确认同步是否按预期进行。
|
||||
|
||||
破坏性更改(`to destroy`)会列出它们会删除的内容(例如 `objectMetadata "auditNote" — drops the table and all its rows`),并且需要交互式确认,或者在脚本中使用 `--force`。
|
||||
|
||||
当同步在某个实体上失败时,错误信息会给出有问题的实体及其 `universalIdentifier`,例如:
|
||||
|
||||
```text
|
||||
@@ -50,39 +61,42 @@ Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337)
|
||||
|
||||
使用该标识符在 manifest 中(以及在需要时在工作区中)定位该实体,而不是猜测哪个实体发生了冲突。
|
||||
|
||||
## 预览更改(干跑 / dry run)
|
||||
## 预览更改(计划)
|
||||
|
||||
`yarn twenty dev --once --dry-run` 会构建你的 manifest,向服务器请求迁移计划并打印出来——**不会实际应用任何内容**。 这是在真正执行前,以安全方式回答“这次同步会改动什么?”的办法。
|
||||
`yarn twenty plan` 会构建你的 manifest,向服务器请求迁移计划并打印出来——**不会实际应用任何内容**。 这是在真正执行前,以安全方式回答“这次同步会改动什么?”的办法。
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --once --dry-run
|
||||
yarn twenty plan
|
||||
```
|
||||
|
||||
```text filename="Terminal"
|
||||
Building manifest...
|
||||
Computing metadata diff (dry run, nothing will be applied)...
|
||||
Metadata changes: 1 created, 1 updated
|
||||
created fieldMetadata timelineActivities
|
||||
updated objectMetadata rocket
|
||||
✓ Dry run complete for My App — no changes were applied
|
||||
Computing metadata plan (read-only, nothing will be applied)...
|
||||
|
||||
# fieldMetadata "timelineActivities" will be created
|
||||
+ ...
|
||||
|
||||
Plan: 1 to add, 1 to change, 0 to destroy.
|
||||
|
||||
✓ Plan complete for My App — no changes were applied
|
||||
```
|
||||
|
||||
一次 dry run:
|
||||
一个计划:
|
||||
|
||||
* **不会写入任何内容**——不会进行元数据迁移、不会更新应用记录、不会更改默认角色/标签,也不会生成 API 客户端。
|
||||
* 返回与真实同步将要应用的**相同 diff**,这样你可以预先审查将被创建 / 更新 / 删除的实体。
|
||||
* 在执行高风险改动之前、在审查 AI 生成的改动时,或在某些如果即将落地意外变更就应当失败的脚本中,dry run 都非常有用。
|
||||
|
||||
<Note>
|
||||
dry run 只会预览**元数据**更改,并且要求应用至少已经同步过一次(这样工作区才知道它的存在)。 如果你在一个从未同步过的应用上运行它,服务器会报告该应用尚未安装——先运行一次 `yarn twenty dev`。
|
||||
计划只会预览**元数据**更改,并且要求应用至少已经同步过一次(这样工作区才知道它的存在)。 如果你在一个从未同步过的应用上运行它,服务器会报告该应用尚未安装——先运行一次 `yarn twenty dev`。
|
||||
</Note>
|
||||
|
||||
## 恢复梯子
|
||||
|
||||
当本地元数据看起来不对时,按以下顺序逐步升级,并在问题解决后立即停止。 每一步都比前一步更具破坏性。
|
||||
|
||||
1. **重新同步。** 再次运行 `yarn twenty dev --once`。 同步是幂等的——在干净的 manifest 上重新运行是安全的,并且通常可以解决瞬时故障。
|
||||
2. **预览计划。** 运行 `yarn twenty dev --once --dry-run`,在不应用变更的前提下,准确查看下一次同步打算修改什么。
|
||||
1. **重新同步。** 再次运行 `yarn twenty apply`。 同步是幂等的——在干净的 manifest 上重新运行是安全的,并且通常可以解决瞬时故障。
|
||||
2. **预览计划。** 运行 `yarn twenty plan`,在不应用变更的前提下,准确查看下一次同步打算修改什么。
|
||||
3. **阅读具名错误。** 如果同步失败,记录消息中的元数据类型和 `universalIdentifier`(见上),并在 manifest 中定位该实体。 冲突通常指向重复或被重复使用的标识符。
|
||||
4. **卸载并重新安装。** 先执行 `yarn twenty app:uninstall`,然后再次同步(`yarn twenty dev`)。 这会在保留你工作区其余部分不变的情况下,从零重建该应用的元数据。
|
||||
5. **完全重置(最后手段)。** 执行 `yarn twenty docker:reset`,然后重新播种并重新同步。
|
||||
|
||||
@@ -78,6 +78,13 @@ yarn add -D vitest vite-tsconfig-paths
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '<the pre-seeded local dev key>';
|
||||
|
||||
// Make env vars available to globalSetup (test.env only applies to workers)
|
||||
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
||||
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
@@ -88,66 +95,74 @@ export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
setupFiles: ['src/__tests__/setup-test.ts'],
|
||||
globalSetup: ['src/__tests__/global-setup.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL: 'http://localhost:2020',
|
||||
TWENTY_API_KEY: 'your-api-key',
|
||||
TWENTY_API_URL,
|
||||
TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
创建一个设置文件,在测试运行前验证服务器可达:
|
||||
创建一个全局设置文件,用于验证服务器是否可访问,写入 SDK 的测试配置文件(`~/.twenty/config.test.json`),并在测试运行前同步应用:
|
||||
|
||||
```ts src/__tests__/setup-test.ts
|
||||
```ts src/__tests__/global-setup.ts
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TEST_CONFIG_DIR = path.join(os.tmpdir(), '.twenty-sdk-test');
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
export async function setup() {
|
||||
const apiUrl = process.env.TWENTY_API_URL!;
|
||||
const apiKey = process.env.TWENTY_API_KEY!;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Verify the server is running
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
const response = await fetch(`${apiUrl}/healthz`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Start the server before running integration tests.',
|
||||
);
|
||||
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
|
||||
}
|
||||
|
||||
// Write a temporary config for the SDK
|
||||
fs.mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
||||
|
||||
// Write the SDK's test config (the CLI reads config.test.json when NODE_ENV=test)
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(TEST_CONFIG_DIR, 'config.json'),
|
||||
path.join(CONFIG_DIR, 'config.test.json'),
|
||||
JSON.stringify({
|
||||
remotes: {
|
||||
local: {
|
||||
apiUrl: process.env.TWENTY_API_URL,
|
||||
apiKey: process.env.TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
remotes: { local: { apiUrl, apiKey } },
|
||||
defaultRemote: 'local',
|
||||
}, null, 2),
|
||||
);
|
||||
});
|
||||
|
||||
// Start from a clean slate, then sync the app
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({ appPath: APP_PATH });
|
||||
if (!result.success) {
|
||||
throw new Error(`Dev sync failed: ${result.error?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
await appUninstall({ appPath: APP_PATH });
|
||||
}
|
||||
```
|
||||
|
||||
## 可编程的 SDK API
|
||||
|
||||
子路径 `twenty-sdk/cli` 导出了可直接在测试代码中调用的函数:
|
||||
|
||||
| 函数 | 描述 |
|
||||
| -------------- | ------------------ |
|
||||
| `appBuild` | 构建应用,并可选地打包为 tar 包 |
|
||||
| `appDeploy` | 将 tar 包上传到服务器 |
|
||||
| `appInstall` | 在活动工作区安装该应用 |
|
||||
| `appUninstall` | 从活动工作区卸载该应用 |
|
||||
| 函数 | 描述 |
|
||||
| -------------- | ----------------------------------- |
|
||||
| `appBuild` | 构建应用,并可选地打包为 tar 包 |
|
||||
| `appDeploy` | 将 tar 包上传到服务器 |
|
||||
| `appDevOnce` | 构建并同步应用一次(与 `yarn twenty apply` 相同) |
|
||||
| `appInstall` | 在活动工作区安装该应用 |
|
||||
| `appUninstall` | 从活动工作区卸载该应用 |
|
||||
|
||||
每个函数都会返回一个结果对象,包含 `success: boolean`,以及 `data` 或 `error` 之一。
|
||||
|
||||
@@ -238,64 +253,10 @@ yarn test:watch
|
||||
yarn twenty dev:typecheck
|
||||
```
|
||||
|
||||
这会运行 `tsc --noEmit` 并报告所有类型错误。
|
||||
这会针对你的应用的 `tsconfig.json` 运行 `tsc --noEmit`,并报告所有类型错误。 脚手架生成的应用还会提供一个 `yarn typecheck` 脚本,它也会覆盖测试文件(`tsconfig.spec.json`)。
|
||||
|
||||
## 使用 GitHub Actions 进行 CI
|
||||
|
||||
脚手架工具会在 `.github/workflows/ci.yml` 生成一个开箱即用的 GitHub Actions 工作流。 它会在每次向 `main` 推送以及拉取请求上自动运行你的集成测试。
|
||||
脚手架工具会在 `.github/workflows/ci.yml` 生成一个开箱即用的工作流。 在每次向 `main` 推送代码以及每个拉取请求上,它都会在 runner 中启动一个临时的 Twenty 服务器(通过 `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test` action),然后运行 `yarn lint`、`yarn typecheck`、`yarn test:unit` 和 `yarn test`,并将 `TWENTY_API_URL` / `TWENTY_API_KEY` 指向该服务器。 无需任何机密信息,你可以在工作流顶部通过 `TWENTY_VERSION` 环境变量固定服务器版本。
|
||||
|
||||
工作流:
|
||||
|
||||
1. 检出你的代码
|
||||
2. 使用 `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` 动作启动一个临时的 Twenty 服务器
|
||||
3. 使用 `yarn install --immutable` 安装依赖
|
||||
4. 运行 `yarn test`,并从该动作的输出中注入 `TWENTY_API_URL` 和 `TWENTY_API_KEY`
|
||||
|
||||
```yaml .github/workflows/ci.yml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-docker-image@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
你无需配置任何机密——`spawn-twenty-docker-image` 动作会在运行器中直接启动一个临时的 Twenty 服务器,并输出连接详情。 GitHub 会自动提供 `GITHUB_TOKEN` 机密。
|
||||
|
||||
若要固定为特定的 Twenty 版本而不是 `latest`,请在工作流顶部修改 `TWENTY_VERSION` 环境变量。
|
||||
完整的脚手架工作流(`ci.yml` 和 `cd.yml` 部署流水线)的演练说明,请参见 [发布 → 自动化 CI/CD](/l/zh/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows)。
|
||||
|
||||
+7
-3
@@ -87,9 +87,11 @@ const GenerateDocumentForm = () => {
|
||||
}, []);
|
||||
|
||||
const generate = async () => {
|
||||
const apiBaseUrl = process.env.TWENTY_API_URL;
|
||||
// Prefer the injected functions URL; fall back to the legacy /s prefix (self-hosted/local)
|
||||
const functionsBaseUrl =
|
||||
process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL}/s`;
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
||||
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
|
||||
const res = await fetch(`${functionsBaseUrl}/documents/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ templateId, recordId }),
|
||||
@@ -176,7 +178,9 @@ const DocumentViewer = () => {
|
||||
const recordId = useFrontComponentExecutionContext((c) => c.recordId ?? null);
|
||||
// ...load { content, file } for recordId, then derive the links:
|
||||
const pdfUrl = document.file?.[0]?.url;
|
||||
const webUrl = `${process.env.TWENTY_API_URL ?? ''}/s/documents/view?id=${recordId}`;
|
||||
const functionsBaseUrl =
|
||||
process.env.TWENTY_FUNCTIONS_URL || `${process.env.TWENTY_API_URL ?? ''}/s`;
|
||||
const webUrl = `${functionsBaseUrl}/documents/view?id=${recordId}`;
|
||||
|
||||
// Render the template body, plus quick links to the web page and the PDF.
|
||||
// Links open in a new tab so they don't navigate the embedded component.
|
||||
|
||||
+6
-2
@@ -9,8 +9,12 @@ description: 通过 HTTP 触发函数并将文档渲染为 web 页面。
|
||||
* a **POST** 让UI 调用来生成文档的端点,和
|
||||
* 一个公开的 **GET** 端点,将文档作为可打印的网页。
|
||||
|
||||
两者都使用 `httpRouteTriggerSettings` 。 App rough are served under `/s` under your
|
||||
20 server (e.g. `http://localhost:2020/s/documents/generate`).
|
||||
两者都使用 `httpRouteTriggerSettings` 。 在本地开发服务器上,应用路由在带有 `/s` 前缀的路径下提供服务(例如 `http://localhost:2020/s/documents/generate`)。
|
||||
|
||||
<Note>
|
||||
在 Twenty Cloud 上,路由通过工作区专用的函数域名提供服务——即 Twenty 注入的、作为 `TWENTY_FUNCTIONS_URL` 的 URL,且没有 `/s` 前缀。 在那里,`/s` 前缀已被弃用,仅保留用于自托管和本地实例。
|
||||
参见[调用逻辑函数](/l/zh/developers/extend/apps/layout/front-components#calling-a-logic-function)。
|
||||
</Note>
|
||||
|
||||
## POST 路由 — 按需生成
|
||||
|
||||
|
||||
+3
-3
@@ -76,11 +76,11 @@ export default defineApplication({
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
yarn twenty plan # preview the metadata diff
|
||||
```
|
||||
|
||||
干线运行正是在不应用它的情况下打印服务器上会改变的内容——
|
||||
是一个很好的最后智能检查。 见
|
||||
该计划会精确列出在服务器上将发生的更改,而不会实际应用这些更改——
|
||||
这是一次很好的最终健全性检查。 见
|
||||
[Testing](/l/zh/developers/extend/apps/operations/testing) 和
|
||||
[同步和恢复](/l/zh/developers/extend/apps/operations/sync-and-recovery)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user