i18n - docs translations (#19234)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
f3e2e00e79
commit
1622c87b7a
@@ -4,24 +4,24 @@ description: 使用 Twenty SDK 定义对象、逻辑函数、前端组件等。
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Apps are currently in alpha. The feature works but is still evolving.
|
||||
应用目前处于 Alpha 阶段。 该功能可用,但仍在演进中。
|
||||
</Warning>
|
||||
|
||||
The `twenty-sdk` package provides typed building blocks to create your app. This page covers every entity type and API client available in the SDK.
|
||||
`twenty-sdk` 包提供类型化的构建块,用于创建你的应用。 本页涵盖 SDK 中可用的所有实体类型和 API 客户端。
|
||||
|
||||
## DefineEntity functions
|
||||
## DefineEntity 函数
|
||||
|
||||
The SDK provides functions to define your app entities. You must use `export default defineEntity({...})` for the SDK to detect your entities. 这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
|
||||
SDK 提供用于定义你的应用实体的函数。 你必须使用 `export default defineEntity({...})`,这样 SDK 才能检测到你的实体。 这些函数会在构建时校验你的配置,并提供 IDE 自动补全和类型安全。
|
||||
|
||||
<Note>
|
||||
**File organization is up to you.**
|
||||
Entity detection is AST-based — the SDK finds `export default defineEntity(...)` calls regardless of where the file lives. Grouping files by type (e.g., `logic-functions/`, `roles/`) is just a convention, not a requirement.
|
||||
**文件组织由你决定。**
|
||||
实体检测基于 AST——无论文件位于何处,SDK 都能找到 `export default defineEntity(...)` 的调用。 按类型对文件分组(例如 `logic-functions/`、`roles/`)只是代码组织的一种约定,并非必需。
|
||||
</Note>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="defineRole" description="配置角色权限和对象访问">
|
||||
|
||||
Roles encapsulate permissions on your workspace's objects and actions.
|
||||
角色封装了对你的工作空间对象与操作的权限。
|
||||
|
||||
```ts restricted-company-role.ts
|
||||
import {
|
||||
@@ -69,12 +69,12 @@ export default defineRole({
|
||||
</Accordion>
|
||||
<Accordion title="defineApplication" description="配置应用元数据(必需,每个应用一个)">
|
||||
|
||||
Every app must have exactly one `defineApplication` call that describes:
|
||||
每个应用必须且只能有一个 `defineApplication` 调用,用于描述:
|
||||
|
||||
* **Identity**: identifiers, display name, and description.
|
||||
* **Permissions**: which role its functions and front components use.
|
||||
* **(Optional) Variables**: key–value pairs exposed to your functions as environment variables.
|
||||
* **(Optional) Pre-install / post-install functions**: logic functions that run before or after installation.
|
||||
* **应用的身份**:标识符、显示名称和描述。
|
||||
* **权限**:其函数和前端组件所使用的角色。
|
||||
* **(可选)变量**:以环境变量形式提供给函数的键值对。
|
||||
* **(可选)安装前/安装后函数**:在安装之前或之后运行的逻辑函数。
|
||||
|
||||
```ts src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
@@ -98,21 +98,21 @@ export default defineApplication({
|
||||
```
|
||||
|
||||
备注:
|
||||
* `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs.
|
||||
* `applicationVariables` become environment variables for your functions and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` must reference a role defined with `defineRole()` (see above).
|
||||
* Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||||
* `universalIdentifier` 字段是你拥有的确定性 ID。 只需生成一次,并在多次同步过程中保持稳定不变。
|
||||
* `applicationVariables` 会变成你的函数和前端组件可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
|
||||
* `defaultRoleUniversalIdentifier` 必须引用使用 `defineRole()` 定义的角色(见上文)。
|
||||
* 在构建清单时会自动检测安装前/安装后函数——无需在 `defineApplication()` 中引用它们。
|
||||
|
||||
#### 应用市场元数据
|
||||
|
||||
If you plan to [publish your app](/l/zh/developers/extend/apps/publishing), these optional fields control how it appears in the marketplace:
|
||||
如果你计划[发布你的应用](/l/zh/developers/extend/apps/publishing),这些可选字段将控制你的应用在应用市场中的展示:
|
||||
|
||||
| 字段 | 描述 |
|
||||
| ------------------ | -------------------------------------------------------------- |
|
||||
| `作者` | 作者或公司名称 |
|
||||
| `类别` | 用于应用市场筛选的应用类别 |
|
||||
| `logoUrl` | Path to your app logo (e.g., `public/logo.png`) |
|
||||
| `screenshots` | Array of screenshot paths (e.g., `public/screenshot-1.png`) |
|
||||
| `logoUrl` | 应用徽标的路径(例如 `public/logo.png`) |
|
||||
| `screenshots` | 截图路径数组(例如 `public/screenshot-1.png`) |
|
||||
| `aboutDescription` | 用于“关于”选项卡的更长的 Markdown 描述。 如果省略,市场将使用该软件包在 npm 上的 `README.md`。 |
|
||||
| `websiteUrl` | 你的网站链接 |
|
||||
| `termsUrl` | 服务条款链接 |
|
||||
@@ -121,15 +121,15 @@ If you plan to [publish your app](/l/zh/developers/extend/apps/publishing), thes
|
||||
|
||||
#### 角色和权限
|
||||
|
||||
The `defaultRoleUniversalIdentifier` in `application-config.ts` designates the default role used by your app's logic functions and front components. See `defineRole` above for details.
|
||||
`application-config.ts` 中的 `defaultRoleUniversalIdentifier` 字段指定你的应用的逻辑函数和前端组件所使用的默认角色。 详见上文的 `defineRole`。
|
||||
|
||||
* The runtime token injected as `TWENTY_APP_ACCESS_TOKEN` is derived from this role.
|
||||
* The typed client is restricted to the permissions granted to that role.
|
||||
* Follow least-privilege: create a dedicated role with only the permissions your functions need.
|
||||
* 作为 `TWENTY_APP_ACCESS_TOKEN` 注入的运行时令牌来源于该角色。
|
||||
* 类型化客户端将受限于该角色授予的权限。
|
||||
* 遵循最小权限原则:创建一个仅包含你的函数所需权限的专用角色。
|
||||
|
||||
##### Default function role
|
||||
##### 默认函数角色
|
||||
|
||||
When you scaffold a new app, the CLI creates a default role file:
|
||||
当你使用脚手架创建新应用时,CLI 会创建一个默认角色文件:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
@@ -155,16 +155,16 @@ export default defineRole({
|
||||
});
|
||||
```
|
||||
|
||||
This role's `universalIdentifier` is referenced in `application-config.ts` as `defaultRoleUniversalIdentifier`:
|
||||
该角色的 `universalIdentifier` 会在 `application-config.ts` 中被引用为 `defaultRoleUniversalIdentifier`:
|
||||
|
||||
* **\*.role.ts** defines what the role can do.
|
||||
* **\*.role.ts** 定义该角色可以执行的操作。
|
||||
* **application-config.ts** 指向该角色,使你的函数继承其权限。
|
||||
|
||||
备注:
|
||||
* 从脚手架生成的角色开始,然后按照最小权限原则逐步收紧权限。
|
||||
* Replace `objectPermissions` and `fieldPermissions` with the objects and fields your functions actually need.
|
||||
* `permissionFlags` 控制对平台级能力的访问。 Keep them minimal.
|
||||
* See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
* 将 `objectPermissions` 和 `fieldPermissions` 替换为你的函数所需的对象/字段。
|
||||
* `permissionFlags` 控制对平台级能力的访问。 尽量保持最小化。
|
||||
* 查看一个可运行示例:[`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts)。
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineObject" description="定义带字段的自定义对象">
|
||||
@@ -256,7 +256,7 @@ export default defineObject({
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Standard fields" description="为现有对象扩展额外字段">
|
||||
<Accordion title="defineField — 标准字段" description="为现有对象扩展额外字段">
|
||||
|
||||
使用 `defineField()` 向你不拥有的对象添加字段——例如标准的 Twenty 对象(Person、Company 等)。 或来自其他应用的对象。 与在 `defineObject()` 中的内联字段不同,独立字段需要一个 `objectUniversalIdentifier` 来指定它们要扩展的对象:
|
||||
|
||||
@@ -284,7 +284,7 @@ export default defineField({
|
||||
* `defineField()` 是为非通过 `defineObject()` 创建的对象添加字段的唯一方式。
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="defineField — Relation fields" description="Connect objects together with bidirectional relations">
|
||||
<Accordion title="defineField — 关联字段" description="使用双向关系将对象连接在一起">
|
||||
|
||||
关系用于将对象彼此连接。 在 Twenty 中,关系始终是双向的——你需要定义两侧,每一侧都引用另一侧。
|
||||
|
||||
@@ -443,7 +443,7 @@ export default defineObject({
|
||||
});
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="defineLogicFunction" description="Define logic functions and their triggers">
|
||||
<Accordion title="defineLogicFunction" description="定义逻辑函数及其触发器">
|
||||
|
||||
每个函数文件都使用 `defineLogicFunction()` 导出包含处理程序和可选触发器的配置。
|
||||
|
||||
@@ -487,15 +487,15 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
Available trigger types:
|
||||
* **httpRoute**: Exposes your function on an HTTP path and method **under the `/s/` endpoint**:
|
||||
> e.g. `path: '/post-card/create'` is callable at `https://your-twenty-server.com/s/post-card/create`
|
||||
可用的触发器类型:
|
||||
* **httpRoute**:在 **`/s/` 端点**下通过 HTTP 路径和方法公开你的函数:
|
||||
> 例如 `path: '/post-card/create'` 可在 `https://your-twenty-server.com/s/post-card/create` 调用
|
||||
* **cron**:使用 CRON 表达式按计划运行你的函数。
|
||||
* **databaseEvent**:在工作空间对象生命周期事件上运行。 当事件操作为 `updated` 时,可以在 `updatedFields` 数组中指定要监听的特定字段。 如果未定义或为空,任何更新都会触发该函数。
|
||||
> e.g. `person.updated`, `*.created`, `company.*`
|
||||
> 例如 `person.updated`、`*.created`、`company.*`
|
||||
|
||||
<Note>
|
||||
You can also manually execute a function using the CLI:
|
||||
你也可以使用 CLI 手动执行函数:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
||||
@@ -505,7 +505,7 @@ yarn twenty exec -n create-new-post-card -p '{"key": "value"}'
|
||||
yarn twenty exec -y e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf
|
||||
```
|
||||
|
||||
You can watch logs with:
|
||||
你可以通过以下方式查看日志:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty logs
|
||||
@@ -514,9 +514,9 @@ yarn twenty logs
|
||||
|
||||
#### 路由触发器负载
|
||||
|
||||
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
|
||||
[AWS HTTP API v2 format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
|
||||
Import the `RoutePayload` type from `twenty-sdk`:
|
||||
当路由触发器调用你的逻辑函数时,它会接收一个遵循
|
||||
[AWS HTTP API v2 格式](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html)的 `RoutePayload` 对象。
|
||||
从 `twenty-sdk` 导入 `RoutePayload` 类型:
|
||||
|
||||
```ts
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk';
|
||||
@@ -531,21 +531,21 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
`RoutePayload` 类型具有以下结构:
|
||||
|
||||
| 属性 | 类型 | 描述 | 示例 |
|
||||
| ---------------------------- | ------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) | see section below |
|
||||
| `queryStringParameters` | `Record<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record<string, string \| undefined>` | Path parameters extracted from the route pattern | `/users/:id`, `/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | 已解析的请求体(JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 | |
|
||||
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE) | |
|
||||
| `requestContext.http.path` | `string` | 原始请求路径 | |
|
||||
| 属性 | 类型 | 描述 | 示例 |
|
||||
| ---------------------------- | ------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `headers` | `Record<string, string \| undefined>` | HTTP 请求头(仅限 `forwardedRequestHeaders` 中列出的那些) | 见下文 |
|
||||
| `queryStringParameters` | `Record<string, string \| undefined>` | 查询字符串参数(多个值以逗号连接) | `/users?ids=1&ids=2&ids=3&name=Alice` -> `{ ids: '1,2,3', name: 'Alice' }` |
|
||||
| `pathParameters` | `Record<string, string \| undefined>` | 从路由模式中提取的路径参数 | `/users/:id`,`/users/123` -> `{ id: '123' }` |
|
||||
| `body` | `object \| null` | 已解析的请求体(JSON) | `{ id: 1 }` -> `{ id: 1 }` |
|
||||
| `isBase64Encoded` | `boolean` | 请求体是否为 base64 编码 | |
|
||||
| `requestContext.http.method` | `string` | HTTP 方法(GET、POST、PUT、PATCH、DELETE) | |
|
||||
| `requestContext.http.path` | `string` | 原始请求路径 | |
|
||||
|
||||
|
||||
#### forwardedRequestHeaders
|
||||
|
||||
出于安全原因,默认**不会**将传入请求的 HTTP 请求头传递给你的逻辑函数。
|
||||
To access specific headers, list them in the `forwardedRequestHeaders` array:
|
||||
如需访问特定请求头,请在 `forwardedRequestHeaders` 数组中显式列出:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
@@ -561,7 +561,7 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
In your handler, access the forwarded headers like this:
|
||||
在你的处理程序中,可以这样访问被转发的请求头:
|
||||
|
||||
```ts
|
||||
const handler = async (event: RoutePayload) => {
|
||||
@@ -574,14 +574,14 @@ const handler = async (event: RoutePayload) => {
|
||||
```
|
||||
|
||||
<Note>
|
||||
请求头名称会被规范化为小写。 Access them using lowercase keys (e.g., `event.headers['content-type']`).
|
||||
请求头名称会被规范化为小写。 请使用小写键访问它们(例如,`event.headers['content-type']`)。
|
||||
</Note>
|
||||
|
||||
#### Exposing a function as a tool
|
||||
#### 将函数作为工具公开
|
||||
|
||||
逻辑函数可以作为供 AI 智能体和工作流使用的**工具**对外提供。 When marked as a tool, a function becomes discoverable by Twenty's AI features and can be used in workflow automations.
|
||||
逻辑函数可以作为供 AI 智能体和工作流使用的**工具**对外提供。 当函数被标记为工具时,Twenty 的 AI 功能即可发现它,并可在工作流自动化中使用。
|
||||
|
||||
To mark a logic function as a tool, set `isTool: true`:
|
||||
要将逻辑函数标记为工具,请设置 `isTool: true`:
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
@@ -617,8 +617,8 @@ export default defineLogicFunction({
|
||||
|
||||
关键点:
|
||||
|
||||
* You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events at the same time.
|
||||
* **`toolInputSchema`** (optional): A JSON Schema object describing the parameters your function accepts. The schema is computed automatically from source code static analysis, but you can set it explicitly:
|
||||
* 你可以将 `isTool` 与触发器结合使用——一个函数既可以作为工具(由 AI 代理调用),也可以同时由事件触发。
|
||||
* **`toolInputSchema`**(可选):描述函数可接受参数的 JSON Schema 对象。 该模式会通过对源代码的静态分析自动推导,但你也可以显式设置:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
@@ -715,11 +715,11 @@ yarn twenty exec --postInstall
|
||||
</Accordion>
|
||||
<Accordion title="defineFrontComponent" description="为自定义 UI 定义前端组件">
|
||||
|
||||
Front components are React components that render directly inside Twenty's UI. They run in an **isolated Web Worker** using Remote DOM — your code is sandboxed but renders natively in the page, not in an iframe.
|
||||
前端组件是直接在 Twenty 的 UI 内渲染的 React 组件。 它们在使用 Remote DOM 的**隔离 Web Worker**中运行——你的代码在沙盒中执行,但会原生渲染到页面中,而非在 iframe 里。
|
||||
|
||||
#### Basic example
|
||||
#### 基础示例
|
||||
|
||||
The quickest way to see a front component in action is to register it as a **command**. Adding a `command` field with `isPinned: true` makes it appear as a quick-action button in the top-right corner of the page — no page layout needed:
|
||||
最快体验前端组件运行方式的方法是将其注册为一个**命令**。 添加一个 `command` 字段并设置 `isPinned: true`,即可让它以快速操作按钮的形式出现在页面右上角——无需页面布局:
|
||||
|
||||
```tsx src/front-components/hello-world.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk';
|
||||
@@ -749,24 +749,24 @@ export default defineFrontComponent({
|
||||
});
|
||||
```
|
||||
|
||||
After syncing with `yarn twenty dev`, the quick action appears in the top-right corner of the page:
|
||||
使用 `yarn twenty dev` 同步后,快速操作会出现在页面右上角:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="Quick action button in the top-right corner" />
|
||||
<img src="/images/docs/developers/extends/apps/quick-action.png" alt="右上角的快速操作按钮" />
|
||||
</div>
|
||||
|
||||
Click it to render the component inline.
|
||||
点击它以内联方式渲染该组件。
|
||||
|
||||
{/* TODO: add screenshot of the rendered front component */}
|
||||
|
||||
#### Configuration fields
|
||||
#### 配置字段
|
||||
|
||||
| 字段 | 必填 | 描述 |
|
||||
| --------------------- | -- | ----------------------------------------------------------------------------------- |
|
||||
| `universalIdentifier` | 是 | Stable unique ID for this component |
|
||||
| `component` | 是 | A React component function |
|
||||
| `name` | 否 | Display name |
|
||||
| `描述` | 否 | Description of what the component does |
|
||||
| `universalIdentifier` | 是 | 该组件的稳定唯一 ID |
|
||||
| `component` | 是 | 一个 React 组件函数 |
|
||||
| `name` | 否 | 显示名称 |
|
||||
| `描述` | 否 | 组件的功能描述 |
|
||||
| `isHeadless` | 否 | Set to `true` if the component has no visible UI (see below) |
|
||||
| `命令` | 否 | Register the component as a command (see [command options](#command-options) below) |
|
||||
|
||||
|
||||
@@ -4,142 +4,142 @@ description: 几分钟内创建你的第一个 Twenty 应用。
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Apps are currently in alpha. The feature works but is still evolving.
|
||||
应用目前处于 Alpha 阶段。 该功能可用,但仍在演进中。
|
||||
</Warning>
|
||||
|
||||
应用可通过自定义对象、字段、逻辑函数、AI 技能和 UI 组件来扩展 Twenty——全部以代码进行管理。
|
||||
|
||||
## 先决条件
|
||||
|
||||
Before you begin, make sure the following is installed on your machine:
|
||||
在开始之前,请确保你的机器已安装以下内容:
|
||||
|
||||
* **Node.js 24+** — [Download here](https://nodejs.org/)
|
||||
* **Yarn 4** — Comes with Node.js via Corepack. Enable it by running `corepack enable`
|
||||
* **Docker** — [Download here](https://www.docker.com/products/docker-desktop/). Required to run a local Twenty instance. Not needed if you already have a Twenty server running.
|
||||
* **Node.js 24+** — [在此下载](https://nodejs.org/)
|
||||
* **Yarn 4** — 通过 Corepack 随 Node.js 提供。 通过运行 `corepack enable` 启用它
|
||||
* **Docker** — [在此下载](https://www.docker.com/products/docker-desktop/)。 运行本地 Twenty 实例所必需。 如果你已经有一个正在运行的 Twenty 服务器,则不需要。
|
||||
|
||||
## Step 1: Scaffold your app
|
||||
## 步骤 1:为你的应用创建脚手架
|
||||
|
||||
Open a terminal and run:
|
||||
打开终端并运行:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
```
|
||||
|
||||
You will be prompted to enter a name and a description for your app. Press **Enter** to accept the defaults.
|
||||
系统会提示你为应用输入名称和描述。 按下 **Enter** 接受默认值。
|
||||
|
||||
This creates a new folder called `my-twenty-app` with everything you need.
|
||||
这会创建一个名为 `my-twenty-app` 的新文件夹,其中包含你所需的一切。
|
||||
|
||||
<Note>
|
||||
The scaffolder supports these flags:
|
||||
脚手架工具支持以下标志:
|
||||
|
||||
* `--minimal` — scaffold only the essential files, no examples (default)
|
||||
* `--exhaustive` — scaffold all example entities
|
||||
* `--name <name>` — set the app name (skips the prompt)
|
||||
* `--display-name <displayName>` — set the display name (skips the prompt)
|
||||
* `--description <description>` — set the description (skips the prompt)
|
||||
* `--skip-local-instance` — skip the local server setup prompt
|
||||
* `--minimal` — 仅创建必要文件,不包含示例(默认)
|
||||
* `--exhaustive` — 创建所有示例实体
|
||||
* `--name <name>` — 设置应用名称(跳过提示)
|
||||
* `--display-name <displayName>` — 设置显示名称(跳过提示)
|
||||
* `--description <description>` — 设置描述(跳过提示)
|
||||
* `--skip-local-instance` — 跳过本地服务器设置提示
|
||||
</Note>
|
||||
|
||||
## Step 2: Set up a local Twenty instance
|
||||
## 步骤 2:设置本地 Twenty 实例
|
||||
|
||||
The scaffolder will ask:
|
||||
脚手架工具会询问:
|
||||
|
||||
> **Would you like to set up a local Twenty instance?**
|
||||
> **是否要设置本地 Twenty 实例?**
|
||||
|
||||
* **Type `yes`** (recommended) — This pulls the `twenty-app-dev` Docker image and starts a local Twenty server on port `2020`. Make sure Docker is running before you continue.
|
||||
* **Type `no`** — Choose this if you already have a Twenty server running locally.
|
||||
* **输入 `yes`**(推荐)— 这将拉取 `twenty-app-dev` Docker 镜像,并在端口 `2020` 上启动本地 Twenty 服务器。 继续之前,请确保 Docker 正在运行。
|
||||
* **输入 `no`** — 如果你已经有一个在本地运行的 Twenty 服务器,请选择此项。
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="Should start local instance?" />
|
||||
<img src="/images/docs/developers/extends/apps/start-instance.png" alt="是否启动本地实例?" />
|
||||
</div>
|
||||
|
||||
## Step 3: Sign in to your workspace
|
||||
## 步骤 3:登录你的工作区
|
||||
|
||||
Next, a browser window will open with the Twenty login page. Sign in with the pre-seeded demo account:
|
||||
接下来,将打开一个浏览器窗口,显示 Twenty 登录页面。 使用预置的演示账户登录:
|
||||
|
||||
* **Email:** `tim@apple.dev`
|
||||
* **Password:** `tim@apple.dev`
|
||||
* **邮箱:** `tim@apple.dev`
|
||||
* **密码:** `tim@apple.dev`
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty login screen" />
|
||||
<img src="/images/docs/developers/extends/apps/login.png" alt="Twenty 登录界面" />
|
||||
</div>
|
||||
|
||||
## Step 4: Authorize the app
|
||||
## 步骤 4:授权该应用
|
||||
|
||||
After you sign in, you will see an authorization screen. This lets your app interact with your workspace.
|
||||
登录后,你会看到一个授权界面。 这使你的应用可以与工作区交互。
|
||||
|
||||
Click **Authorize** to continue.
|
||||
点击 **授权** 继续。
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI authorization screen" />
|
||||
<img src="/images/docs/developers/extends/apps/authorize.png" alt="Twenty CLI 授权界面" />
|
||||
</div>
|
||||
|
||||
Once authorized, your terminal will confirm that everything is set up.
|
||||
授权后,你的终端会确认一切已就绪。
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="App scaffolded successfully" />
|
||||
<img src="/images/docs/developers/extends/apps/scaffolded.png" alt="应用脚手架创建成功" />
|
||||
</div>
|
||||
|
||||
## Step 5: Start developing
|
||||
## 步骤 5:开始开发
|
||||
|
||||
Go into your new app folder and start the development server:
|
||||
进入你的新应用文件夹并启动开发服务器:
|
||||
|
||||
```bash filename="Terminal"
|
||||
cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
This watches your source files, rebuilds on every change, and syncs your app to the local Twenty server automatically. You should see a live status panel in your terminal.
|
||||
它会监听你的源文件,每次更改都会重建,并自动将你的应用同步到本地 Twenty 服务器。 你应当在终端中看到一个实时状态面板。
|
||||
|
||||
For more detailed output (build logs, sync requests, error traces), use the `--verbose` flag:
|
||||
如需更详细的输出(构建日志、同步请求、错误跟踪),请使用 `--verbose` 标志:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev --verbose
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests. Use `yarn twenty deploy` to deploy to production servers — see [Publishing Apps](/l/zh/developers/extend/apps/publishing) for details.
|
||||
开发模式仅适用于以开发模式运行的 Twenty 实例(`NODE_ENV=development`)。 生产实例会拒绝开发同步请求。 使用 `yarn twenty deploy` 部署到生产服务器——详见[发布应用](/l/zh/developers/extend/apps/publishing)。
|
||||
</Warning>
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="Dev mode terminal output" />
|
||||
<img src="/images/docs/developers/extends/apps/dev.jpg" alt="开发模式终端输出" />
|
||||
</div>
|
||||
|
||||
## Step 6: See your app in Twenty
|
||||
## 步骤 6:在 Twenty 中查看你的应用
|
||||
|
||||
Open [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer) in your browser. Navigate to **Settings > Apps** and select the **Developer** tab. You should see your app listed under **Your Apps**:
|
||||
在浏览器中打开 [http://localhost:2020/settings/applications#developer](http://localhost:2020/settings/applications#developer)。 前往 **设置 > 应用**,并选择 **开发者** 选项卡。 你应当在 **你的应用** 下看到你的应用:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="Your Apps list showing My twenty app" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-1.png" alt="“你的应用”列表显示 My twenty app" />
|
||||
</div>
|
||||
|
||||
Click on **My twenty app** to open its **application registration**. A registration is a server-level record that describes your app — its name, unique identifier, OAuth credentials, and source (local, npm, or tarball). It lives on the server, not inside any specific workspace. When you install an app into a workspace, Twenty creates a workspace-scoped **application** that points back to this registration. One registration can be installed across multiple workspaces on the same server.
|
||||
点击 **My twenty app** 打开其 **应用注册**。 注册项是一个服务器级记录,用于描述你的应用——其名称、唯一标识符、OAuth 凭据以及来源(本地、npm 或 tarball)。 它位于服务器上,而不在任何特定工作区内。 当你将应用安装到工作区时,Twenty 会创建一个工作区范围的 **应用**,指向该注册项。 同一服务器上的多个工作区可以安装同一个注册项。
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="Application registration details" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-2.png" alt="应用注册详情" />
|
||||
</div>
|
||||
|
||||
Click **View installed app** to see the installed app. The **About** tab shows the current version and management options:
|
||||
点击 **查看已安装的应用** 以查看已安装的应用。 **关于** 选项卡显示当前版本和管理选项:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="Installed app — About tab" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-3.png" alt="已安装的应用 — “关于”选项卡" />
|
||||
</div>
|
||||
|
||||
Switch to the **Content** tab to see everything your app provides — objects, fields, logic functions, and agents:
|
||||
切换到 **内容** 选项卡,以查看你的应用提供的全部内容——对象、字段、逻辑函数和智能体:
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="Installed app — Content tab" />
|
||||
<img src="/images/docs/developers/extends/apps/app-in-ui-4.png" alt="已安装的应用 — “内容”选项卡" />
|
||||
</div>
|
||||
|
||||
You are all set! Edit any file in `src/` and the changes will be picked up automatically.
|
||||
一切就绪! 编辑 `src/` 中的任意文件,更改会被自动检测到。
|
||||
|
||||
Head over to [Building Apps](/l/zh/developers/extend/apps/building) for a detailed guide on creating objects, logic functions, front components, skills, and more.
|
||||
前往[构建应用](/l/zh/developers/extend/apps/building),查看关于创建对象、逻辑函数、前端组件、技能等的详细指南。
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
## 项目结构
|
||||
|
||||
The scaffolder generates the following file structure (shown with `--exhaustive` mode, which includes examples for every entity type):
|
||||
脚手架工具会生成以下文件结构(以 `--exhaustive` 模式展示,其中包含每种实体类型的示例):
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -190,30 +190,30 @@ my-twenty-app/
|
||||
└── example-agent.ts # Example AI agent definition
|
||||
```
|
||||
|
||||
By default (`--minimal`), only the core files are created: `application-config.ts`, `roles/default-role.ts`, `logic-functions/pre-install.ts`, and `logic-functions/post-install.ts`. Use `--exhaustive` to include all the example files shown above.
|
||||
默认情况下(`--minimal`),仅创建核心文件:`application-config.ts`、`roles/default-role.ts`、`logic-functions/pre-install.ts` 和 `logic-functions/post-install.ts`。 使用 `--exhaustive` 可包含上面展示的所有示例文件。
|
||||
|
||||
### Key files
|
||||
### 关键文件
|
||||
|
||||
| File / Folder | 目的 |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `package.json` | Declares your app name, version, and dependencies. Includes a `twenty` script so you can run `yarn twenty help` to see all commands. |
|
||||
| `src/application-config.ts` | **Required.** The main configuration file for your app. |
|
||||
| `src/roles/` | Defines roles that control what your logic functions can access. |
|
||||
| `src/logic-functions/` | Server-side functions triggered by routes, cron schedules, or database events. |
|
||||
| `src/front-components/` | React components that render inside Twenty's UI. |
|
||||
| `src/objects/` | Custom object definitions to extend your data model. |
|
||||
| `src/fields/` | Custom fields added to existing objects. |
|
||||
| `src/views/` | Saved view configurations. |
|
||||
| `src/navigation-menu-items/` | Custom links in the sidebar navigation. |
|
||||
| `src/skills/` | 用于扩展 Twenty 的 AI 代理的技能. |
|
||||
| `src/agents/` | AI agents with custom prompts. |
|
||||
| `src/page-layouts/` | Custom page layouts for record views. |
|
||||
| `src/__tests__/` | Integration tests (setup + example test). |
|
||||
| `public/` | Static assets (images, fonts) served with your app. |
|
||||
| 文件 / 文件夹 | 目的 |
|
||||
| ---------------------------- | ------------------------------------------------------------------ |
|
||||
| `package.json` | 声明应用的名称、版本和依赖。 包含一个 `twenty` 脚本,因此你可以运行 `yarn twenty help` 查看所有命令。 |
|
||||
| `src/application-config.ts` | **必需。** 应用的主配置文件。 |
|
||||
| `src/roles/` | 定义角色,用于控制逻辑函数的访问权限。 |
|
||||
| `src/logic-functions/` | 由路由、cron 调度或数据库事件触发的服务端函数。 |
|
||||
| `src/front-components/` | 在 Twenty 的 UI 中渲染的 React 组件。 |
|
||||
| `src/objects/` | 用于扩展数据模型的自定义对象定义。 |
|
||||
| `src/fields/` | 添加到现有对象的自定义字段。 |
|
||||
| `src/views/` | 已保存的视图配置。 |
|
||||
| `src/navigation-menu-items/` | 侧边栏导航中的自定义链接。 |
|
||||
| `src/skills/` | 用于扩展 Twenty 的 AI 代理的技能. |
|
||||
| `src/agents/` | 具有自定义提示词的 AI 智能体。 |
|
||||
| `src/page-layouts/` | 记录视图的自定义页面布局。 |
|
||||
| `src/__tests__/` | 集成测试(设置 + 示例测试)。 |
|
||||
| `public/` | 随应用一起提供的静态资源(图像、字体)。 |
|
||||
|
||||
## Managing remotes
|
||||
## 管理远程
|
||||
|
||||
A **remote** is a Twenty server that your app connects to. During setup, the scaffolder creates one for you automatically. You can add more remotes or switch between them at any time.
|
||||
“远程”是指你的应用连接到的 Twenty 服务器。 在设置期间,脚手架工具会为你自动创建一个。 你可以随时添加更多远程或在它们之间切换。
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new remote (opens a browser for OAuth login)
|
||||
@@ -232,11 +232,11 @@ yarn twenty remote list
|
||||
yarn twenty remote switch <name>
|
||||
```
|
||||
|
||||
Your credentials are stored in `~/.twenty/config.json`.
|
||||
你的凭据存储在 `~/.twenty/config.json` 中。
|
||||
|
||||
## Local development server (`yarn twenty server`)
|
||||
## 本地开发服务器(`yarn twenty server`)
|
||||
|
||||
The CLI can manage a local Twenty server running in Docker. This is the same server started automatically when you scaffold an app with `create-twenty-app`, but you can also manage it manually.
|
||||
CLI 可以管理在 Docker 中运行的本地 Twenty 服务器。 这与使用 `create-twenty-app` 搭建应用时自动启动的服务器相同,但你也可以手动管理它。
|
||||
|
||||
### 启动服务器
|
||||
|
||||
@@ -244,85 +244,85 @@ The CLI can manage a local Twenty server running in Docker. This is the same ser
|
||||
yarn twenty server start
|
||||
```
|
||||
|
||||
This pulls the `twentycrm/twenty-app-dev:latest` Docker image (if not already present), creates a container named `twenty-app-dev`, and starts it on port **2020**. The CLI waits until the server passes its health check before returning.
|
||||
这将拉取 `twentycrm/twenty-app-dev:latest` Docker 镜像(如果尚未存在),创建名为 `twenty-app-dev` 的容器,并在端口 **2020** 上启动它。 CLI 会等待服务器通过健康检查后再返回。
|
||||
|
||||
Two Docker volumes are created to persist data between restarts:
|
||||
会创建两个 Docker 卷,以在重启之间持久化数据:
|
||||
|
||||
* `twenty-app-dev-data` — PostgreSQL database
|
||||
* `twenty-app-dev-storage` — file storage
|
||||
* `twenty-app-dev-data` — PostgreSQL 数据库
|
||||
* `twenty-app-dev-storage` — 文件存储
|
||||
|
||||
If port 2020 is already in use, you can start on a different port:
|
||||
如果端口 2020 已被占用,你可以在其他端口上启动:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server start --port 3030
|
||||
```
|
||||
|
||||
The CLI automatically configures the container's internal `NODE_PORT` and `SERVER_URL` to match the chosen port, so logic functions, OAuth, and all other internal networking work correctly.
|
||||
CLI 会自动配置容器内部的 `NODE_PORT` 和 `SERVER_URL` 以匹配所选端口,从而使逻辑函数、OAuth 以及所有其他内部网络正常工作。
|
||||
|
||||
Once started, the server is automatically registered as the `local` remote in your CLI config.
|
||||
启动后,该服务器会在你的 CLI 配置中自动注册为 `local` 远程。
|
||||
|
||||
### Checking server status
|
||||
### 检查服务器状态
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server status
|
||||
```
|
||||
|
||||
Displays whether the server is running, its URL, and the default login credentials (`tim@apple.dev` / `tim@apple.dev`).
|
||||
显示服务器是否在运行、其 URL,以及默认登录凭据(`tim@apple.dev` / `tim@apple.dev`)。
|
||||
|
||||
### Viewing server logs
|
||||
### 查看服务器日志
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server logs
|
||||
```
|
||||
|
||||
Streams the container logs. Use `--lines` to control how many recent lines to show:
|
||||
持续输出容器日志。 使用 `--lines` 控制显示的最近日志行数:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server logs --lines 100
|
||||
```
|
||||
|
||||
### Stopping the server
|
||||
### 停止服务器
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server stop
|
||||
```
|
||||
|
||||
Stops the container. Your data is preserved in the Docker volumes — the next `start` picks up where you left off.
|
||||
停止容器。 你的数据会保存在 Docker 卷中——下次 `start` 会从上次中断处继续。
|
||||
|
||||
### Resetting the server
|
||||
### 重置服务器
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty server reset
|
||||
```
|
||||
|
||||
Removes the container **and** deletes both Docker volumes, wiping all data. The next `start` creates a fresh instance.
|
||||
移除容器并删除这两个 Docker 卷,清除所有数据。 下一次 `start` 会创建一个全新实例。
|
||||
|
||||
<Note>
|
||||
The server requires **Docker** to be running. If you see a "Docker not running" error, make sure Docker Desktop (or the Docker daemon) is started.
|
||||
服务器需要 Docker 处于运行状态。 如果看到 "Docker not running" 错误,请确保 Docker Desktop(或 Docker 守护进程)已启动。
|
||||
</Note>
|
||||
|
||||
### Command reference
|
||||
### 命令参考
|
||||
|
||||
| 命令 | 描述 |
|
||||
| -------------------------------------- | ---------------------------------------------- |
|
||||
| `yarn twenty server start` | Start the local server (pulls image if needed) |
|
||||
| `yarn twenty server start --port 3030` | Start on a custom port |
|
||||
| `yarn twenty server stop` | Stop the server (preserves data) |
|
||||
| `yarn twenty server status` | Show server status, URL, and credentials |
|
||||
| `yarn twenty server logs` | Stream server logs |
|
||||
| `yarn twenty server logs --lines 100` | Show the last 100 log lines |
|
||||
| `yarn twenty server reset` | Delete all data and start fresh |
|
||||
| 命令 | 描述 |
|
||||
| -------------------------------------- | --------------- |
|
||||
| `yarn twenty server start` | 启动本地服务器(按需拉取镜像) |
|
||||
| `yarn twenty server start --port 3030` | 在自定义端口启动 |
|
||||
| `yarn twenty server stop` | 停止服务器(保留数据) |
|
||||
| `yarn twenty server status` | 显示服务器状态、URL 和凭据 |
|
||||
| `yarn twenty server logs` | 流式输出服务器日志 |
|
||||
| `yarn twenty server logs --lines 100` | 显示最近 100 行日志 |
|
||||
| `yarn twenty server reset` | 删除所有数据并全新开始 |
|
||||
|
||||
## CI with GitHub Actions
|
||||
## 使用 GitHub Actions 进行 CI
|
||||
|
||||
The scaffolder generates a ready-to-use GitHub Actions workflow at `.github/workflows/ci.yml`. It runs your integration tests automatically on every push to `main` and on pull requests.
|
||||
脚手架工具会在 `.github/workflows/ci.yml` 生成一个开箱即用的 GitHub Actions 工作流。 它会在每次向 `main` 推送以及拉取请求上自动运行你的集成测试。
|
||||
|
||||
The workflow:
|
||||
工作流:
|
||||
|
||||
1. Checks out your code
|
||||
2. Spins up a temporary Twenty server using the `twentyhq/twenty/.github/actions/spawn-twenty-docker-image` action
|
||||
3. Installs dependencies with `yarn install --immutable`
|
||||
4. Runs `yarn test` with `TWENTY_API_URL` and `TWENTY_API_KEY` injected from the action outputs
|
||||
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
|
||||
@@ -369,21 +369,21 @@ jobs:
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.access-token }}
|
||||
```
|
||||
|
||||
You don't need to configure any secrets — the `spawn-twenty-docker-image` action starts an ephemeral Twenty server directly in the runner and outputs the connection details. The `GITHUB_TOKEN` secret is provided automatically by GitHub.
|
||||
你无需配置任何机密——`spawn-twenty-docker-image` 动作会在运行器中直接启动一个临时的 Twenty 服务器,并输出连接详情。 GitHub 会自动提供 `GITHUB_TOKEN` 机密。
|
||||
|
||||
To pin a specific Twenty version instead of `latest`, change the `TWENTY_VERSION` environment variable at the top of the workflow.
|
||||
若要固定为特定的 Twenty 版本而不是 `latest`,请在工作流顶部修改 `TWENTY_VERSION` 环境变量。
|
||||
|
||||
## 手动设置(不使用脚手架)
|
||||
|
||||
If you prefer to set things up yourself instead of using `create-twenty-app`, you can do it in two steps.
|
||||
如果你不想使用 `create-twenty-app`,而是自行完成设置,可以分两步进行。
|
||||
|
||||
**1. Add `twenty-sdk` and `twenty-client-sdk` as dependencies:**
|
||||
\*\*1. 将 `twenty-sdk` 和 `twenty-client-sdk` 添加为依赖项:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add twenty-sdk twenty-client-sdk
|
||||
```
|
||||
|
||||
**2. Add a `twenty` script to your `package.json`:**
|
||||
\*\*2. 在你的 `package.json` 中添加一个 `twenty` 脚本:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
@@ -393,19 +393,19 @@ yarn add twenty-sdk twenty-client-sdk
|
||||
}
|
||||
```
|
||||
|
||||
You can now run `yarn twenty dev`, `yarn twenty help`, and all other commands.
|
||||
现在你可以运行 `yarn twenty dev`、`yarn twenty help` 以及所有其他命令。
|
||||
|
||||
<Note>
|
||||
Do not install `twenty-sdk` globally. Always use it as a local project dependency so that each project can pin its own version.
|
||||
不要全局安装 `twenty-sdk`。 始终将其作为本地项目依赖使用,以便每个项目都能固定其自己的版本。
|
||||
</Note>
|
||||
|
||||
## 故障排除
|
||||
|
||||
If you run into issues:
|
||||
如果遇到问题:
|
||||
|
||||
* Make sure **Docker is running** before starting the scaffolder with a local instance.
|
||||
* Make sure you are using **Node.js 24+** (`node -v` to check).
|
||||
* Make sure **Corepack is enabled** (`corepack enable`) so Yarn 4 is available.
|
||||
* Try deleting `node_modules` and running `yarn install` again if dependencies seem broken.
|
||||
* 在使用本地实例启动脚手架工具之前,请确保**Docker 已在运行**。
|
||||
* 请确保使用 **Node.js 24+**(运行 `node -v` 进行检查)。
|
||||
* 请确保**已启用 Corepack**(`corepack enable`),以便可使用 Yarn 4。
|
||||
* 如果依赖似乎有问题,尝试删除 `node_modules` 并重新运行 `yarn install`。
|
||||
|
||||
Still stuck? Ask for help on the [Twenty Discord](https://discord.com/channels/1130383047699738754/1130386664812982322).
|
||||
仍然遇到问题? 在 [Twenty 的 Discord](https://discord.com/channels/1130383047699738754/1130386664812982322) 上寻求帮助。
|
||||
|
||||
Reference in New Issue
Block a user