i18n - docs translations (#22617)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?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
07a921f8ca
commit
18ca89bcdd
+81
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: 5. An AI agent
|
||||
icon: robot
|
||||
description: 让代理人使用您的工具从聊天室生成文档。
|
||||
---
|
||||
|
||||
因为`生成文档` 暴露于一个 **工具**\*,故AI 代理人可以调用它。
|
||||
让我们添加一个代理人和技能,用户可以说\*"生成一个
|
||||
Jeffery Griffin"\*。
|
||||
|
||||
## 技能
|
||||
|
||||
[skill](/l/zh/developers/extend/apps/logic/skills-and-agents) 是可重复使用的
|
||||
说明 — — 您附加到代理的知识。 我们教的模型如何使用
|
||||
这个工具。
|
||||
|
||||
```ts filename="src/skills/document-drafting.skill.ts"
|
||||
import { defineSkill } from 'twenty-sdk/define';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: DOCUMENT_SKILL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'document-drafting',
|
||||
label: 'Document drafting',
|
||||
icon: 'IconFileText',
|
||||
content: [
|
||||
'To generate a document, call the `generate-document` tool with:',
|
||||
'- `templateId`: the id of the document template to use.',
|
||||
'- `recordId`: the id of the Person or Company the document is for.',
|
||||
'',
|
||||
'If the user names a template or person instead of an id, find the record first,',
|
||||
'then pass its id. Make sure the template target matches the record type.',
|
||||
].join('\n'),
|
||||
});
|
||||
```
|
||||
|
||||
## 代理
|
||||
|
||||
[agent](/l/zh/developers/extend/apps/logic/skills-and-agents) 与
|
||||
模型配对一个提示。 明确设置 "responseFormat" 以避免构建警告。
|
||||
|
||||
```ts filename="src/agents/document-assistant.agent.ts"
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: DOCUMENT_AGENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'document-assistant',
|
||||
label: 'Document Assistant',
|
||||
description: 'Generates documents from your templates and CRM records.',
|
||||
icon: 'IconFileText',
|
||||
responseFormat: { type: 'text' },
|
||||
prompt: [
|
||||
'You are the Document Assistant for a CRM.',
|
||||
'You help users generate personalized documents from reusable templates',
|
||||
'and the data already in their CRM. Use the generate-document tool, and',
|
||||
'always confirm what you created.',
|
||||
].join(' '),
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
代理只能在其角色允许的情况下调用工具。 我们已经在应用的角色中设置了
|
||||
`canAccessAllTools: true` 和 `canBeAssignedToAgents: true`,
|
||||
详见[第 2 章](/l/zh/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access)。
|
||||
</Note>
|
||||
|
||||
## 试试
|
||||
|
||||
打开与 **Document Assistant** 的聊天,并要求它为您的 CRM 中的
|
||||
人起草一份文档。 它找到记录, 调用 \`generate-document', 并报告
|
||||
返回它创建的文档 — 现在出现在你的 **Documents** 视图中。
|
||||
就像命令菜单和工作流路径。
|
||||
|
||||
这是显示逻辑作为工具的回报:**一个函数、多个前门** -
|
||||
命令菜单、HTTP、工作流步骤以及现在的自然语言。
|
||||
|
||||
**在这一步之后:** 应用程序是功能完整和真正有用的。
|
||||
送货时间。
|
||||
|
||||
<Card title="下一步:发布 →" icon="rocket" href="/l/zh/developers/extend/apps/tutorials/document-generator/发布">
|
||||
添加市场元数据和发布。
|
||||
</Card>
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
---
|
||||
title: 4. 构建界面
|
||||
icon: table-columns
|
||||
description: 查看、侧边栏导航、命令和前部件。
|
||||
---
|
||||
|
||||
现在,对象只能通过设置访问。 让这个应用在 UI 中真正“现身”:列表视图、侧边栏条目、一键式 **Generate document** 命令、用于**预览**文档的记录页面 front 组件,以及用于模板的原生富文本 **editor** 选项卡。
|
||||
|
||||
## 视图和导航
|
||||
|
||||
[view](/l/zh/developers/extend/apps/layout/views) 是一个已保存的对象列表。
|
||||
[导航菜单项](/l/zh/developers/extend/apps/layout/navigation-menu-items)
|
||||
将此视图放置在侧边栏中。
|
||||
|
||||
```ts filename="src/views/documents.view.ts"
|
||||
import { defineView, ViewKey } from 'twenty-sdk/define';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'All documents',
|
||||
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconFile',
|
||||
key: ViewKey.INDEX,
|
||||
position: 0,
|
||||
fields: [
|
||||
{ universalIdentifier: DOCUMENTS_VIEW_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: DOCUMENT_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0, isVisible: true, size: 280 },
|
||||
{ universalIdentifier: DOCUMENTS_VIEW_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: DOCUMENT_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 1, isVisible: true, size: 120 },
|
||||
{ universalIdentifier: DOCUMENTS_VIEW_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 2, isVisible: true, size: 200 },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```ts filename="src/navigation-menu-items/documents.navigation-menu-item.ts"
|
||||
import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: DOCUMENTS_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Documents',
|
||||
icon: 'IconFile',
|
||||
color: 'green',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: DOCUMENTS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
为模板添加相同的配对。 两者现在都显示在侧边栏:
|
||||
|
||||
<Frame caption="侧边栏中的文档和模板,并列出生成的文档。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/04-documents-view.png" alt="使用生成文档的文档视图" />
|
||||
</Frame>
|
||||
|
||||
## 前台组件
|
||||
|
||||
[front component](/l/zh/developers/extend/apps/layout/front-components) 是在 Twenty 内部沙盒运行的 React 组件。 我们读取了选中的记录,通过 `CoreApiClient` 和 POSTs 将
|
||||
人模板加载到最后一章的
|
||||
路线。
|
||||
|
||||
```tsx filename="src/front-components/generate-document-form.front-component.tsx"
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { enqueueSnackbar, useSelectedRecordIds } from 'twenty-sdk/front-component';
|
||||
|
||||
const GenerateDocumentForm = () => {
|
||||
const selectedRecordIds = useSelectedRecordIds();
|
||||
const recordId = selectedRecordIds.length === 1 ? selectedRecordIds[0] : null;
|
||||
const [templates, setTemplates] = useState<{ id: string; name: string }[]>([]);
|
||||
const [templateId, setTemplateId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
new CoreApiClient()
|
||||
.query({ documentTemplates: {
|
||||
__args: { filter: { target: { eq: 'PERSON' } }, first: 100 },
|
||||
edges: { node: { id: true, name: true } } } })
|
||||
.then(({ documentTemplates }) => {
|
||||
const list = documentTemplates?.edges?.map((e) => e.node) ?? [];
|
||||
setTemplates(list);
|
||||
if (list[0]) setTemplateId(list[0].id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const generate = async () => {
|
||||
const apiBaseUrl = process.env.TWENTY_API_URL;
|
||||
const token = process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
||||
const res = await fetch(`${apiBaseUrl}/s/documents/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ templateId, recordId }),
|
||||
}).then((r) => r.json());
|
||||
await enqueueSnackbar({
|
||||
message: res.success ? 'Document generated.' : 'Generation failed.',
|
||||
variant: res.success ? 'success' : 'error',
|
||||
});
|
||||
};
|
||||
|
||||
// ...render a <select> of templates and a Generate button
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'generate-document-form',
|
||||
component: GenerateDocumentForm,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
内联 CSS 变量的样式(`var(--t-color-blu)`),不是从
|
||||
`twai`导入的值。 构建过程中的 SDK 模型,所以模块级导入的
|
||||
主题常量将是“未定义的”。 请参阅
|
||||
[完整组件](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx)。
|
||||
</Warning>
|
||||
|
||||
## 打开它的命令
|
||||
|
||||
当选中一个 Person 时,带有 `availabilityType: 'RECORD_SELECTION'` 的 [command menu item](/l/zh/developers/extend/apps/layout/command-menu-items) 会显示出来,并在侧边面板中打开该组件。
|
||||
|
||||
```ts filename="src/command-menu-items/generate-document.command-menu-item.ts"
|
||||
import { defineCommandMenuItem, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: GENERATE_DOCUMENT_COMMAND_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Generate document',
|
||||
availabilityType: 'RECORD_SELECTION',
|
||||
availabilityObjectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
frontComponentUniversalIdentifier:
|
||||
GENERATE_DOCUMENT_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
## 尝试整个流程
|
||||
|
||||
打开 **People**,勾选一个人,然后按下 <kbd>⌘K</kbd> / <kbd>Ctrl K</kbd>。
|
||||
“生成文档”出现,标记为您的应用:
|
||||
|
||||
<Frame caption="命令显示何时选中某人。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/06-command-menu.png" alt="带生成文档的命令菜单" />
|
||||
</Frame>
|
||||
|
||||
运行它 — — 您的组件在侧面板中打开. 选择一个模板,点击
|
||||
**生成**,以及在 **Documents** 中选择一个新的记录土地。
|
||||
|
||||
<Frame caption="前端组件,加载模板并生成点击。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/06b-front-compon.png" alt="生成文档侧面板" />
|
||||
</Frame>
|
||||
|
||||
每个生成的文档都将您的应用记录为其作者:
|
||||
|
||||
<Frame caption="由文档生成器创建,状态已生成。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/05-document-record.png" alt="生成的文档记录" />
|
||||
</Frame>
|
||||
|
||||
## 预览其记录页面上的文档
|
||||
|
||||
前面的组件不仅仅是命令菜单 — — 你可以在
|
||||
录制页面上挂载一个 \*\*选项卡。 让我们在文档记录中添加一个 *Preview* 选项卡,使得
|
||||
Markdown 物体作为一个可打印的打印页面。
|
||||
|
||||
组件从执行上下文读取当前记录ID,加载
|
||||
文档并将其渲染。 Front 组件运行在一个只允许白名单 HTML 标签的**沙盒**中——原始 HTML 注入(`dangerouslySetInnerHTML`)和 `\<style>` 会被阻止——因此我们通过一个小型 [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx) helper,将 Markdown 渲染为带有内联样式的 React 元素。
|
||||
|
||||
```tsx filename="src/front-components/document-viewer.front-component.tsx"
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { useFrontComponentExecutionContext } from 'twenty-sdk/front-component';
|
||||
import { Markdown } from 'src/utils/markdown-to-react';
|
||||
|
||||
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}`;
|
||||
|
||||
// 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.
|
||||
return (
|
||||
<div style={styles.scroll}>
|
||||
<div style={styles.actions}>
|
||||
<a style={styles.actionLink} href={webUrl} target="_blank" rel="noopener noreferrer">
|
||||
Open web page
|
||||
</a>
|
||||
{pdfUrl ? (
|
||||
<a style={styles.actionLink} href={pdfUrl} target="_blank" rel="noopener noreferrer">
|
||||
Download PDF
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={styles.paper}>
|
||||
<div style={styles.body}>
|
||||
<Markdown content={document.content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'document-viewer',
|
||||
component: DocumentViewer,
|
||||
});
|
||||
```
|
||||
|
||||
用[页面布局](/l/zh/developers/extend/apps/layout/page-layouts)挂载它。 `RECORD_PAGE` 布局会在对象的记录视图中添加选项卡;`CANVAS` 选项卡中的 `FRONT_COMPONENT` 小部件则承载该组件:
|
||||
|
||||
```ts filename="src/page-layouts/document-record.page-layout.ts"
|
||||
import { definePageLayout, PageLayoutTabLayoutMode } from 'twenty-sdk/define';
|
||||
|
||||
export default definePageLayout({
|
||||
universalIdentifier: DOCUMENT_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Document record page',
|
||||
type: 'RECORD_PAGE',
|
||||
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
tabs: [{
|
||||
universalIdentifier: DOCUMENT_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Preview',
|
||||
icon: 'IconEye',
|
||||
position: 50,
|
||||
layoutMode: PageLayoutTabLayoutMode.CANVAS,
|
||||
widgets: [{
|
||||
universalIdentifier: DOCUMENT_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Document preview',
|
||||
type: 'FRONT_COMPONENT',
|
||||
configuration: {
|
||||
configurationType: 'FRONT_COMPONENT',
|
||||
frontComponentUniversalIdentifier: DOCUMENT_VIEWER_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
打开任何文档 — **预览** 选项卡使它美丽,并链接到
|
||||
可分享的网页和 PDF :
|
||||
|
||||
<Frame caption="预览选项卡使用内联样式以及快速链接打开文档。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/09-document-viewer.png" alt="记录页面选项卡中的文档查看器前置组件" />
|
||||
</Frame>
|
||||
|
||||
## 使用富文本编辑器编辑模板
|
||||
|
||||
模板根本不需要自定义组件。 因为`body` 是一个
|
||||
`RICH_TEXT` 字段 20个已经为此提供了一个完整的文本编辑器——
|
||||
与标准注释和任务对象相同。 我们只是在
|
||||
模板记录页面上显示。
|
||||
|
||||
在 `EDITOR` 显示模式中添加一个 `FIELD` 小部件的标签,通过 `field MetadataId` 指向`body`
|
||||
字段:
|
||||
|
||||
```ts filename="src/page-layouts/template-record.page-layout.ts"
|
||||
{
|
||||
universalIdentifier: TEMPLATE_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Template',
|
||||
position: 1,
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
widgets: [{
|
||||
universalIdentifier: TEMPLATE_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
title: 'Template',
|
||||
type: 'FIELD',
|
||||
gridPosition: { row: 0, column: 0, rowSpan: 6, columnSpan: 12 },
|
||||
configuration: {
|
||||
configurationType: 'FIELD',
|
||||
fieldMetadataId: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldDisplayMode: 'EDITOR',
|
||||
},
|
||||
}],
|
||||
}
|
||||
```
|
||||
|
||||
一个`RICH_TEXT`字段既存储编辑器块JSON也存储Markdown
|
||||
投影。 生成管道读取Markdown 投影, 所以
|
||||
占位符, PDF, 和可共享的网页都保持正常工作状态 —
|
||||
查看完整的
|
||||
[“模板-记录”。 年龄布局。s\`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts)。
|
||||
现在编辑器在一个合适的富文本编辑器中写入模板:
|
||||
|
||||
<Frame caption="模板选项卡:20个本地的富文本编辑器绑定到实体字段。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/10-template-editor.png" alt="本地富文本编辑器选项卡的模板记录" />
|
||||
</Frame>
|
||||
|
||||
**在这一步之后:** 文档预览,模板是可编辑的
|
||||
在应用中。 接下来,让AI 代理人从聊天室中生成它们。
|
||||
|
||||
<Card title="下一步:一个 AI 代理 →" icon="robot" href="/l/zh/developers/extend/apps/tutorials/document-generator/ai-agent">
|
||||
添加代理和技能来呼叫您的工具。
|
||||
</Card>
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: 1. 数据模型
|
||||
icon: database
|
||||
description: 带有对象、字段和关系的模型文档和模板。
|
||||
---
|
||||
|
||||
我们的应用需要两个自定义对象:**文档模板**(写什么)和
|
||||
**文档**(生成的结果)。 让我们来定义它们。
|
||||
|
||||
用CLI扫描每个实体的文件 — — 它为您生成一个有效的 UUID 和
|
||||
文件夹:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
```
|
||||
|
||||
在下方显示完成的文件。
|
||||
|
||||
<Note>
|
||||
每一个 `*_UNIVERSAL_IDENTIFIER' 常住寿命为
|
||||
`src/constants/universal-identifiers.ts\` 并且在使用时导入。 下面的
|
||||
代码片段省略了这些导入的简洁度 - 将它们保留在您自己的文件中。
|
||||
</Note>
|
||||
|
||||
## 模板对象
|
||||
|
||||
一个模板具有一个 `name`、一个包含 `{{placeholders}}` 的 `body`,以及一个 `target`,用于指明它是为 Person 还是 Company 编写的。 `body` 是一个
|
||||
`RICH_TEXT` 字段,所以二十个字段给它一个完整的文本编辑器。
|
||||
|
||||
```ts filename="src/objects/document-template.object.ts"
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'documentTemplate',
|
||||
namePlural: 'documentTemplates',
|
||||
labelSingular: 'Document template',
|
||||
labelPlural: 'Document templates',
|
||||
icon: 'IconFileText',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{ universalIdentifier: TEMPLATE_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.TEXT, name: 'name', label: 'Name', icon: 'IconAbc' },
|
||||
{ universalIdentifier: TEMPLATE_BODY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RICH_TEXT, name: 'body', label: 'Body', icon: 'IconFileText',
|
||||
description: 'Use {{placeholders}} like {{name.firstName}} or {{jobTitle}}.' },
|
||||
{ universalIdentifier: TEMPLATE_TARGET_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.SELECT, name: 'target', label: 'Target', icon: 'IconTarget',
|
||||
defaultValue: `'PERSON'`,
|
||||
options: [
|
||||
{ id: TEMPLATE_TARGET_OPTION_PERSON_UNIVERSAL_IDENTIFIER,
|
||||
value: 'PERSON', label: 'Person', color: 'blue', position: 0 },
|
||||
{ id: TEMPLATE_TARGET_OPTION_COMPANY_UNIVERSAL_IDENTIFIER,
|
||||
value: 'COMPANY', label: 'Company', color: 'green', position: 1 },
|
||||
] },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`SELECT` 选项 **值** 必须是 `UPPER_CASE` (`PERSON`, 而不是`person`),并且
|
||||
`defaultValue` 用额外引号包裹:`` `PERSON` ``。 "label" 是
|
||||
用户所看到的。
|
||||
</Warning>
|
||||
|
||||
## 文档对象
|
||||
|
||||
生成的文档存储渲染的 `content` 和 `status` 。 以相同的方式定义它,并添加一个 `status` 选择字段,取值为 `DRAFT` / `GENERATED`。 完整文件:
|
||||
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts)。
|
||||
|
||||
## 将它们与关联到关系
|
||||
|
||||
每个文档都应该回到它所产生的模板。 关系是
|
||||
**双向** — — 您在自己的字段文件中定义双方。
|
||||
|
||||
```ts filename="src/fields/document-template-relation.field.ts"
|
||||
import { defineField, FieldType, OnDeleteAction, RelationType } from 'twenty-sdk/define';
|
||||
|
||||
// The "many" side: each document belongs to one template.
|
||||
export default defineField({
|
||||
universalIdentifier: DOCUMENT_TEMPLATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: DOCUMENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'template',
|
||||
label: 'Template',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
DOCUMENT_TEMPLATE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
TEMPLATE_DOCUMENTS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'templateId',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
另一边(`template-documents-relation.field.ts`) 是一个名为`documents`的
|
||||
`RelationType.ONE_TO_MANY` 字段,它指出相反的方向。
|
||||
完整模式请参阅 [Relations](/l/zh/developers/extend/apps/data/relations)。
|
||||
|
||||
## 在 20 中查看
|
||||
|
||||
用 `yarn 20dev` 运行,打开 **设置 -> 数据模型**。 这两个对象都会出现,并带有你的应用标签。
|
||||
|
||||
<Frame caption="两个自定义对象,由文档生成器应用程序所拥有。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/01-data-model.png" alt="显示文档和文档模板的数据模型设置" />
|
||||
</Frame>
|
||||
|
||||
创建一个模板来测试 — 名称是 *销售建议*, 设置 **Target** 为
|
||||
*Person*, 并粘贴一个几个占位符的机构:
|
||||
|
||||
```text
|
||||
Dear {{name.firstName}} {{name.lastName}},
|
||||
|
||||
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
|
||||
|
||||
Best,
|
||||
The Team
|
||||
```
|
||||
|
||||
<Frame caption="模板记录。 该机构保留其占位符直到文档生成为止。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/03-template-record.png" alt="带占位符主体的销售建议模板记录" />
|
||||
</Frame>
|
||||
|
||||
**在这个步骤之后:** 你有 `documentTemplate` 和 `document` 对象,用
|
||||
的关系链接和一个模板生成它们。 接下来,填充它的逻辑。
|
||||
|
||||
<Card title="下一步:生成文档 →" icon="bolt" href="/l/zh/developers/extend/apps/tutorials/document-generator/generating-document">
|
||||
写下填充模板的逻辑函数。
|
||||
</Card>
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: 2. 正在生成文档
|
||||
icon: bolt
|
||||
description: 一个逻辑函数作为一个 AI 工具和工作流动作暴露。
|
||||
---
|
||||
|
||||
现在核心:一个[逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions)
|
||||
加载模板和记录,填充占位符,并保存一个新的
|
||||
文档。
|
||||
|
||||
我们将把业务逻辑写成一个**处理器**,然后透露它通过
|
||||
几个触发器。 本章将其中的两个线路连接起来——一个 **AI 工具** 和
|
||||
**Workflow 操作** 。
|
||||
|
||||
## 渲染帮助器
|
||||
|
||||
将纯逻辑保留在它自己的文件中,这样便于拆除测试。 这会将一个记录
|
||||
展平成 `{{dot.path}}` 标记并对其进行替换。
|
||||
|
||||
```ts filename="src/logic-functions/utils/render-template.ts"
|
||||
const PLACEHOLDER_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
|
||||
|
||||
export const renderTemplate = (body: string, values: Record<string, string>) => {
|
||||
const missingTokens = new Set<string>();
|
||||
const content = body.replace(PLACEHOLDER_PATTERN, (_m, token: string) => {
|
||||
const value = values[token];
|
||||
if (value === undefined || value === '') { missingTokens.add(token); return ''; }
|
||||
return value;
|
||||
});
|
||||
return { content, missingTokens: [...missingTokens] };
|
||||
};
|
||||
```
|
||||
|
||||
<Tip>
|
||||
由于此文件没有副作用,你可以使用快速单元测试对其进行覆盖
|
||||
(`yarn test:unit`)。 见 [Testing](/l/zh/developers/extend/apps/operations/testing)。
|
||||
</Tip>
|
||||
|
||||
## 处理程序
|
||||
|
||||
处理程序使用生成的 [`CoreApiClient`](/l/zh/developers/extend/apps/logic/logic-functions)
|
||||
读写CRM 数据。 它加载模板,加载目标记录,填充
|
||||
物体,并创建一个“文档”。
|
||||
|
||||
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { loadRecordValues } from 'src/logic-functions/utils/load-record-values';
|
||||
import { renderTemplate } from 'src/logic-functions/utils/render-template';
|
||||
|
||||
export const generateDocumentHandler = async (
|
||||
input: { templateId: string; recordId: string },
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Use a filtered list query, not the singular lookup: the singular query
|
||||
// throws when nothing matches, which would become a 500 instead of a 404.
|
||||
const { documentTemplates } = await client.query({
|
||||
documentTemplates: {
|
||||
__args: { filter: { id: { eq: input.templateId } }, first: 1 },
|
||||
edges: { node: { id: true, name: true, body: true, target: true } },
|
||||
},
|
||||
});
|
||||
const documentTemplate = documentTemplates?.edges?.[0]?.node;
|
||||
if (!documentTemplate?.id) return { success: false, status: 404, message: 'Template not found.' };
|
||||
|
||||
const record = await loadRecordValues(client, documentTemplate.target, input.recordId);
|
||||
if (!record.found) return { success: false, status: 404, message: 'Record not found.' };
|
||||
|
||||
const { content, missingTokens } = renderTemplate(documentTemplate.body ?? '', record.values);
|
||||
|
||||
const { createDocument } = await client.mutation({
|
||||
createDocument: {
|
||||
__args: { data: {
|
||||
name: `${documentTemplate.name} — ${record.displayName}`,
|
||||
content, status: 'GENERATED', templateId: documentTemplate.id,
|
||||
} },
|
||||
id: true, name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, documentId: createDocument.id, content, missingTokens };
|
||||
};
|
||||
```
|
||||
|
||||
`loadRecordValues` 会针对 Person 与 Company 运行不同的查询,并将结果展平 — 参见
|
||||
[`load-record-values.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/load-record-values.ts)。
|
||||
|
||||
## 显示为一个工具和工作流操作
|
||||
|
||||
单个的 `defineeLogicFunction` 可以带几个触发器。 在这里,`toolTriggerSettings`
|
||||
让它可以被AI 代理人调用,`workflowActionTriggerSettings` 将它变成可视工作流生成器中的
|
||||
步骤。 两者都用JSON方案描述他们的输入情况。
|
||||
|
||||
```ts filename="src/logic-functions/generate-document.ts"
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-sdk/logic-function';
|
||||
import { GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
|
||||
import { generateDocumentInputSchema } from 'src/logic-functions/schemas/generate-document-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: GENERATE_DOCUMENT_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'generate-document',
|
||||
description: 'Generate a document from a template and a CRM record.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: generateDocumentInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Generate Document',
|
||||
icon: 'IconFileText',
|
||||
inputSchema: jsonSchemaToInputSchema(generateDocumentInputSchema),
|
||||
outputSchema: [{ type: 'object', properties: {
|
||||
success: { type: 'boolean' }, documentId: { type: 'string' },
|
||||
} }],
|
||||
},
|
||||
handler: generateDocumentHandler,
|
||||
});
|
||||
```
|
||||
|
||||
输入schema是一个普通的 JSON schema 描述了 `templateId` 和 `recordId` -
|
||||
查看 [`generate-document-input.schema.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/schemas/generate-document-input.schema.ts)。
|
||||
|
||||
## 授予访问权限
|
||||
|
||||
逻辑函数作为应用程序的角色运行。 它需要阅读模板和记录
|
||||
并创建文档,以便允许在 "src/roles/default-role.ts" 中:
|
||||
|
||||
```ts
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Document Generator default role',
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canAccessAllTools: true,
|
||||
canBeAssignedToAgents: true,
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
|
||||
});
|
||||
```
|
||||
|
||||
`UPLOAD_FILE` 让函数在下一节上传生成的 PDF。
|
||||
请参阅 [Roles](/l/zh/developers/extend/apps/config/roles) 以获取更精细的权限。
|
||||
|
||||
## 附加一个真实的 PDF 文件
|
||||
|
||||
渲染文本字段是有用的,但用户需要一个真正的文档。 让我们生成一个
|
||||
**PDF** 并将其作为可下载的文件存储在记录上。
|
||||
|
||||
首先,给`文档`对象一个`FILES`字段来持有PDF。 应用将
|
||||
上传到他们**拥有** 的文件字段,所以此字段是上传的路线:
|
||||
|
||||
```ts filename="src/objects/document.object.ts"
|
||||
{
|
||||
universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.FILES,
|
||||
name: 'file',
|
||||
label: 'File',
|
||||
icon: 'IconFileTypePdf',
|
||||
universalSettings: { maxNumberOfValues: 1 },
|
||||
}
|
||||
```
|
||||
|
||||
现在渲染那个PDF。 一个应用是一个真正的节点项目,所以您可以添加任何您需要的npm
|
||||
包,并且像其他任何地方一样导入它。 我们使用 **[pdf-lib](https://pdf-lib.js.org/)**
|
||||
绘制PDF 和 **[marked](https://marked.js.org/)** 解析Markdown
|
||||
正文--CLI 将它们安装到函数中供您使用的运行时间:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add pdf-lib marked
|
||||
```
|
||||
|
||||
完整的助手是
|
||||
[`generate-document-pdf.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/logic-functions/utils/generate-document-pdf.ts)。
|
||||
它将Markdown解析为代币,标记为“标记”。 \`运行,然后使用
|
||||
pdf-lib:真实标题,**bold**/*italic* 运行,子弹和编号列表
|
||||
blockquotes and rules — — 一个经过筛选、多页的 A4 渲染模板
|
||||
本身,而不是一个文本墙。
|
||||
|
||||
<Frame caption="生成的 PDF:真实的排版和Markdown 格式化,呈现模板正文。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/07b-generated-pdf.png" alt="一个打造的可营销的 PDF" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
pdf-lib的内置字体使用WinAnsi编码,所以西欧语音会将
|
||||
渲染到盒子之外; 助手地图的智能引用和破折号并掉落它
|
||||
无法编码。 渲染非拉丁脚本(中文、 阿拉伯语、 西里尔语) 将意味着
|
||||
嵌入一个 Unicode 字体。
|
||||
</Note>
|
||||
|
||||
然后上传它并在记录中存储参考。 `uploadFile` 路由字节
|
||||
到你的应用拥有的文件字段。返回的 `id` 是你保存的:
|
||||
|
||||
```ts filename="src/logic-functions/handlers/generate-document-handler.ts"
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { generateDocumentPdf } from 'src/logic-functions/utils/generate-document-pdf';
|
||||
|
||||
const documentName = `${documentTemplate.name} — ${record.displayName}`;
|
||||
const bytes = await generateDocumentPdf(documentName, content);
|
||||
const fileName = 'proposal.pdf';
|
||||
|
||||
const uploaded = await new MetadataApiClient().uploadFile(
|
||||
Buffer.from(bytes),
|
||||
fileName,
|
||||
'application/pdf',
|
||||
DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
await client.mutation({
|
||||
updateDocument: {
|
||||
__args: {
|
||||
id: documentId,
|
||||
data: { file: [{ fileId: uploaded.id, label: fileName }] },
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
生成的文档现在带有可下载的 PDF:
|
||||
|
||||
<Frame caption="已生成的 PDF 存储在文档文件字段中。">
|
||||
<img src="/images/docs/developers/extends/apps/documents/document-generator/08-document-with-pdf.png" alt="生成一个 PDF 文件的文档记录" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
`uploadFile` 仅针对**app-owned** 文件字段。(所以上传文件总需要一个拥有字段的
|
||||
应用,加上`UPLOAD_FILE` 角色标志)。 这就是为什么PDF
|
||||
会降落在记录自己的“file”字段上——相同的样式
|
||||
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
|
||||
用于录制的原因。
|
||||
</Note>
|
||||
|
||||
**在这一步之后:** 每个生成的文档都有一个真实的、可下载的 PDF。 但
|
||||
没有任何东西能够\*调用UI 的生成器 — — 因为我们需要一个 HTTP 路由。
|
||||
|
||||
<Card title="下一步:HTTP路由 →" icon="全局模式" href="/l/zh/developers/extend/apps/tutorials/document-generator/http-route">
|
||||
通过 HTTP 提供函数并将文档渲染为 web 页面。
|
||||
</Card>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: 3. HTTP 路由
|
||||
icon: globe
|
||||
description: 通过 HTTP 触发函数并将文档渲染为 web 页面。
|
||||
---
|
||||
|
||||
相同的处理程序也可以回答 HTTP 请求。 我们将添加两个路由:
|
||||
|
||||
* a **POST** 让UI 调用来生成文档的端点,和
|
||||
* 一个公开的 **GET** 端点,将文档作为可打印的网页。
|
||||
|
||||
两者都使用 `httpRouteTriggerSettings` 。 App rough are served under `/s` under your
|
||||
20 server (e.g. `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## POST 路由 — 按需生成
|
||||
|
||||
这会重用\`generateDocuments Handler',所以没有重复的逻辑——只是一个能读取请求正文的薄
|
||||
适配器。
|
||||
|
||||
```ts filename="src/logic-functions/generate-document-route.ts"
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
import { generateDocumentHandler } from 'src/logic-functions/handlers/generate-document-handler';
|
||||
|
||||
const handler = async (event: RoutePayload): Promise<Response> => {
|
||||
const body = event.body as Record<string, unknown> | null;
|
||||
|
||||
const result = await generateDocumentHandler({
|
||||
templateId: (body?.templateId as string) ?? '',
|
||||
recordId: (body?.recordId as string) ?? '',
|
||||
});
|
||||
|
||||
// Map the handler's failure reason onto a real HTTP status (400/404/500)
|
||||
// instead of always returning 200.
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: result.success ? 200 : (result.status ?? 400),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: GENERATE_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'generate-document-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/documents/generate',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
共享处理程序在失败时会返回建议的 `status`,因此路由可以使用适当的 `4xx`/`5xx` 状态码进行响应。 `isauth:true`是指调用者
|
||||
必须提供一个有效的令牌——下一章的前面组件自动通过
|
||||
用户的访问令牌。
|
||||
|
||||
## GET 路由 — 渲染为网页
|
||||
|
||||
若要返回HTML而不是JSON,将物体用
|
||||
`Content-Type`标头包装`Response`。 此路由是公开的(`isauth:必填:false`),所以
|
||||
生成的文档可以作为链接共享。
|
||||
|
||||
```ts filename="src/logic-functions/view-document.ts"
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { documentHtmlPage } from 'src/utils/render-document';
|
||||
|
||||
const htmlResponse = (html: string, status = 200): Response =>
|
||||
new Response(html, { status, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||
|
||||
const handler = async (event: RoutePayload): Promise<Response> => {
|
||||
const documentId = event.queryStringParameters?.id;
|
||||
|
||||
if (!documentId) {
|
||||
return htmlResponse(documentHtmlPage('Missing document id', 'Provide ?id=<documentId>.'), 400);
|
||||
}
|
||||
|
||||
// Filtered list query so an unknown id renders a clean 404 page instead of throwing.
|
||||
const { documents } = await new CoreApiClient().query({
|
||||
documents: {
|
||||
__args: { filter: { id: { eq: documentId } }, first: 1 },
|
||||
edges: { node: { id: true, name: true, content: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const document = documents?.edges?.[0]?.node;
|
||||
if (!document?.id) {
|
||||
return htmlResponse(documentHtmlPage('Document not found', `No document with id ${documentId}.`), 404);
|
||||
}
|
||||
|
||||
return htmlResponse(documentHtmlPage(document.name ?? 'Document', document.content ?? ''));
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: VIEW_DOCUMENT_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'view-document',
|
||||
timeoutSeconds: 15,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/documents/view',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`documentHtmlPage`将Markdown体变成HTML(带 [marked](https://marked.js.org/),
|
||||
净化) 只显示模板
|
||||
内容的可打印页面——与 PDF 和应用程序内预览相同。
|
||||
[参见辅助函数](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts)。
|
||||
|
||||
## 试试
|
||||
|
||||
在您的工作区内有一个模板和一个人, 调用路由(从
|
||||
**设置 -> API 和 Webhooks**获取一个令牌):
|
||||
|
||||
```bash filename="Terminal"
|
||||
curl -X POST http://localhost:2020/s/documents/generate \
|
||||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"templateId":"<templateId>","recordId":"<personId>"}'
|
||||
# → {"success":true,"documentId":"...","content":"Dear Jeffery Griffin, ..."}
|
||||
```
|
||||
|
||||
在您的浏览器中打开返回的文档:
|
||||
|
||||
```
|
||||
http://localhost:2020/s/documents/view?id=<documentId>
|
||||
```
|
||||
|
||||
<Frame caption="公开的 GET 路由使文档成为一个可打印的页面。">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="呈现的文档页面" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
您也可以在测试
|
||||
`yarn 20dev:functions:logs`时串流函数日志,或直接通过
|
||||
`yarn 20dev:function:exec` 直接调用。
|
||||
</Tip>
|
||||
|
||||
**在这一步之后:** 应用程序可以通过 HTTP 生成文档并以
|
||||
网页服务。 现在让我们让它在没有“curl”的情况下可以使用。
|
||||
|
||||
<Card title="下一步:构建界面→" icon="table-columns" href="/l/zh/developers/extend/apps/tutorials/document-generator/building-the-ui">
|
||||
查看、导航、命令和前端组件。
|
||||
</Card>
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 教程:文档生成器
|
||||
icon: wand-magic-sparkles
|
||||
description: 构建一个真实的 Twenty 应用,从你的 CRM 数据生成个性化文档。
|
||||
---
|
||||
|
||||
在本教程中,你将构建 **Document Generator** —— 一个应用,它将可复用的模板转换为使用你现有 CRM 数据生成的个性化文档。
|
||||
|
||||
使用 `{{placeholders}}` 一次性编写模板,然后即可为任意 Person 或 Company 一键生成已填充的文档——可从命令菜单、AI 代理或工作流中发起。
|
||||
|
||||
<Frame caption="为特定人员生成的单个模板,以可打印页面的形式打开。">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="生成的销售提案文档" />
|
||||
</Frame>
|
||||
|
||||
## 您将会学到什么
|
||||
|
||||
每一章都会增加一项功能。 在本教程结束时,您将接触到 SDK 的大部分内容。
|
||||
|
||||
| 章节 | 功能 | 参考 |
|
||||
| ------------------------------------------------------------------------------------ | -------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [1. 数据模型](/l/zh/developers/extend/apps/tutorials/document-generator/data-model) | 对象、字段和一个关系 | [数据](/l/zh/developers/extend/apps/data/overview) |
|
||||
| [2. 生成文档](/l/zh/developers/extend/apps/tutorials/document-generator/generating-documents) | 一个逻辑函数(AI 工具 + 工作流操作),用于填充 Markdown 模板并附加一份精美的 PDF | [逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions) |
|
||||
| [3. HTTP 路由](/l/zh/developers/extend/apps/tutorials/document-generator/http-routes) | 从路由提供 JSON 和可分享的 HTML 页面 | [逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions) |
|
||||
| [4. 构建 UI](/l/zh/developers/extend/apps/tutorials/document-generator/building-the-ui) | 视图、导航、命令菜单,以及用于预览文档和编辑模板的前端组件 | [布局](/l/zh/developers/extend/apps/layout/overview) |
|
||||
| [5. 一个 AI 智能体](/l/zh/developers/extend/apps/tutorials/document-generator/ai-agent) | 智能体 + 技能 | [技能与智能体](/l/zh/developers/extend/apps/logic/skills-and-agents) |
|
||||
| [6. 发布](/l/zh/developers/extend/apps/tutorials/document-generator/publishing) | 把它发布到应用市场 | [发布](/l/zh/developers/extend/apps/operations/publishing) |
|
||||
|
||||
## 先决条件
|
||||
|
||||
您应该已经完成了[快速开始](/l/zh/developers/extend/apps/getting-started/quick-start):
|
||||
一个在端口 `2020` 上运行的本地 Twenty 服务器,以及已通过身份验证并连接到它的 CLI。
|
||||
|
||||
如果还没有,现在就创建并启动一个:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest document-generator
|
||||
cd document-generator
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
更想直接阅读完成版代码? 完整应用位于
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator)。
|
||||
下面的每个代码片段都来自该应用。
|
||||
</Note>
|
||||
|
||||
## 应用如何协同运作
|
||||
|
||||
<Frame>
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="带有占位符的模板会被生成为一份带 PDF 的精美文档,可以通过命令菜单、AI 智能体、工作流或可分享链接来触发" />
|
||||
</Frame>
|
||||
|
||||
您只需在富文本编辑器中编写一次**模板**,其中包含 `{{placeholders}}`。 选择一个
|
||||
模板和一条 CRM 记录,就会填充占位符,并存储一份精美的
|
||||
**文档**(带有一个 PDF 文件)。 其他所有内容——命令菜单、AI 智能体、工作流步骤、可分享链接——都只是触发同一个生成器的不同方式。
|
||||
|
||||
## 让这个循环持续运行
|
||||
|
||||
在整个教程期间,在一个终端中保持运行 `yarn twenty dev`。 每当您在 `src/` 下添加或编辑文件时,它都会在几秒钟内重新同步到服务器,这样您就可以在构建的同时,在 UI 中看到每项功能逐步出现。
|
||||
|
||||
<Card title="开始构建 →" icon="database" href="/l/zh/developers/extend/apps/tutorials/document-generator/data-model">
|
||||
第 1 章:为文档和模板建模。
|
||||
</Card>
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: 6. 发布
|
||||
icon: rocket
|
||||
description: 添加市场元数据并发布您的应用。
|
||||
---
|
||||
|
||||
您的应用正常工作。 最后一步是为市场描述并发布。
|
||||
|
||||
## 添加市场元数据
|
||||
|
||||
[应用程序配置](/l/zh/developers/extend/apps/config/application) 带有显示在市场中的
|
||||
身份:作者、类别、标志和支持
|
||||
链接。 在 `public/` 中放置一个徽标,然后使用 `logoUrl` 引用。
|
||||
|
||||
```ts filename="src/application-config.ts"
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Document Generator',
|
||||
description:
|
||||
'Create reusable document templates and generate personalized documents from your CRM records.',
|
||||
logoUrl: 'public/document-generator.svg',
|
||||
author: 'Twenty',
|
||||
category: 'Productivity',
|
||||
websiteUrl: 'https://docs.twenty.com/l/zh/developers/extend/apps',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: 'contact@twenty.com',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
默认角色在其自身文件中使用 `defineApplicationRole()` 声明——你不再在这里传递 `defaultRoleUniversalIdentifier`。
|
||||
</Tip>
|
||||
|
||||
也将 `twentapp` 关键字添加到 `package.json` 中,所以应用程序是可以发现的:
|
||||
|
||||
```json filename="package.json"
|
||||
{ "keywords": ["twenty-app"] }
|
||||
```
|
||||
|
||||
## 添加相册截图
|
||||
|
||||
市场列表用屏幕截图销售自己。 在
|
||||
`public/gallery/` 中拖放几个PNGs,然后使用 `screshots` - 它们在列表页面上渲染成一个相册
|
||||
。
|
||||
|
||||
```ts filename="src/application-config.ts"
|
||||
export default defineApplication({
|
||||
// ...identity from above
|
||||
screenshots: [
|
||||
'public/gallery/01-generated-document.png',
|
||||
'public/gallery/02-command-menu.png',
|
||||
'public/gallery/03-template-editor.png',
|
||||
'public/gallery/04-documents.png',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
领取付款: 让第一个屏幕截图完成的结果(生成的
|
||||
文档),然后显示它是如何触发和编写的。 使用简洁的高分辨率
|
||||
抓取 — — 他们是用户看到的第一件事。
|
||||
</Tip>
|
||||
|
||||
给`README.md`同样的处理方法——它是npm 和 GitHub 的首页。
|
||||
打开时使用值建议和屏幕截图,列出标题功能,
|
||||
然后保留折叠下方的构建详细信息。
|
||||
|
||||
## 在您发货前检查
|
||||
|
||||
运行相同的 CI 门:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
干线运行正是在不应用它的情况下打印服务器上会改变的内容——
|
||||
是一个很好的最后智能检查。 见
|
||||
[Testing](/l/zh/developers/extend/apps/operations/testing) 和
|
||||
[同步和恢复](/l/zh/developers/extend/apps/operations/sync-and-recovery)。
|
||||
|
||||
## 发布
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Public app → npm (default)
|
||||
yarn twenty app:publish
|
||||
|
||||
# Or deploy privately to a specific server's registry
|
||||
yarn twenty app:publish --private -r <remote>
|
||||
```
|
||||
|
||||
`app:publish` 构建并默认向npm 发布;`--private`上传一个
|
||||
tarball到20个服务器的私人注册表。 要在一个实例的市场上显示一个已发布的应用
|
||||
,触发一个目录同步:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync -r <remote>
|
||||
```
|
||||
|
||||
详细信息和发布检查列表:
|
||||
[Publishing](/l/zh/developers/extend/apps/operations/publishing)。
|
||||
|
||||
## 您构建了一个应用 :party_popper:
|
||||
|
||||
在六章中,你使用了大部分SDK表面:
|
||||
|
||||
* **对象、字段和关系** 以模拟数据
|
||||
* 一个**逻辑函数** 显示为 **AI 工具**,一个 **Workflow 动作** 和 **HTTP 路由**
|
||||
* 界面**查看、导航、命令和前面组件**
|
||||
* 用于生成自然语言的 **agent + 技能**
|
||||
* **市场元数据** 和发布流
|
||||
|
||||
已完成的应用位于
|
||||
[\`软件包/二十个应用/示例/文件生成器'](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator)。
|
||||
|
||||
## 下一步
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="数据参考" icon="database" href="/l/zh/developers/extend/apps/data/overview">
|
||||
每个字段类型、关系和索引选项。
|
||||
</Card>
|
||||
<Card title="逻辑引用" icon="bolt" href="/l/zh/developers/extend/apps/logic/overview">
|
||||
Cron 和数据库事件触发了密钥价值存储,OAuth 连接。
|
||||
</Card>
|
||||
<Card title="布局引用" icon="table-columns" href="/l/zh/developers/extend/apps/layout/overview">
|
||||
页面布局、仪表板小部件和更多用户界面。
|
||||
</Card>
|
||||
<Card title="操作" icon="rocket" href="/l/zh/developers/extend/apps/operations/overview">
|
||||
CLI, test, remotes, and CI.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user