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,131 @@
---
title: CLI
description: yarn twenty 命令可用于执行函数、流式传输日志、管理应用安装以及切换远程。
icon: terminal
---
`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/operations/translations) |
| `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`)
手动运行逻辑函数,而无需通过 HTTP、定时任务或数据库事件来触发:
```bash filename="Terminal"
# Execute by function name
yarn twenty dev:function:exec -n create-new-post-card
# Execute by universalIdentifier
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 install and uninstall hooks
yarn twenty dev:function:exec --postInstall
yarn twenty dev:function:exec --preInstall
yarn twenty dev:function:exec --uninstall
```
## 查看函数日志(`yarn twenty dev:function:logs`)
实时流式查看你的应用逻辑函数的执行日志:
```bash filename="Terminal"
# Stream all function logs
yarn twenty dev:function:logs
# Filter by function name
yarn twenty dev:function:logs -n create-new-post-card
# Filter by universalIdentifier
yarn twenty dev:function:logs -u e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
```
<Note>
这与 `yarn twenty docker:logs` 不同,后者显示的是 Docker 容器日志。 `yarn twenty dev:function:logs` 会显示来自 Twenty 服务器的应用函数执行日志。
</Note>
## 生成类型化客户端(`yarn twenty dev:generate-client`)
根据当前远程的 schema 重新生成类型化 API 客户端(`twenty-client-sdk`),无需构建或同步应用。 在任何项目中使用它来获取一个类型化客户端——例如位于单独代码库中的后端服务——用于与您的 Twenty 实例通信:
```bash filename="Terminal"
# In your project (no Twenty app definition required)
yarn add twenty-sdk twenty-client-sdk
# Connect to the Twenty instance to generate the client from
yarn twenty remote:add
# Generate the typed client into node_modules/twenty-client-sdk
yarn twenty dev:generate-client
```
然后在代码中导入该客户端:
```typescript
import { CoreApiClient } from 'twenty-client-sdk/core';
```
每当数据模型发生更改时,请重新运行该命令,以刷新生成的类型。
<Note>
客户端是在 `node_modules` 中生成的,因此不会随代码一起提交。 每次安装后运行 `yarn twenty dev:generate-client`(例如在 `postinstall` 脚本或 CI 中)。
</Note>
## 卸载应用(`yarn twenty app:uninstall`)
将你的应用从活动工作区中移除:
```bash filename="Terminal"
yarn twenty app:uninstall
# Skip the confirmation prompt
yarn twenty app:uninstall --yes
```
## 管理远程
“远程”是指你的应用连接到的 Twenty 服务器。 在设置期间,脚手架工具会为你自动创建一个。 你可以随时添加更多远程或在它们之间切换。
```bash filename="Terminal"
# Add a new remote (opens a browser for OAuth login)
yarn twenty remote:add
# Connect to a local Twenty server (auto-detects port 2020 or 3000)
yarn twenty remote:add --local
# Add a remote non-interactively (useful for CI)
yarn twenty remote:add --url https://your-twenty-server.com --api-key $TWENTY_API_KEY --as my-remote
# List all configured remotes
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` 中。

View File

@@ -0,0 +1,32 @@
---
title: 概览
description: 构建、测试并发布你的应用——CLI 命令、集成测试、CI,以及发布到服务器或 npm。
icon: rocket
---
**运维层**指的是你对应用*进行*而不是*使用*它所做的一切:调用 CLI 命令、针对真实的 Twenty 服务器运行集成测试、配置 CI,以及发布发行版——可以是部署到单个服务器的 tarball,也可以是发布到应用市场中列出的 npm 包。
```text
develop ─▶ test ─▶ build ─▶ deploy / publish
─────── ──── ───── ─────────────────
yarn yarn yarn yarn twenty app:publish --private (tarball → one server)
twenty test twenty
dev dev:build yarn twenty app:publish (npm → marketplace)
```
## 本节内容
<CardGroup cols={2}>
<Card title="CLI" icon="terminal" href="/l/zh/developers/extend/apps/operations/cli">
`yarn twenty` 参考——exec、logs、uninstall、remotes。
</Card>
<Card title="同步与恢复" icon="compass" href="/l/zh/developers/extend/apps/operations/sync-and-recovery">
在阅读同步差异时应使用哪些命令,以及分步恢复流程。
</Card>
<Card title="测试" icon="flask" href="/l/zh/developers/extend/apps/operations/testing">
Vitest 配置、集成测试、类型检查、CI 工作流。
</Card>
<Card title="发布" icon="upload" href="/l/zh/developers/extend/apps/operations/publishing">
构建、部署 tarball、发布到 npm、安装。
</Card>
</CardGroup>

View File

@@ -0,0 +1,293 @@
---
title: 发布
icon: upload
description: 将你的 Twenty 应用分发到应用市场,或进行内部部署。
---
## 概览
一旦你的应用已[在本地构建并完成测试](/l/zh/developers/extend/apps/getting-started/concepts),你可以通过两种方式进行分发:
* **部署 tar 包** — 直接将你的应用上传到特定的 Twenty 服务器,以供内部或私有使用。
* **发布到 npm** — 将你的应用在 Twenty 应用市场上架,供任何工作区发现并安装。
两种路径都从同一个**构建**步骤开始。
## 构建你的应用
运行构建命令来编译你的应用,并生成适用于分发的 `manifest.json`:
```bash filename="Terminal"
yarn twenty dev:build
```
这会编译 TypeScript 源码,转译逻辑函数和前端组件,并将所有内容写入 `.twenty/output/`。 添加 `--tarball`,还可以生成用于手动分发或 publish 命令的 `.tgz` 包。
## 部署到服务器(tar 包)
对于你不希望公开的应用(专有工具、仅供企业使用的集成或实验性构建),你可以将 tar 包直接部署到某台 Twenty 服务器。
### 先决条件
在部署之前,你需要配置一个指向目标服务器的远程。 远程会将服务器 URL 和身份验证凭据本地存储在 `~/.twenty/config.json` 中。
添加远程:
```bash filename="Terminal"
yarn twenty remote:add --url https://your-twenty-server.com --as production
```
### 部署
一步构建并将你的应用上传到服务器:
```bash filename="Terminal"
yarn twenty app:publish --private
# To deploy to a specific remote:
# yarn twenty app:publish --private --remote production
```
### 共享已部署的应用
通过 tar 包分发的应用不会出现在公共市场中,因此同一服务器上的其他工作区无法通过浏览发现它们。 要共享已部署的应用:
1. 前往 **Settings > Applications > Registrations** 并打开你的应用
2. 在 **Distribution** 选项卡中,点击 **Copy share link**
3. 将此链接分享给其他工作区的用户 — 它会将他们直接带到该应用的安装页面
该分享链接使用服务器的基础 URL(不包含任何工作区子域),因此适用于该服务器上的任意工作区。
### 版本管理
在更新已部署的 tarball 应用时,服务器要求 `package.json` 中的 `version` 必须**严格高于**(按[语义化版本](https://semver.org)排序)当前已部署的版本。 在 tar 包存储之前,重新部署相同版本或推送更低版本都会被拒绝 — 你会在 CLI 中看到 `VERSION_ALREADY_EXISTS` 错误。
要发布更新:
1. 将 `package.json` 中的 `version` 字段递增(例如:`1.2.3` → `1.2.4`、`1.3.0` 或 `2.0.0`)
2. 运行 `yarn twenty app:publish --private`(或 `yarn twenty app:publish --private --remote production`)
3. 已安装此应用并在应用的 Settings 选项卡中启用自动升级的工作区会在后台自动升级;其他工作区会在其设置中看到可用的升级
<Note>
预发布标签按预期工作:将 `1.0.0-rc.1` 递增为 `1.0.0-rc.2` 是允许的,并且像 `1.0.0` 这样的正式发布会被正确识别为高于 `1.0.0-rc.5`。 `package.json` 中的版本本身必须是有效的 SemVer 字符串。
</Note>
{/* TODO: add screenshot of the Upgrade button */}
### 服务器版本兼容性
如果你的应用使用了特定 Twenty 服务器版本中引入的功能(例如在 v2.3.0 中新增的 OAuth 提供方),应当在 `package.json` 的 `engines.twenty` 字段中声明应用所需的最低服务器版本:
```json filename="package.json"
{
"name": "twenty-my-app",
"version": "1.0.0",
"engines": {
"node": "^24.5.0",
"twenty": ">=2.3.0"
}
}
```
该值是标准的 [semver 范围](https://github.com/npm/node-semver#ranges)。 常见模式:
| 范围 | 含义 |
| ---------------------------------- | --------------------------------------- |
| `>=2.3.0` | 任何 2.3.0 及以上的服务器 |
| `>=2.3.0 \<3.0.0` | 2.3.0 或更高,但低于下一个主版本 |
| `^2.3.0` | 与 `>=2.3.0 \<3.0.0` 相同 |
**在部署和安装时会发生什么:**
* 如果已设置 `engines.twenty`,且目标服务器的版本不满足该范围,则部署(tarball 上传)或安装将被拒绝,并返回 `SERVER_VERSION_INCOMPATIBLE` 错误以及一条同时指明所需范围和实际服务器版本的消息。
* 如果 `engines.twenty` **未设置**,则该应用可在任何服务器版本上被接受(与现有应用向后兼容)。
* 如果服务器未配置 `APP_VERSION`,则跳过该检查。
<Note>
服务器是权威校验方——它会在 tarball 上传和工作区安装时验证 `engines.twenty`。 即使你通过带外方式部署 tarball 或从应用市场安装,服务器仍会强制执行兼容性要求。
</Note>
## 自动化 CI/CD(脚手架生成的工作流)
使用 `create-twenty-app` 生成的应用开箱即带有三个 GitHub Actions 工作流,位于 `.github/workflows/`。 CI 可在无需任何设置的情况下运行,CD 只需要配置一个 secret,而发布到 npm 只需进行一次性的 npm trusted-publisher 设置。
### CI — `ci.yml`
它会在每次向 `main` 推送以及拉取请求上自动运行你的集成测试。
**作用:**
1. 检出你的应用源代码。
2. 使用 `twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main` 组合 action 启动一个隔离的 Twenty 测试实例(相当于 CI 中的 `yarn twenty docker:start --test`)。
3. 启用 Corepack,从你的 `.nvmrc` 设置 Node.js,并使用 `yarn install --immutable` 安装依赖。
4. 运行 `yarn test`,并从启动的实例传入 `TWENTY_API_URL` 和 `TWENTY_API_KEY`,以便你的测试可以与真实服务器通信。
**配置选项:**
* `TWENTY_VERSION`(环境变量,默认 `latest`)— 通过在 `ci.yml` 中编辑它来固定 CI 使用的 Twenty 服务器版本。
* 并发按 `github.ref` 分组,并会在有新的推送时取消进行中的运行。
不需要任何机密——测试实例是临时的,只在作业持续期间存在。
### CD — `cd.yml`
在每次向 `main` 推送时将你的应用部署到已配置的 Twenty 服务器;当为拉取请求添加 `deploy` 标签时,也可从该拉取请求进行部署。
**作用:**
1. 检出 PR 的 head(针对已加标签的 PR),或被推送的提交。
2. 运行 `twentyhq/twenty/.github/actions/deploy-twenty-app@main`——相当于 CI 中的 `yarn twenty app:publish --private`。
3. 运行 `twentyhq/twenty/.github/actions/install-twenty-app@main`,将新部署的版本安装到目标工作区。
**必需的配置:**
| 设置 | 位置 | 目的 |
| ----------------------- | -------------------------------------------------------- | ---------------------------------------- |
| `TWENTY_DEPLOY_URL` | `cd.yml` 中的 `env`(默认值为 `http://localhost:3000`) | 要部署到的 Twenty 服务器。 首次使用前将其更改为你真实的服务器 URL。 |
| `TWENTY_DEPLOY_API_KEY` | GitHub 仓库 **Settings → Secrets and variables → Actions** | 在目标服务器上具有部署权限的 API 密钥。 |
<Note>
默认的 `TWENTY_DEPLOY_URL` 值 `http://localhost:3000` 只是占位符——从 GitHub 托管的 runner 无法访问任何资源。 在启用 CD 之前,将其更新为你服务器的公网 URL(或使用具有网络访问权限的自托管 runner)。
</Note>
**从 PR 触发预览部署:**
为拉取请求添加 `deploy` 标签。 在 `cd.yml` 中的 `if:` 守卫会使用该 PR 的 head 提交为其运行作业,使你能在合并前在目标服务器上验证更改。
### 发布 — `publish.yml`
当你推送版本标签(例如 `v1.0.0`)时,或从 Actions 选项卡中手动运行该工作流时,会将你的应用附带来源证明发布到 npm。
**作用:**
1. 检出你的应用,设置 Node.js,并更新 npm(受信任发布需要 npm 11.5.1 或更高版本)。
2. 运行 `yarn twenty app:publish`,它会构建应用并将 `.twenty/output` 发布到 npm。 在 CI 中,它会自动添加 `--provenance` 和 `--access public`,因此在工作流中不需要任何标志。
**一次性设置:**
在 npmjs.com 上打开你的包 > **Settings → Trusted Publisher**,并使用 `publish.yml` 工作流为此仓库注册(参见 [npm trusted publishing 文档](https://docs.npmjs.com/trusted-publishers))。 带有 provenance 的发布会证明是哪个 GitHub 仓库构建了该包,这也同样是你在 Twenty 市场中声明应用所有权的方式。
<Note>
npm 仅接受来自**公共**源代码仓库的 provenance。 如果你从私有仓库发布,npm 会以 `E422 ...` 错误拒绝 OIDC provenance 包。 Unsupported GitHub Actions source repository visibility: "private"`错误。 要从私有仓库进行发布,请在发布步骤的`env`中将`TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'`设置为退出 provenance(在脚手架生成的`publish.yml\` 中包含了一条已注释的提示):
```yaml filename=".github/workflows/publish.yml"
- name: Publish to npm
env:
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
run: yarn twenty app:publish
```
</Note>
### 固定可复用的 actions
`ci.yml` 和 `cd.yml` 工作流引用了 `@main` 上的可复用 actions,因此会自动获取 `twentyhq/twenty` 仓库中的 action 更新。 如果你希望构建具有确定性,请在每个 `uses:` 行中将 `@main` 替换为某个提交的 SHA 或发行标签。
## 发布到 npm
发布到 npm 可让你的应用在 Twenty 应用市场中被发现。 任何 Twenty 工作区都可以直接通过 UI 浏览、安装和升级应用市场中的应用。
### 要求
* 一个 [npm](https://www.npmjs.com) 账户
* 你在 `package.json` 的 `keywords` 数组中的 `twenty-app` 关键字(需要手动添加 — 在 `create-twenty-app` 模板中默认不包含)
```json filename="package.json"
{
"name": "twenty-app-postcard-sender",
"version": "1.0.0",
"keywords": ["twenty-app"]
}
```
### 应用市场元数据
`defineApplication()` 配置支持可选字段,用于控制你的应用在市场中的展示方式。 使用 `logo` 和 `galleryImages` 来引用 `public/` 文件夹中的图片:
```ts src/application-config.ts
export default defineApplication({
universalIdentifier: '...',
displayName: 'My App',
description: 'A great app',
logo: 'public/logo.png',
galleryImages: [
'public/screenshot-1.png',
'public/screenshot-2.png',
],
});
```
完整的市场字段列表(`author`、`category`、`aboutDescription`、`websiteUrl`、`termsUrl` 等)请参见“构建应用”页面中的 [defineApplication 折叠面板](/l/zh/developers/extend/apps/config/application#marketplace-metadata)。
#### 建议的图库图片尺寸
该市场会在固定的 `8:5` 容器中渲染 `galleryImages`(例如,`1600×1000 px`)。
<Note>
任意纵横比的图库图片都会完整显示,且绝不会被裁剪,但相对于 `8:5` 明显更高或更窄的图片,两侧会出现空白边。
</Note>
#### 图片大小限制
`logo` 和每个 `galleryImages` 文件大小不得超过 **10 MB**。 当 marketplace 重新托管你已发布的资源时,较大的文件将被跳过,因此将不会显示。
### 发布
```bash filename="Terminal"
yarn twenty app:publish
```
要在特定的 dist-tag(例如 `beta` 或 `next`)下发布:
```bash filename="Terminal"
yarn twenty app:publish --tag beta
```
### 应用市场的发现机制如何运作
Twenty 服务器会**每小时**从 npm 注册表同步其市场目录。
你可以立即触发同步,而无需等待:
```bash filename="Terminal"
yarn twenty dev:catalog-sync
# To target a specific remote:
# yarn twenty dev:catalog-sync --remote production
```
在应用市场中显示的元数据来自你的 `defineApplication()` 配置——参见上面的 [应用市场元数据](#marketplace-metadata)。
<Note>
如果您的应用未在 `defineApplication()` 中定义 `aboutDescription`,市场将自动使用 npm 上您的软件包的 `README.md` 作为关于页面内容。 这意味着您可以为 npm 和 Twenty 市场维护同一个 README。 如果您希望在市场中使用不同的描述,请显式设置 `aboutDescription`。
</Note>
### CI 发布
上文所述生成的 `publish.yml` 工作流会在打上版本标签时,自动将包连同 provenance 一并发布到 npm。 由于 `yarn twenty app:publish` 在 CI 中运行时会为你添加 `--provenance` 和 `--access public`,因此该工作流不需要任何 npm 标志——只需要一次性完成受信任发布者的设置。
对于其他 CI 系统(GitLab CI、CircleCI 等),先运行 `yarn install`,然后运行 `yarn twenty app:publish`。 当环境可以生成 OIDC 令牌时会发出 provenance,否则会自动跳过。
<Note>
**npm provenance** 会在你的 npm 列表中添加可信徽章,使用户可以验证该包是由公共 CI 流水线中的特定提交构建的。 它也同样是你在 Twenty 市场中声明应用所有权的方式。 有关详细信息,请参见 [npm provenance 文档](https://docs.npmjs.com/generating-provenance-statements)。
</Note>
## 安装应用
一旦应用已发布(npm)或已部署(tarball),各工作区就可以通过 UI 进行安装。
在 Twenty 的 **Settings > Applications** 页面中,既可以浏览也可以安装来自市场或通过 tarball 部署的应用。
{/* TODO: add screenshot of the UI when the app is registered */}
你也可以通过命令行安装应用:
```bash filename="Terminal"
yarn twenty app:install
```
<Note>
服务器在安装时强制执行 SemVer 版本控制,与部署时的规则一致:
* 尝试安装与工作区中已安装版本相同的版本将被拒绝,并返回 `APP_ALREADY_INSTALLED` 错误。
* 尝试安装低于当前已安装版本的版本将被拒绝,并返回 `CANNOT_DOWNGRADE_APPLICATION` 错误。
若要安装较新的版本,请先部署或发布它,然后重新运行 `yarn twenty app:install`。
</Note>

View File

@@ -0,0 +1,126 @@
---
title: 同步与恢复
description: 何时使用哪个命令、如何解读同步输出,以及在本地元数据发生漂移时的恢复阶梯——在不得不执行完全重置之前。
icon: compass
---
本地应用开发围绕着**同步**展开:CLI 会重建你的 manifest,而服务器只会应用它与工作区中已存在元数据之间的差异。 本页介绍在不同情况下应使用哪个命令、如何阅读同步修改内容,以及当本地状态看起来不一致时——按顺序——应该怎么做。
## 在什么情况下用哪个命令
<Note>
在日常本地迭代中,你几乎总是需要 `yarn twenty dev`。 Deploy 和 publish 用于发布版本,**而不是**用于本地循环。
</Note>
| 如果你想要…… | 命令 | 备注 |
| ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| 使用实时同步进行本地迭代 | `yarn twenty dev` | 监视你的文件,并在每次更改时执行同步。 |
| 同步一次后退出(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`。 如果你发现自己为了测试本地改动而不断提升版本号,说明你在想要使用开发循环时却走了发布路径。
## 阅读同步输出
每次同步都会打印它实际应用的(或在使用 `plan` 时将要应用的)元数据更改,Terraform 风格——每个实体一个区块,包含其属性,然后是一行总结:
```text filename="Terminal"
# 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
Migration action 'create' for 'fieldMetadata' (universalIdentifier: 2020...4337) failed
```
使用该标识符在 manifest 中(以及在需要时在工作区中)定位该实体,而不是猜测哪个实体发生了冲突。
## 预览更改(计划)
`yarn twenty plan` 会构建你的 manifest,向服务器请求迁移计划并打印出来——**不会实际应用任何内容**。 这是在真正执行前,以安全方式回答“这次同步会改动什么?”的办法。
```bash filename="Terminal"
yarn twenty plan
```
```text filename="Terminal"
Building manifest...
Computing metadata plan (read-only, nothing will be applied)...
# fieldMetadata "crewCapacity" will be created
+ ...
Plan: 1 to add, 1 to change, 0 to destroy.
✓ Plan complete for My App — no changes were applied
```
一个计划:
* **不会写入任何内容**——不会进行元数据迁移、不会更新应用记录、不会更改默认角色/标签,也不会生成 API 客户端。
* 返回与真实同步将要应用的**相同 diff**,这样你可以预先审查将被创建 / 更新 / 删除的实体。
* 在执行高风险改动之前、在审查 AI 生成的改动时,或在某些如果即将落地意外变更就应当失败的脚本中,dry run 都非常有用。
<Note>
计划只会预览**元数据**更改,并且要求应用至少已经同步过一次(这样工作区才知道它的存在)。 如果你在一个从未同步过的应用上运行它,服务器会报告该应用尚未安装——先运行一次 `yarn twenty dev`。
</Note>
## 恢复梯子
当本地元数据看起来不对时,按以下顺序逐步升级,并在问题解决后立即停止。 每一步都比前一步更具破坏性。
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`,然后重新播种并重新同步。
<Warning>
`yarn twenty docker:reset` 会删除本地实例中的**所有**数据——包括每个工作区、记录和应用。 只有在前面步骤全部无效时才使用它。
</Warning>
<Note>
遇到元数据错误了吗? 请[提交 issue](https://github.com/twentyhq/twenty/issues/new/choose),并附上失败的迁移消息(包含其中的元数据类型和 `universalIdentifier`)、同步时的 `Metadata changes` 输出,以及你执行过的命令。
</Note>
## 避免在同一工作区上并发同步
同步会应用元数据迁移。 在**同一个工作区同时**运行多个同步、部署或安装操作——例如多个终端或多个 AI 代理并行迭代——会让这些迁移交错执行,从而让元数据处于部分应用的状态。
服务器会按工作区串行化同步以防止这种情况,但你仍然应当通过**单一**进程来处理敏感的元数据操作,而不要并发触发。 如果你使用多个代理来编排开发,请将它们的同步 / 部署 / 安装调用通过一个队列进行排队,这样任何时刻只有一个操作在运行。
## 区分不同类型的失败
当出现问题时,元数据 diff 和具名错误可以帮助你定位失败位置:
* **Manifest 构建错误**——CLI 在同步前失败(`MANIFEST_BUILD_FAILED`、`TYPECHECK_FAILED`);请修复你的应用源代码。
* **同步 / 迁移错误**——构建成功,但在应用 diff 时失败,并给出实体名称和 `universalIdentifier`;请修复冲突的元数据。
* **依赖项大小错误** — 同步或安装失败,是因为应用的生产环境 `dependencies` 过大,无法作为运行时层进行安装(`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`);请将逻辑函数在运行时不导入的包(UI 库、开发工具等)移动到 `devDependencies`。
* **应用代码运行时错误** — 同步成功,但你的逻辑函数或组件在运行时行为异常;请检查[函数日志](/l/zh/developers/extend/apps/operations/cli)。
* **本地实例状态** — 不属于以上任何一种情况,但工作区仍然显示异常;请按照恢复步骤逐级排查。

View File

@@ -0,0 +1,264 @@
---
title: 测试
description: Vitest 设置、针对真实 Twenty 服务器的集成测试、类型检查,以及使用 GitHub Actions 的 CI。
icon: flask
---
该 SDK 提供可编程的 API,使你可以在测试代码中构建、部署、安装和卸载你的应用。 结合 [Vitest](https://vitest.dev/) 和类型化 API 客户端,你可以编写集成测试,在真实的 Twenty 服务器上验证你的应用端到端运行是否正常。
## 使用 npm 包
可以在应用中安装并使用任意 npm 包。 逻辑函数和前端组件都通过 [esbuild](https://esbuild.github.io/) 打包,其依赖会被内联到输出中——运行时不需要 `node_modules`。
### 安装包
```bash filename="Terminal"
yarn add axios
```
然后在代码中导入它:
```ts src/logic-functions/fetch-data.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import axios from 'axios';
const handler = async (): Promise<any> => {
const { data } = await axios.get('https://api.example.com/data');
return { data };
};
export default defineLogicFunction({
universalIdentifier: '...',
name: 'fetch-data',
description: 'Fetches data from an external API',
timeoutSeconds: 10,
handler,
});
```
前端组件同样适用:
```tsx src/front-components/chart.tsx
import { defineFrontComponent } from 'twenty-sdk/define';
import { format } from 'date-fns';
const DateWidget = () => {
return <p>Today is {format(new Date(), 'MMMM do, yyyy')}</p>;
};
export default defineFrontComponent({
universalIdentifier: '...',
name: 'date-widget',
component: DateWidget,
});
```
### 打包的工作原理
构建步骤使用 esbuild 为每个逻辑函数和每个前端组件生成一个自包含文件。 导入的包会被内联到打包文件中,下面列出的除外。
**逻辑函数** 运行在 Node.js 环境中。 Node 内置模块(`fs`、`path`、`crypto`、`http` 等) 可用且无需安装。
**前端组件** 运行在 Web Worker 中。 Node 内置模块不可用——仅可使用可在浏览器环境中运行的 npm 包。 请注意,沙盒实现了*部分* DOM,因此一个包在构建时可能一切正常,但在运行时仍可能失败;参见[当前限制](/l/zh/developers/extend/apps/layout/front-components#current-limitations)。
两个环境都将 `twenty-client-sdk/core` 和 `twenty-client-sdk/metadata` 作为预置模块提供 — 这些模块不会被打包,而是在运行时由服务器解析。
前端组件也可以通过[共享依赖包](/l/zh/developers/extend/apps/layout/front-components#sharing-dependencies-across-front-components)来共享依赖。 列在应用 package.json 的 `frontComponentSharedDependencies` 字段中的包会被构建到一个单独的文件中,并在运行时从该文件加载,而不是被内联到每个组件中,因此只导入共享包的组件几乎不包含任何自身的依赖代码。
## 设置
脚手架生成的应用已包含 Vitest。 如果你手动进行设置,请安装这些依赖:
```bash filename="Terminal"
yarn add -D vitest vite-tsconfig-paths
```
在应用根目录下创建一个 `vitest.config.ts`:
```ts vitest.config.ts
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({
projects: ['tsconfig.spec.json'],
ignoreConfigErrors: true,
}),
],
test: {
testTimeout: 120_000,
hookTimeout: 120_000,
fileParallelism: false,
include: ['src/**/*.integration-test.ts'],
globalSetup: ['src/__tests__/global-setup.ts'],
env: {
TWENTY_API_URL,
TWENTY_API_KEY,
},
},
});
```
创建一个全局设置文件,用于验证服务器是否可访问,写入 SDK 的测试配置文件(`~/.twenty/config.test.json`),并在测试运行前同步应用:
```ts src/__tests__/global-setup.ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
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!;
// Verify the server is running
const response = await fetch(`${apiUrl}/healthz`);
if (!response.ok) {
throw new Error(`Twenty server is not reachable at ${apiUrl}.`);
}
// 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(CONFIG_DIR, 'config.test.json'),
JSON.stringify({
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 包上传到服务器 |
| `appDevOnce` | 构建并同步应用一次(与 `yarn twenty apply` 相同) |
| `appInstall` | 在活动工作区安装该应用 |
| `appUninstall` | 从活动工作区卸载该应用 |
每个函数都会返回一个结果对象,包含 `success: boolean`,以及 `data` 或 `error` 之一。
## 编写集成测试
下面是一个完整示例:构建、部署并安装该应用,然后验证它出现在工作区中:
```ts src/__tests__/app-install.integration-test.ts
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/application-config';
import { appBuild, appDeploy, appInstall, appUninstall } from 'twenty-sdk/cli';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const APP_PATH = process.cwd();
describe('App installation', () => {
beforeAll(async () => {
const buildResult = await appBuild({
appPath: APP_PATH,
tarball: true,
onProgress: (message: string) => console.log(`[build] ${message}`),
});
if (!buildResult.success) {
throw new Error(`Build failed: ${buildResult.error?.message}`);
}
const deployResult = await appDeploy({
tarballPath: buildResult.data.tarballPath!,
onProgress: (message: string) => console.log(`[deploy] ${message}`),
});
if (!deployResult.success) {
throw new Error(`Deploy failed: ${deployResult.error?.message}`);
}
const installResult = await appInstall({ appPath: APP_PATH });
if (!installResult.success) {
throw new Error(`Install failed: ${installResult.error?.message}`);
}
});
afterAll(async () => {
await appUninstall({ appPath: APP_PATH });
});
it('should find the installed app in the workspace', async () => {
const metadataClient = new MetadataApiClient();
const result = await metadataClient.query({
findManyApplications: {
id: true,
name: true,
universalIdentifier: true,
},
});
const installedApp = result.findManyApplications.find(
(app: { universalIdentifier: string }) =>
app.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
);
expect(installedApp).toBeDefined();
});
});
```
## 运行测试
确保你的本地 Twenty 服务器正在运行,然后:
```bash filename="Terminal"
yarn test
```
或者在开发期间使用监听模式:
```bash filename="Terminal"
yarn test:watch
```
## 类型检查
你也可以在不运行测试的情况下对应用进行类型检查:
```bash filename="Terminal"
yarn twenty dev:typecheck
```
这会针对你的应用的 `tsconfig.json` 运行 `tsc --noEmit`,并报告所有类型错误。 脚手架生成的应用还会提供一个 `yarn typecheck` 脚本,它也会覆盖测试文件(`tsconfig.spec.json`)。
## 使用 GitHub Actions 进行 CI
脚手架工具会在 `.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` 环境变量固定服务器版本。
完整的三个脚手架工作流(`ci.yml`、`cd.yml` 部署流水线以及用于 npm 发布的 `publish.yml`)的详细演练说明,请参见 [发布 → 自动化 CI/CD](/l/zh/developers/extend/apps/operations/publishing#automated-cicd-scaffolded-workflows)。

View File

@@ -0,0 +1,120 @@
---
title: 翻译
description: 以多种语言发布你的应用——通过一个 locales 目录翻译 manifest 标签和 UI 字符串。
icon: language
---
Twenty 应用均以**英文**编写:你的源码和 manifest 中的字符串
是英文源文本,`en` 是你进行翻译时的源语言环境,任何未被翻译的语言环境都会回退到它。
你的应用有两类可翻译文本,并且它们都通过同一个
`locales/` 目录:
* **Manifest labels** —— 对象和字段名称、视图标题、菜单项,以及
在应用元数据中声明的其他字符串。
* **前端组件字符串** —— 你的 React 前端组件渲染的 UI 文本。
你标记可翻译字符串,将它们提取到按语言环境划分的目录中,翻译这些目录,然后构建过程会为当前用户提供正确的语言——无需额外配置。
## 标记前端组件字符串
从 `twenty-sdk/front-component` 中导入翻译辅助函数:
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
const STATUSES = [
{ id: 'draft', label: msg('Draft') },
{ id: 'sent', label: msg('Sent') },
];
const Card = ({ count, name }: { count: number; name: string }) => {
const { t } = useTranslate();
return (
<section>
{/* Static text — reactive to the user's locale */}
<Trans>Loading postcard…</Trans>
{/* Disambiguate identical sources with a context */}
<Trans context="card-title">Untitled</Trans>
{/* Interpolation: pass values explicitly */}
<p>{t('Hi {name}', { name })}</p>
<p>{t('Saved {count} cards', { count })}</p>
{/* Resolve a lazily-declared descriptor */}
<ul>{STATUSES.map((s) => <li key={s.id}>{t(s.label)}</li>)}</ul>
</section>
);
};
```
### 何时使用哪一种
* **`Trans`** —— JSX 中的静态文本。 使用 `message` 和 `values` 属性进行插值,如 `Trans message="Hi {name}" values={{ name }}`;直接在子节点中插值无法被静态提取。
* **`useTranslate().t`** —— 组件内部的动态字符串。 当用户切换语言时会重新渲染。 在渲染逻辑中优先使用这个。
* **`t(...)`**(直接导入)—— 可在**任何地方**使用的即时翻译,包括事件处理函数、辅助函数和模块作用域——不仅仅在渲染逻辑中。
* **`msg(...)`** —— 以数据形式(常量、配置)声明字符串的惰性描述符。 在之后通过 `t(descriptor)` 来解析它。
### 上下文
传入 `context` 以区分那些源字符串相同但译法不同的情况:
```tsx
t({ message: 'Open', context: 'door' });
t({ message: 'Open', context: 'window' });
<Trans context="card-title">Untitled</Trans>
```
在区域设置目录中,上下文字符串会作为一组内容出现,其键为对应的上下文:`"\<context>": { "\<message>": "\<translation>" }`。 清单(Manifest)标签在结构上就是带有上下文的——每个标签都携带其所扮演的元数据角色(`objectMetadata.labelSingular`、`fieldMetadata.label`、`view.name` 等),因此同一个词在作为对象名称和作为视图名称时,可以有不同的翻译。
## 提取和翻译
在你的应用目录中运行提取命令:
```bash
twenty dev:translations-extract # collect strings into locales/en.json
twenty dev:translations-extract --locale fr-FR # also scaffold a target locale
```
提取会收集你的 manifest 标签以及前端组件源码中的 `t()`/`msg()`/`Trans` 字符串,并将它们放入 `locales/\<locale>.json` 中,以源字符串作为键。 填写翻译:
```json
// locales/fr-FR.json
{
"Loading postcard…": "Chargement de la carte…",
"Hi {name}": "Bonjour {name}",
"Saved {count} cards": "{count} cartes enregistrées",
"door": {
"Open": "Ouvrir"
},
"objectMetadata.labelSingular": {
"Postcard": "Carte postale"
}
}
```
无上下文字符串是扁平条目;有上下文字符串——包括每个清单标签——都嵌套在其上下文键下。
像 `{name}` 这样的占位符会在运行时被替换——请在翻译中保留它们。 任何留空的字符串都会回退到源文本。
## 运行方式
`twenty dev:build` 会编译目录并为当前用户提供正确的语言:manifest 标签在服务端解析,前端组件目录被打包进每个组件 bundle 中。 在运行时,组件会从其执行上下文(宿主的当前语言)中读取语言环境,并将每个字符串在其目录中解析,当翻译缺失时回退到源文本。 在宿主中切换语言会实时重新渲染 `Trans` 和 `useTranslate().t` 的字符串。
清单标签会在每次同步时编译——`twenty dev:build`、`twenty apply`
以及持续监视的 `twenty dev` 都是如此——因此,编辑区域设置文件并
同步即可看到新文本。 Front 组件目录会在构建时被嵌入到
每个组件包中,因此更改其中之一仍意味着
需要重新运行 `twenty dev:build`(并重新部署)。
`Trans` 的文本子节点可以跨多行——空白会以与 JSX 相同的方式被折叠,因此这两种写法都会被提取为键 `Welcome back`:
```tsx
<Trans>Welcome back</Trans>
<Trans>
Welcome
back
</Trans>
```