Files
upstream-mirror e7da97194a
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
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

270 lines
13 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 连接
description: 通过 OAuth 让你的应用代表用户在第三方服务中执行操作。
icon: plug
---
连接是用户为外部服务(Linear、GitHub、Slack 等)持有的凭据。 你的应用声明**如何**获取这些凭据——即**连接提供程序**——并在运行时使用它们向第三方 API 发起认证调用。
目前仅支持 OAuth 2.0。 将来的凭据类型(个人访问令牌、API 密钥、基本身份验证)将接入相同的接口——已经使用 `defineConnectionProvider({ type: 'oauth', ... })` 的应用将无需迁移。
<AccordionGroup>
<Accordion title="defineConnectionProvider" description="声明你的应用如何获取连接">
连接提供程序描述了你的应用所需的 OAuth 握手流程。 用户在你的应用设置中点击"添加连接",完成提供方的授权同意页面后,会在其工作区中创建一条 `ConnectedAccount` 行。
一个可用的配置需要**两个文件**——连接提供程序,以及在 `defineApplication` 上与之匹配、用于保存 OAuth 客户端凭据的 `serverVariables` 声明。
```ts src/connection-providers/linear-connection.ts
import { defineConnectionProvider } from 'twenty-sdk/define';
export default defineConnectionProvider({
universalIdentifier: '9c7d1f5e-6a0b-4d44-be0c-3f8b5a9d4e6f',
name: 'linear',
displayName: 'Linear',
icon: 'IconBrandLinear',
type: 'oauth',
oauth: {
authorizationEndpoint: 'https://linear.app/oauth/authorize',
tokenEndpoint: 'https://api.linear.app/oauth/token',
scopes: ['read', 'write'],
// These must match keys in `defineApplication.serverVariables` below.
clientIdVariable: 'LINEAR_CLIENT_ID',
clientSecretVariable: 'LINEAR_CLIENT_SECRET',
// Optional: defaults to 'json'. Some providers (Linear, Slack) want
// 'form-urlencoded' for the token request.
tokenRequestContentType: 'form-urlencoded',
// Optional: defaults to true. Disable only if the provider rejects PKCE.
usePkce: false,
// Optional: extra query params on the authorize URL.
// authorizationParams: { prompt: 'consent' },
// Optional: provider's RFC 7009 token revocation endpoint, called on disconnect.
// revokeEndpoint: 'https://example.com/oauth/revoke',
},
// Optional: a logic function in this app to run right after a connection is
// established. See "Run a logic function on connect".
// onConnectLogicFunction: { universalIdentifier: '3a2b1c0d-...-...' },
// Optional: a logic function in this app to run right after a connection is
// removed. See "Run a logic function on disconnect".
// onDisconnectLogicFunction: { universalIdentifier: '4d5e6f70-...-...' },
});
```
```ts src/application.config.ts
import { defineApplication } from 'twenty-sdk/define';
export default defineApplication({
universalIdentifier: '...',
displayName: 'Linear',
description: 'Connect Linear to Twenty.',
// OAuth client credentials live on the app registration (one OAuth app per
// Twenty server, configured by the admin) — not per-workspace. Declare them
// as serverVariables so the admin can fill them in once for all installs.
serverVariables: {
LINEAR_CLIENT_ID: {
description: 'OAuth client ID from your Linear OAuth application.',
isSecret: false,
isRequired: true,
},
LINEAR_CLIENT_SECRET: {
description: 'OAuth client secret from your Linear OAuth application.',
isSecret: true,
isRequired: true,
},
},
});
```
关键点:
* `name` 是在 `listConnections({ providerName })` 中使用的唯一标识符字符串(短横线命名(kebab-case),必须匹配 `^[a-z][a-z0-9-]*$`)。
* `displayName` 会显示在每个应用的设置选项卡以及 AI 工具列表中。
* `clientIdVariable` / `clientSecretVariable` 是**名称**,而不是值——它们必须与 `defineApplication.serverVariables` 中声明的键匹配。 实际的 `client_id` 和 `client_secret` 由服务器管理员通过应用注册 UI 输入,绝不会提交到你的仓库。
* 请使用 `serverVariables`(而非 `applicationVariables`)——OAuth 凭据是服务器范围的,并且每个 Twenty 服务器只配置一个 OAuth 应用。
* 在两个 `serverVariables` 都填写之前,每个应用的设置选项卡会显示"需要服务器管理员"的提示,并且"添加连接"按钮将被禁用。
* `type: 'oauth'` 是目前唯一受支持的取值。 该判别器具备前向兼容性:未来的类型(`'pat'`、`'api-key'` 等) 将会与 `oauth` 并列新增子配置块。
你的提供方需要加入白名单的 OAuth 回调 URL 为:
```
https://<your-twenty-server>/auth/apps/callback
```
</Accordion>
<Accordion title="在连接时运行逻辑函数" description="在连接建立时立即响应">
某些提供方会在连接时向你提供数据,而在连接真正可用之前,你需要先持久化这些数据——一个典型示例是 Slack,其中 OAuth 响应会标识工作区的 `team_id`,后续传入事件将会基于该值进行关联。 将 `onConnectLogicFunction` 设置为引用同一应用中的某个逻辑函数(通过其 `universalIdentifier`),该函数会在创建 `ConnectedAccount` 之后立即运行。
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs claimSlackTeam after every successful Slack connection.
onConnectLogicFunction: {
universalIdentifier: '3a2b1c0d-1111-4222-8333-444455556666',
},
});
```
该钩子**在发起连接的工作区中以异步方式运行**(它会入队执行,而不是被等待),因此缓慢或失败的钩子永远不会阻塞或破坏 OAuth 回调——请确保该钩子是幂等的,并能自行处理重试。 处理程序会接收到:
```ts
type OnConnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
接下来,使用 `getConnection(connectedAccountId)` 读取最新的访问令牌,并调用提供商的 API(例如 Slack `auth.test`),或者使用[key-value store](/l/zh/developers/extend/apps/logic/key-value-store) 持久化映射。
</Accordion>
<Accordion title="在断开连接时运行逻辑函数" description="在移除连接时进行清理">
应用在连接时声明的任何内容,都必须在连接断开时被释放。 例如,一个在连接时声明了 `team_id` 的 Slack 集成,必须在连接结束时释放该声明,以便其他工作区可以连接同一个 Slack 团队。 将 `onDisconnectLogicFunction` 设置为引用同一应用中的某个逻辑函数,该函数会在删除 `ConnectedAccount` 之后立即运行。
```ts src/connection-providers/slack-connection.ts
export default defineConnectionProvider({
universalIdentifier: '...',
name: 'slack',
displayName: 'Slack',
type: 'oauth',
oauth: {
/* ... */
},
// Runs releaseSlackTeam after every Slack disconnection.
onDisconnectLogicFunction: {
universalIdentifier: '4470aba8-5ff5-4800-88db-2a427cd8677c',
},
});
```
与 on-connect 钩子类似,它在**正在断开连接的工作区中以异步方式运行**,并且不会阻塞断开过程。 处理程序接收的负载结构相同:
```ts
type OnDisconnectPayload = {
connectionProviderId: string;
connectionProviderName: string; // e.g. 'slack'
connectedAccountId: string;
};
```
当钩子运行时,`ConnectedAccount` 已经不存在了,因此 `getConnection(connectedAccountId)` 将不再返回结果。 清理所需的任何内容(例如 `team_id`、外部订阅 ID)必须在连接时写入到按 `connectedAccountId` 作为键的[key-value store](/l/zh/developers/extend/apps/logic/key-value-store) 中。
当连接自身被移除时,该钩子会被触发。 卸载应用会通过数据库级联来删除其连接,因此该钩子在这种情况下不会运行。 在 `defineApplication` 上声明一个 `uninstallLogicFunction` 用于该路径:它在应用的元数据被删除之前运行,因此仍然可以调用 `listConnections` 并清理剩余的内容。
</Accordion>
<Accordion title="listConnections / getConnection" description="在逻辑函数中使用连接">
在逻辑函数处理器内,`listConnections({ providerName })` 会返回此应用针对给定提供方的 `ConnectedAccount` 行,并附带已刷新的访问令牌。
```ts src/logic-functions/handlers/create-linear-issue-handler.ts
import { listConnections } from 'twenty-sdk/logic-function';
export const createLinearIssueHandler = async (input: {
teamId?: string;
title?: string;
}) => {
if (!input.teamId || !input.title) {
return { success: false, error: 'teamId and title are required' };
}
const connections = await listConnections({ providerName: 'linear' });
// Workspace-shared credentials win when present; fall back to the first
// user-visibility one. For HTTP-route triggers you typically pick the
// request user's connection via event.userWorkspaceId instead.
const connection =
connections.find((c) => c.visibility === 'workspace') ?? connections[0];
if (!connection) {
return {
success: false,
error:
'Linear is not connected. Open the app settings and click "Add connection".',
};
}
// Use connection.accessToken to call the third-party API.
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${connection.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation { issueCreate(input: { teamId: "${input.teamId}", title: "${input.title}" }) { success } }`,
}),
});
return { success: response.ok };
};
```
每个连接包含:
| 字段 | 描述 |
| ----------------- | -------------------------------------------------------------------------------------------- |
| `id` | 唯一的行 id;传给 `getConnection(id)` 以重新获取单个连接 |
| `visibility` | `'user'`(仅对单个工作区成员私有)或 `'workspace'`(与所有成员共享) |
| `scopes` | 上游提供方授予的 OAuth 权限(不同于 `visibility`——两者不相关) |
| `userWorkspaceId` | 所有者的 userWorkspace id——在 HTTP 路由触发器中用于选择"请求用户的连接"很有用 |
| `accessToken` | 最新的 OAuth 访问令牌(若已过期会自动刷新) |
| `name` | 连接的显示名称(在 OAuth 回调时自动生成,用户可重命名) |
| `handle` | 已连接的上游帐户电子邮件,每次重新连接时刷新; 从 OIDC `id_token` 返回的 token\` 解决方案在代币交换中返回,回退到连接20个用户的电子邮件,当提供商不返回一个 |
| `authFailedAt` | 当最近一次刷新失败时会设置;用户必须重新连接 |
关键点:
* 传入 `{ providerName }` 以按提供方筛选;省略它则可获取此应用在所有提供方上的全部连接。
* 服务器会在返回前透明地刷新访问令牌。 你的处理器始终会拿到可用的令牌(或已设置 `authFailedAt`)。
* `getConnection(id)` 是获取单行记录的对应方法。
</Accordion>
<Accordion title="按用户与工作区共享的可见性" description="用户如何在私有与共享凭据之间进行选择">
当用户点击"添加连接"时,系统会提示其选择可见性:
* **仅限我**——该凭据仅对连接的用户私有。 代表其调用的任何逻辑函数(带有 `isAuthRequired: true` 的 HTTP 路由触发器)都可以看到它;Cron 触发器和数据库事件则不可。
* **工作区共享**——任何工作区成员都可以使用该凭据。 Cron / 数据库触发器也可以使用它,因为它们没有请求用户。
为每个处理器使用合适的类型:
```ts
// HTTP-route trigger — prefer the request user's own connection.
const conn =
connections.find((c) => c.userWorkspaceId === event.userWorkspaceId) ??
connections.find((c) => c.visibility === 'workspace');
// Cron trigger — no request user; only shared credentials are sensible.
const conn = connections.find((c) => c.visibility === 'workspace');
```
每个(用户、提供方)允许有多个连接,因此同一用户可以同时拥有"个人 Linear"和"工作 Linear"。
</Accordion>
<Accordion title="一次性提供方设置" description="在第三方服务中注册你的 OAuth 应用">
对于每个连接提供方,服务器管理员需要先在第三方注册一个 OAuth 应用。
1. 前往提供方的开发者设置(例如 https://linear.app/settings/api/applications/new)。
2. 将 **Redirect URI** 设置为 `\<SERVER_URL>/auth/apps/callback`。
3. 复制生成的**Client ID**和**Client Secret**。
4. 以服务器管理员身份在 Twenty 中打开已安装的应用 → 在相应的 `serverVariables` 上设置这些值。
5. 之后,工作区成员可以在每个应用的**连接**部分添加连接。
</Accordion>
</AccordionGroup>