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: Lassen Sie einen Agenten Dokumente mit Hilfe Ihres Tools aus einem Chat generieren.
|
||||
---
|
||||
|
||||
Weil `generate-document` als **Tool** exponiert wird, kann ein KI-Agent es aufrufen.
|
||||
Fügen wir einen Agenten und eine Fertigkeit hinzu, damit Benutzer einfach *"einen Vorschlag für
|
||||
Jeffery Griffin"* erstellen können.
|
||||
|
||||
## Die Fähigkeit
|
||||
|
||||
Eine [skill](/l/de/developers/extend/apps/logic/skills-and-agents) ist wiederverwendbare
|
||||
Anleitung — Wissen, das Sie Agenten anhängen. Unser lehrt das Modell, wie man das Werkzeug
|
||||
benutzt.
|
||||
|
||||
```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'),
|
||||
});
|
||||
```
|
||||
|
||||
## Der Agent
|
||||
|
||||
Ein [agent](/l/de/developers/extend/apps/logic/skills-and-agents) paßt einen Prompt mit einem
|
||||
Modell. Setze `responseFormat` explizit, um eine Build-Warnung zu vermeiden.
|
||||
|
||||
```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>
|
||||
Der Agent kann das Werkzeug nur aufrufen, wenn es seine Rolle erlaubt. Wir haben bereits
|
||||
`canAccessAllTools: true` und `canBeAssignedToAgents: true` über die Rolle der App in
|
||||
[Chapter 2](/l/de/developers/extend/apps/tutorials/document-generator/generating-documents#grant-it-access).
|
||||
</Note>
|
||||
|
||||
## Testen
|
||||
|
||||
Öffne einen Chat mit dem **Dokumenten-Assistent** und ersuche ihn, ein Dokument für eine
|
||||
-Person in deinem CRM zu erstellen. Es findet den Datensatz, ruft `generate-document` auf und meldet das erstellte Dokument
|
||||
zurück — das nun in der Ansicht **Dokumente** erscheint
|
||||
gleicht dem Kommandomenü und dem Workflow-Pfad.
|
||||
|
||||
Das ist die Auszahlung der Logik als Werkzeug: **eine Funktion, viele Eingangstüren** —
|
||||
Befehlsmenü, HTTP, Workflow-Schritt und jetzt natürliche Sprache.
|
||||
|
||||
**Nach diesem Schritt:** ist die App komplett und wirklich nützlich. Zeit bis
|
||||
es verschickt wird.
|
||||
|
||||
<Card title="Weiter: Publizieren →" icon="rocket" href="/l/de/developers/extend/apps/tutorials/document-generator/publishing">
|
||||
Marktplatz-Metadaten hinzufügen und veröffentlichen.
|
||||
</Card>
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
---
|
||||
title: 4. Erstelle die UI
|
||||
icon: table-columns
|
||||
description: Views, Sidebar Navigation, ein Befehl und Frontkomponenten.
|
||||
---
|
||||
|
||||
Im Moment sind die Objekte nur über Einstellungen erreichbar. Geben wir der App eine
|
||||
echte Präsenz in der Benutzeroberfläche: Listenansichten, Seitenleisteneinträge, ein Ein-Klick-
|
||||
**Dokumenten** Befehl generieren, eine Front-Komponente der Eintragsseite für **Vorschau** eines
|
||||
Dokuments und einen nativen Reiter für den Volltext **Editor** für Vorlagen.
|
||||
|
||||
## Ansichten und Navigation
|
||||
|
||||
Eine [view](/l/de/developers/extend/apps/layout/views) ist eine gespeicherte Liste eines bestimmten Objekts.
|
||||
Ein [Navigationsmenüeintrag](/l/de/developers/extend/apps/layout/navigation-menu-items)
|
||||
bringt diese Ansicht in die Sidebar.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Fügen Sie das gleiche Paar für Vorlagen hinzu. Beide zeigen nun in der Seitenleiste:
|
||||
|
||||
<Frame caption="Dokumente und Vorlagen in der Seitenleiste mit dem generierten Dokument.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/04-documents-view.png" alt="Dokumentenansicht mit einem generierten Dokument" />
|
||||
</Frame>
|
||||
|
||||
## Eine Frontkomponente
|
||||
|
||||
Eine [Frontkomponent](/l/de/developers/extend/apps/layout/front-components) ist eine Reaktions-
|
||||
Komponente mit Sandkasten in Twenty. Wir liest den ausgewählten Datensatz ein, lädt die
|
||||
Personen-Vorlagen über `CoreApiClient` und POSTs aus dem letzten
|
||||
Kapitel.
|
||||
|
||||
```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>
|
||||
Stil mit Inline-CSS-Variablen (`var(--t-color-blue)`), nicht aus
|
||||
`twenty-ui` importiert. Das SDK verspottet das Paket während des Builds, so dass Modul-Level-Importe von
|
||||
Theme-Konstanten `undefiniert` wären. Siehe
|
||||
[vollständige Komponente](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/front-components/generate-document-form.front-component.tsx).
|
||||
</Warning>
|
||||
|
||||
## Ein Befehl um es zu öffnen
|
||||
|
||||
Ein [Commandmenu item](/l/de/developers/extend/apps/layout/command-menu-items) mit
|
||||
`availabilityType: 'RECORD_SELECTION'` wird angezeigt, wenn eine Person ausgewählt ist, und
|
||||
öffnet die Komponente in der Seitenleiste.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
## Den gesamten Fluss testen
|
||||
|
||||
Öffne **People**, markiere eine Person und drücke <kbd>⌘K</kbd> / <kbd>Strg K</kbd>.
|
||||
"Dokument erstellen" erscheint, mit Ihrer App markiert:
|
||||
|
||||
<Frame caption="Der Befehl wird angezeigt, wenn eine Person ausgewählt ist.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/06-command-menu.png" alt="Befehlsmenü mit Dokument generieren" />
|
||||
</Frame>
|
||||
|
||||
Führen Sie es aus — Ihre Komponente öffnet sich in der Seitenleiste. Wähle eine Vorlage, klicke auf
|
||||
**Generieren**, und ein neues Datensatzland in **Dokumenten**.
|
||||
|
||||
<Frame caption="Die Frontkomponente, das Laden von Vorlagen und das Generieren auf Klick.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/06b-front-component.png" alt="Dokumentseite erstellen" />
|
||||
</Frame>
|
||||
|
||||
Jedes generierte Dokument speichert Ihre App als Autor auf:
|
||||
|
||||
<Frame caption="Erstellt vom Dokumentengenerator, Status generiert.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/05-document-record.png" alt="Ein generierter Dokumentensatz" />
|
||||
</Frame>
|
||||
|
||||
## Vorschau eines Dokuments auf seiner Aufzeichnungsseite
|
||||
|
||||
Eine Frontkomponente ist nicht nur für Kommandomenüs – Sie können sie als **Tab auf einer
|
||||
Aufnahmeseite einhängen**. Fügen wir dem Dokument-Datensatz einen Tab *Vorschau* hinzu, der den
|
||||
Markdown-Text als polierte, druckbare Seite darstellt.
|
||||
|
||||
Die Komponente liest die aktuelle Datensatz-ID aus ihrem Ausführungskontext, lädt das
|
||||
-Dokument und gibt sie aus. Frontkomponenten laufen in einer **Sandbox**, die nur eine
|
||||
Whitelist von HTML-Tags erlaubt — Roh-HTML-Einspritzung (`dangerouslySetInnerHTML`) und
|
||||
`\<style>` werden blockiert — so dass wir Markdown als React-Elemente mit Inline-
|
||||
Styles über einen kleinen [`Markdown`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/markdown-to-react.tsx)
|
||||
Helfer machen.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Mounten Sie es mit einem [page layout](/l/de/developers/extend/apps/layout/page-layouts). Ein
|
||||
`RECORD_PAGE` Layout fügt Tabs zur Datensatzansicht eines Objekts hinzu; ein `FRONT_COMPONENT`
|
||||
Widget in einem `CANVAS` Tab Hosts die Komponent:
|
||||
|
||||
```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,
|
||||
},
|
||||
}],
|
||||
}],
|
||||
});
|
||||
```
|
||||
|
||||
Öffne jedes Dokument — ein **Vorschau** Tab macht es wunderschön, mit Links zur
|
||||
freigegebenen Webseite und dem PDF:
|
||||
|
||||
<Frame caption="Auf der Registerkarte Vorschau wird das Dokument mit Inline-Styles und Schnelllinks dargestellt.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/09-document-viewer.png" alt="Front-Komponente des Dokuments in einem Reiter der Datensatzseite" />
|
||||
</Frame>
|
||||
|
||||
## Vorlage mit dem Rich-Text-Editor bearbeiten
|
||||
|
||||
Templates benötigen überhaupt keine eigene Komponente. Da der `body` ein
|
||||
`RICH_TEXT`-Feld ist, stellt Twenty bereits einen vollständigen Rich-Text-Editor dafür bereit – denselben, den auch die Standardobjekte Note und Task verwenden. Wir Oberflächen es nur auf der
|
||||
Template-Datensatzseite.
|
||||
|
||||
Füge einen Tab mit einem `FIELD` Widget im `EDITOR` Anzeigemodus hinzu und zeigt auf das Feld `body`
|
||||
über `fieldMetadataId`:
|
||||
|
||||
```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',
|
||||
},
|
||||
}],
|
||||
}
|
||||
```
|
||||
|
||||
Ein `RICH_TEXT` Feld speichert sowohl den Editorblock JSON als auch eine Markdown
|
||||
Projektion. Die Generation-Pipeline liest diese Markdown-Projektion, also
|
||||
Platzhalter, die PDF, und die freigebbare Webseite arbeiten alle unverändert —
|
||||
sehen Sie den vollständigen
|
||||
[`template-record. age-layout.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/page-layouts/template-record.page-layout.ts).
|
||||
Jetzt schreiben Editoren Vorlagen in einem korrekten Rich-Text-Editor:
|
||||
|
||||
<Frame caption="Die Registerkarte Template: Der native Rich-Text-Editor von 20 ist an das Bodyfeld gebunden.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/10-template-editor.png" alt="Vorlageneintrag mit dem nativen Rich-Text-Editor-Tab" />
|
||||
</Frame>
|
||||
|
||||
**Nach diesem Schritt:** Dokumente Vorschau wunderschön und Vorlagen sind editierbar
|
||||
in-app. Lassen Sie als nächstes einen AI Agenten aus einem Chat generieren.
|
||||
|
||||
<Card title="Weiter: ein KI-Agent →" icon="robot" href="/l/de/developers/extend/apps/tutorials/document-generator/ai-agent">
|
||||
Fügen Sie einen Agenten und eine Fertigkeit hinzu, die Ihr Werkzeug aufruft.
|
||||
</Card>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: 1. Datenmodell
|
||||
icon: database
|
||||
description: Musterdokumente und Vorlagen mit Objekten, Feldern und einer Relation.
|
||||
---
|
||||
|
||||
Unsere App benötigt zwei benutzerdefinierte Objekte: **Dokumentvorlagen** (was geschrieben werden soll) und
|
||||
**Dokumente** (das generierte Ergebnis). Legen wir sie fest.
|
||||
|
||||
Jede Entitäts-Datei mit dem CLI entpacken — es erzeugt eine gültige UUID und den richtigen
|
||||
-Ordner für Sie:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:add object
|
||||
```
|
||||
|
||||
Unten zeigen wir die fertigen Dateien.
|
||||
|
||||
<Note>
|
||||
Jede `*_UNIVERSAL_IDENTIFIER` Konstante lebt in
|
||||
`src/constants/universal-identifiers.ts` und wird importiert, wo verwendet. Die Snippets
|
||||
unten lassen diese importieren, um kurz zu sein – halten Sie sie in Ihren eigenen Dateien.
|
||||
</Note>
|
||||
|
||||
## Das Template-Objekt
|
||||
|
||||
Eine Vorlage hat einen `name`, einen `body` mit `{{placeholders}}`, und ein `target`, das
|
||||
sagt, ob es für eine Person oder ein Unternehmen geschrieben wurde. Das Feld `body` ist ein
|
||||
`RICH_TEXT` Feld. Zwanzig gibt ihm also einen vollwertigen Rich-Text-Editor.
|
||||
|
||||
```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` Option **Werte** muss `UPPER_CASE` (`PERSON`, nicht `person`) sein und die
|
||||
`defaultValue` ist in extra Anführungszeichen eingewickelt: `` `'PERSON'` ``. Das `label` ist das, was
|
||||
Benutzer sehen.
|
||||
</Warning>
|
||||
|
||||
## Das Dokumentenobjekt
|
||||
|
||||
Das generierte Dokument speichert den gerenderten `content` und einen `status`. Definiere es
|
||||
auf die gleiche Weise, mit einer `status` Auswahl von `DRAFT` / `GENERATED`. Vollständige Datei:
|
||||
[`document.object.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/objects/document.object.ts).
|
||||
|
||||
## Verknüpfung mit einer Beziehung
|
||||
|
||||
Jedes Dokument sollte auf die Vorlage verweisen, aus der es stammt. Beziehungen sind
|
||||
**bidirectional** — Sie definieren beide Seiten, jede in ihrer eigenen Felddatei.
|
||||
|
||||
```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',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Die andere Seite (`template-documents-relation.field.ts`) ist ein
|
||||
`RelationType.ONE_TO_MANY` Feld mit dem Namen `documents`, das den entgegengesetzten Weg weist.
|
||||
Siehe [Relations](/l/de/developers/extend/apps/data/relations) für das vollständige Muster.
|
||||
|
||||
## Sehen Sie es in 20
|
||||
|
||||
Wenn `yarn twenty dev` läuft, öffnen Sie **Einstellungen → Datenmodell**. Beide Objekte erscheinen
|
||||
, markiert mit deiner App.
|
||||
|
||||
<Frame caption="Beide benutzerdefinierte Objekte, die der Document Generator App gehören.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/01-data-model.png" alt="Datenmodell-Einstellungen zeigen Dokumente und Dokumentvorlagen an" />
|
||||
</Frame>
|
||||
|
||||
Erstellen Sie eine Vorlage zum Testen mit — Name sie *Verkaufsvorschlag*, setzen Sie **Ziel** auf
|
||||
*Person*, und fügen Sie einen Körper mit ein paar Platzhaltern ein:
|
||||
|
||||
```text
|
||||
Dear {{name.firstName}} {{name.lastName}},
|
||||
|
||||
As {{jobTitle}} at {{company.name}}, we think you'll love our product.
|
||||
|
||||
Best,
|
||||
The Team
|
||||
```
|
||||
|
||||
<Frame caption="Ein Vorlageneintrag. Der Körper behält seine Platzhalter bis ein Dokument generiert ist.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/03-template-record.png" alt="Ein Verkaufsvorschlagvorlagen-Eintrag mit Platzhalterkörper" />
|
||||
</Frame>
|
||||
|
||||
**Nach diesem Schritt:** hast du `documentTemplate` und `document` Objekte, verlinkt von
|
||||
eine Relation, aus der eine Vorlage generiert werden kann. Als nächstes folgt die Logik, die sie ausfüllt.
|
||||
|
||||
<Card title="Weiter: Dokumente generieren →" icon="bolt" href="/l/de/developers/extend/apps/tutorials/document-generator/generating-documents">
|
||||
Schreibe die logische Funktion, die das Template füllt.
|
||||
</Card>
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
---
|
||||
title: 2. Dokumente generieren
|
||||
icon: bolt
|
||||
description: Eine Logikfunktion, die als KI-Werkzeug und Workflow-Aktion dargestellt wird.
|
||||
---
|
||||
|
||||
Jetzt der Kern: eine [Logikfunktion](/l/de/developers/extend/apps/logic/logic-functions)
|
||||
, die eine Vorlage und einen Datensatz lädt, die Platzhalter füllt und ein neues
|
||||
Dokument speichert.
|
||||
|
||||
Wir werden die Geschäftslogik einmal als **Handler** schreiben und sie dann durch
|
||||
mehrere Trigger ausblenden. Dieses Kapitel verbindet zwei davon – ein **AI-Tool** und eine
|
||||
**Workflow-Aktion**.
|
||||
|
||||
## Der Rendering-Helfer
|
||||
|
||||
Behalten Sie die reine Logik in ihrer eigenen Datei, so dass es leicht zu Unit-Test ist. Dies flattert einen Eintrag
|
||||
in `{{dot.path}}` Token und ersetzt diese.
|
||||
|
||||
```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>
|
||||
Da diese Datei keine Nebenwirkungen hat, können Sie sie mit schnellen Einheitstests
|
||||
(`Garn test:unit`) bedecken. Siehe [Testing](/l/de/developers/extend/apps/operations/testing).
|
||||
</Tip>
|
||||
|
||||
## Der Handler
|
||||
|
||||
Der Handler verwendet die generierte [`CoreApiClient`](/l/de/developers/extend/apps/logic/logic-functions)
|
||||
zum Lesen und Schreiben von CRM-Daten. Es lädt die Vorlage, lädt den Zieldatensatz, füllt den Körper
|
||||
und erstellt ein "Dokument".
|
||||
|
||||
```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` führt eine andere Abfrage für eine Person gegen eine Firma aus und flattert
|
||||
das Ergebnis — siehe
|
||||
[`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).
|
||||
|
||||
## Als Werkzeug und Workflow-Aktion anzeigen
|
||||
|
||||
Eine einzelne `defineLogicFunction` kann mehrere Trigger tragen. Hier macht `toolTriggerSettings`
|
||||
es von KI-Agenten aufrufbar und `workflowActionTriggerSettings` verwandelt es in einen
|
||||
Schritt im visuellen Workflow-Builder. Beide beschreiben ihre Eingabe mit einem JSON-Schema.
|
||||
|
||||
```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,
|
||||
});
|
||||
```
|
||||
|
||||
Das Eingabeschema ist ein einfaches JSON-Schema, das `templateId` und `recordId` —
|
||||
siehe [`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).
|
||||
|
||||
## Zugriff gewähren
|
||||
|
||||
Die logischen Funktionen laufen als Rolle der App. Es muss Vorlagen lesen und Datensätze
|
||||
und Dokumente erstellen, also erlauben Sie dies in `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` lässt die Funktion das generierte PDF im nächsten Abschnitt hochladen.
|
||||
Siehe [Roles](/l/de/developers/extend/apps/config/roles) für feinkörnige Berechtigungen.
|
||||
|
||||
## Eine echte PDF-Datei anhängen
|
||||
|
||||
Ein gerendertes Textfeld ist nützlich, aber Benutzer wollen ein echtes Dokument. Generieren wir ein
|
||||
**PDF** und speichern es als herunterladbare Datei.
|
||||
|
||||
Gib zuerst das `document` Objekt ein `FILES`-Feld um die PDF zu halten. Apps laden
|
||||
in ihre **eigenen** Datei-Felder hoch. Daher ist dieses Feld was das Hochladen leitet:
|
||||
|
||||
```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 },
|
||||
}
|
||||
```
|
||||
|
||||
Rendern Sie nun diese PDF. Eine App ist ein echtes Knoten-Projekt, so dass Sie jedes npm
|
||||
Paket hinzufügen und es wie überall sonst importieren können. Wir verwenden **[pdf-lib](https://pdf-lib.js.org/)**
|
||||
um die PDF zu zeichnen und **[marked](https://marked.js.org/)** um den Markdown
|
||||
Körper zu analysieren — der CLI installiert sie in die Laufzeit der Funktion für Sie:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add pdf-lib marked
|
||||
```
|
||||
|
||||
Der ganze Helfer ist
|
||||
[`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).
|
||||
Es analysiert Markdown in Tokens mit `markiert. exer`, legt sie dann mit
|
||||
pdf-lib: echte Überschriften, **fett**/*kursiv* läuft, Kugel und nummerierte Listen,
|
||||
Blockzitate und Regeln — eine polierte, mehrseitige A4 Darstellung der Vorlage
|
||||
selbst statt einer Wand aus Text.
|
||||
|
||||
<Frame caption="Die generierte PDF: echte Typografie und Markdown Formatierung, Darstellung des Template-Körpers.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07b-generated-pdf.png" alt="Ein poliertes, markierbares PDF" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
Die in pdf-lib integrierten Schriftarten verwenden WinAnsi-Codierung, sodass westeuropäische Akzente direkt funktionieren; der Helfer ordnet typografische Anführungszeichen und Gedankenstriche zu und verwirft Zeichen, die er nicht codieren kann. Das Rendern nicht-lateinischer Skripte (chinesisch, arabisch, kyrillisch) würde bedeuten, dass
|
||||
eine Unicode-Schriftart einbettet.
|
||||
</Note>
|
||||
|
||||
Dann laden Sie es hoch und speichern Sie die Referenz auf dem Datensatz. `uploadFile` ruft Bytes
|
||||
in dein app-owned Datei-Feld zurück; die zurückgegebene `id` ist, was du speicherst:
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Das generierte Dokument enthält jetzt ein herunterladbares PDF:
|
||||
|
||||
<Frame caption="Die erzeugte PDF, die im Dateifeld des Dokuments gespeichert ist.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/08-document-with-pdf.png" alt="Ein Datensatz mit einer generierten PDF-Datei" />
|
||||
</Frame>
|
||||
|
||||
<Note>
|
||||
`uploadFile` zielt nur auf **app-besitzer** Datei-Felder (Uploads erfordern also immer eine
|
||||
-App, die das Feld besitzt, plus das `UPLOAD_FILE` Rollenflag). Aus diesem Grund landet das PDF
|
||||
auf das eigene Feld `file` des Datensatzes — das gleiche Muster wie die
|
||||
[call-recorder app](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/public/call-recorder)
|
||||
verwendet für Aufnahmen.
|
||||
</Note>
|
||||
|
||||
**Nach diesem Schritt:** Jedes generierte Dokument hat eine echte, herunterladbare PDF. Aber
|
||||
nichts kann den Generator noch von der Oberfläche *aufrufen* – dafür benötigen wir eine HTTP-Route.
|
||||
|
||||
<Card title="Weiter: HTTP-Routen →" icon="globe" href="/l/de/developers/extend/apps/tutorials/document-generator/http-routes">
|
||||
Servieren Sie die Funktion über HTTP und rendern Sie Dokumente als Webseiten.
|
||||
</Card>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: 3. HTTP-Routen
|
||||
icon: globe
|
||||
description: Trigger die Funktion über HTTP und rendern Sie Dokumente als Webseiten.
|
||||
---
|
||||
|
||||
Der gleiche Handler kann auch HTTP-Anfragen beantworten. Wir werden zwei Routen hinzufügen:
|
||||
|
||||
* ein **POST** Endpunkt der UI-Aufrufe, um ein Dokument zu generieren, und
|
||||
* ein öffentlicher **GET** Endpunkt, der ein Dokument als druckbare Webseite darstellt.
|
||||
|
||||
Beide verwenden `httpRouteTriggerSettings`. App-Routen werden unter `/s` auf Ihrem
|
||||
20 Server bedient (z.B. `http://localhost:2020/s/documents/generate`).
|
||||
|
||||
## POST-Route — bei Bedarf generieren
|
||||
|
||||
Dies verwendet `generateDocumentHandler`, also gibt es keine Logik zu wiederholen — nur ein dünner
|
||||
-Adapter, der den Request-Körper liest.
|
||||
|
||||
```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,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Der Shared-Handler gibt einen vorgeschlagenen `status` bei einem Fehler zurück, so dass die Route
|
||||
mit einem richtigen `4xx`/`5xx` Code antworten kann. `isAuthRequired: true` bedeutet, dass der Anrufer
|
||||
ein gültiges Token vorweisen muss — die vorderste Komponente im nächsten Kapitel übergeht automatisch das Zugriffstoken des
|
||||
Benutzers.
|
||||
|
||||
## GET-Route — als Webseite rendern
|
||||
|
||||
Um HTML anstelle von JSON zurückzugeben, wickeln Sie den Körper in eine `Response` mit einem
|
||||
`Content-Type` Header. Diese Route ist öffentlich (`isAuthRequired: falsch`), so dass ein
|
||||
generiertes Dokument als Link freigegeben werden kann.
|
||||
|
||||
```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` stellt den Markdown-Text in HTML dar (mit [marked](https://marked.js.org/),
|
||||
bereinigt) und lässt ihn in ein Reinigen druckbare Seite, die nur die Inhalte der Vorlage
|
||||
zeigt — das gleiche Aussehen wie die PDF und die In-App-Vorschau.
|
||||
[Siehe den Helfer](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/document-generator/src/utils/render-document.ts).
|
||||
|
||||
## Testen
|
||||
|
||||
Mit einer Vorlage und einer Person im Arbeitsbereich rufen Sie die Route an (Benutzen Sie ein Token von
|
||||
**Einstellungen → APIs & 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, ..."}
|
||||
```
|
||||
|
||||
Öffnen Sie das zurückgegebene Dokument in Ihrem Browser:
|
||||
|
||||
```
|
||||
http://localhost:2020/s/documents/view?id=<documentId>
|
||||
```
|
||||
|
||||
<Frame caption="Die öffentliche GET-Route macht das Dokument als druckbare Seite.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Eine gerenderte Dokumenten-Webseite" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
Sie können auch Protokolle einer Funktion beim Testen mit
|
||||
`yarn zwanzig dev:function:logs` streamen oder direkt mit
|
||||
`yarn zwanzig dev:function:exec` aufrufen.
|
||||
</Tip>
|
||||
|
||||
**Nach diesem Schritt:** Die App kann Dokumente über HTTP generieren und sie als
|
||||
Webseiten bedienen. Jetzt machen wir es brauchbar ohne `curl`.
|
||||
|
||||
<Card title="Nächste: Bauen der UI →" icon="table-columns" href="/l/de/developers/extend/apps/tutorials/document-generator/building-the-ui">
|
||||
Views, Navigation, ein Kommando und eine Frontkomponente.
|
||||
</Card>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Tutorial: Dokumentgenerator"
|
||||
icon: wand-magic-sparkles
|
||||
description: Erstelle eine echte Twenty-App, die personalisierte Dokumente aus deinen CRM-Daten generiert.
|
||||
---
|
||||
|
||||
In diesem Tutorial erstellst du **Document Generator** – eine App, die wiederverwendbare Vorlagen in personalisierte Dokumente umwandelt, indem sie die bereits in deinem CRM vorhandenen Daten nutzt.
|
||||
|
||||
Schreibe einmal eine Vorlage mit `{{placeholders}}` und generiere dann mit einem Klick ein ausgefülltes Dokument für jede Person oder jedes Unternehmen – über das Befehlsmenü, über einen KI-Agenten oder über einen Workflow.
|
||||
|
||||
<Frame caption="Eine Vorlage, die für eine bestimmte Person generiert wurde und als druckbare Seite geöffnet ist.">
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/07-rendered-document.png" alt="Ein generiertes Verkaufsangebotsdokument" />
|
||||
</Frame>
|
||||
|
||||
## Was Sie lernen werden
|
||||
|
||||
Jedes Kapitel fügt eine Funktion hinzu. Am Ende haben Sie den Großteil des SDK kennengelernt.
|
||||
|
||||
| Kapitel | Funktion | Referenz |
|
||||
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
|
||||
| [1. Datenmodell](/l/de/developers/extend/apps/tutorials/document-generator/data-model) | Objekte, Felder und eine Relation | [Daten](/l/de/developers/extend/apps/data/overview) |
|
||||
| [2. Dokumente generieren](/l/de/developers/extend/apps/tutorials/document-generator/generating-documents) | Eine Logikfunktion (KI-Tool + Workflow-Aktion), die eine Markdown-Vorlage ausfüllt und ein fertiges PDF anhängt | [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) |
|
||||
| [3. HTTP-Routen](/l/de/developers/extend/apps/tutorials/document-generator/http-routes) | Ausliefern von JSON und einer teilbaren HTML-Seite über Routen | [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) |
|
||||
| [4. Die UI erstellen](/l/de/developers/extend/apps/tutorials/document-generator/building-the-ui) | Ansichten, Navigation, Befehlsmenü und Frontend-Komponenten, die ein Dokument anzeigen und eine Vorlage bearbeiten | [Layout](/l/de/developers/extend/apps/layout/overview) |
|
||||
| [5. Ein KI-Agent](/l/de/developers/extend/apps/tutorials/document-generator/ai-agent) | Agent + Skill | [Skills & Agenten](/l/de/developers/extend/apps/logic/skills-and-agents) |
|
||||
| [6. Veröffentlichen](/l/de/developers/extend/apps/tutorials/document-generator/publishing) | In den Marketplace veröffentlichen | [Veröffentlichen](/l/de/developers/extend/apps/operations/publishing) |
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Sie sollten den [Quick Start](/l/de/developers/extend/apps/getting-started/quick-start) abgeschlossen haben:
|
||||
einen lokalen Twenty-Server, der auf Port `2020` läuft, und die CLI, die damit authentifiziert ist.
|
||||
|
||||
Falls nicht, erstellen und starten Sie jetzt einen:
|
||||
|
||||
```bash filename="Terminal"
|
||||
npx create-twenty-app@latest document-generator
|
||||
cd document-generator
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
<Note>
|
||||
Lesen Sie lieber den fertigen Code? Die vollständige App befindet sich unter
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
Jedes der folgenden Codebeispiele ist daraus kopiert.
|
||||
</Note>
|
||||
|
||||
## Wie die App zusammenpasst
|
||||
|
||||
<Frame>
|
||||
<img src="/images/docs/developers/extends/apps/document-generator/how-it-fits.svg" alt="Eine Vorlage mit Platzhaltern wird in ein fertiges Dokument mit PDF generiert, ausgelöst über das Befehlsmenü, einen KI-Agenten, einen Workflow oder einen teilbaren Link" />
|
||||
</Frame>
|
||||
|
||||
Sie schreiben eine **Vorlage** einmal in einem Rich-Text-Editor mit `{{placeholders}}`. Die Auswahl einer
|
||||
Vorlage und eines CRM-Datensatzes füllt die Platzhalter aus und speichert ein fertiges
|
||||
**Dokument** (mit einer PDF-Datei). Alles andere – das Befehlsmenü, der KI-Agent,
|
||||
der Workflow-Schritt, der teilbare Link – ist nur eine andere Möglichkeit, denselben
|
||||
Generator auszulösen.
|
||||
|
||||
## Diesen Kreislauf am Laufen halten
|
||||
|
||||
Lassen Sie `yarn twenty dev` während des gesamten Tutorials in einem Terminal laufen. Jedes Mal, wenn
|
||||
Sie eine Datei unter `src/` hinzufügen oder bearbeiten, wird sie innerhalb weniger
|
||||
Sekunden mit Ihrem Server synchronisiert, sodass Sie beobachten können, wie jede Funktion in der UI erscheint, während Sie sie entwickeln.
|
||||
|
||||
<Card title="Beginnen Sie mit dem Bauen →" icon="database" href="/l/de/developers/extend/apps/tutorials/document-generator/data-model">
|
||||
Kapitel 1: Dokumente und Vorlagen modellieren.
|
||||
</Card>
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: 6. Veröffentlichen
|
||||
icon: rocket
|
||||
description: Fügen Sie Marktplatz-Metadaten hinzu und veröffentlichen Sie Ihre App.
|
||||
---
|
||||
|
||||
Ihre App funktioniert. Der letzte Schritt ist, es für den Marktplatz zu beschreiben und zu veröffentlichen.
|
||||
|
||||
## Marktplatz-Metadaten hinzufügen
|
||||
|
||||
Die [Anwendung config](/l/de/developers/extend/apps/config/application) trägt die
|
||||
Identität, die im Marktplatz erscheint: Autor, Kategorie, Logo und Unterstützung von
|
||||
Links. Lege ein Logo in `public/` ein und verweise es mit `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/de/developers/extend/apps',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: 'contact@twenty.com',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
});
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Die Standardrolle wird mit `defineApplicationRole()` in der eigenen Datei deklariert — Sie
|
||||
geben hier nicht mehr den `defaultRoleUniversalIdentifier` an.
|
||||
</Tip>
|
||||
|
||||
Füge auch das Schlüsselwort "twenty-app" zu "package.json" hinzu, damit die App entdeckt werden kann:
|
||||
|
||||
```json filename="package.json"
|
||||
{ "keywords": ["twenty-app"] }
|
||||
```
|
||||
|
||||
## Galerie Screenshots hinzufügen
|
||||
|
||||
Ein Marktplatz-Listing verkauft sich mit Screenshots. Legen Sie ein paar PNGs in
|
||||
`public/gallery/` und verweisen Sie sie mit `screenshots` — sie rendern als Galerie
|
||||
auf der Listenseite.
|
||||
|
||||
```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>
|
||||
Lead with the payoff: make the first screenshot the finished result (a generated
|
||||
document), then show how it triggered and authored. Verwende scharfe, hochauflösende
|
||||
Aufnahmen – sie sind das Erste, was ein Benutzer sieht.
|
||||
</Tip>
|
||||
|
||||
Gib `README.md` die gleiche Behandlung — es ist die Titelseite auf npm und GitHub.
|
||||
Öffnen Sie mit dem Wertvorschlag und einem Screenshot, führen Sie die Überschrift auf,
|
||||
und halten Sie dann die Baudetails unter dem Ordner.
|
||||
|
||||
## Vor dem Versand prüfen
|
||||
|
||||
Führe die gleichen Tore CI aus:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn lint # oxlint
|
||||
yarn typecheck # tsgo
|
||||
yarn test:unit # unit tests
|
||||
yarn twenty dev --once --dry-run # preview the metadata diff
|
||||
```
|
||||
|
||||
Der Trockenlauf druckt genau das, was sich auf dem Server ändern würde, ohne es anzuwenden —
|
||||
eine gute abschließende Vernunftprüfung. Siehe
|
||||
[Testing](/l/de/developers/extend/apps/operations/testing) und
|
||||
[Synchronisieren & Wiederherstellen](/l/de/developers/extend/apps/operations/sync-and-recovery).
|
||||
|
||||
## Veröffentlichen
|
||||
|
||||
```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` baut und veröffentlicht standardmäßig in npm ; `--private` lädt stattdessen einen
|
||||
Tarball in die private Registry eines Zwanzig Servers hoch. Um eine veröffentlichte App
|
||||
auf dem Marktplatz einer Instanz aufzudecken, löst eine Katalog-Synchronisation aus:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty dev:catalog-sync -r <remote>
|
||||
```
|
||||
|
||||
Vollständige Details und die Freigabe-Checkliste:
|
||||
[Publishing](/l/de/developers/extend/apps/operations/publishing).
|
||||
|
||||
## Du hast eine App :party_popper erstellt:
|
||||
|
||||
In sechs Kapiteln hast du den Großteil der SDK-Oberfläche verwendet:
|
||||
|
||||
* **Objekte, Felder und eine Relation** um die Daten zu modellieren
|
||||
* Eine **Logikfunktion** als **AI-Tool**, eine **Workflow-Aktion** und **HTTP-Routen**
|
||||
* **Ansichten, Navigation, Befehl und Frontkomponent** für die Benutzeroberfläche
|
||||
* Ein **Agent + Fertigkeit** für die Erzeugung natürlicher Sprache
|
||||
* **Marketplace-Metadaten** und der Veröffentlichungsfluss
|
||||
|
||||
Die fertige App ist bei
|
||||
[`packages/twenty-apps/examples/document-generator`](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/document-generator).
|
||||
|
||||
## Wohin Sie weiter gehen
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Datenreferenz" icon="database" href="/l/de/developers/extend/apps/data/overview">
|
||||
Jeder Feldtyp, Relation und Index-Option.
|
||||
</Card>
|
||||
<Card title="Logische Referenz" icon="bolt" href="/l/de/developers/extend/apps/logic/overview">
|
||||
Cron- und Datenbankereignis-Trigger, der Schlüsselwert-Store, OAuth Verbindungen.
|
||||
</Card>
|
||||
<Card title="Layout-Referenz" icon="table-columns" href="/l/de/developers/extend/apps/layout/overview">
|
||||
Seitenlayouts, Dashboard-Widgets und mehr UI-Oberflächen.
|
||||
</Card>
|
||||
<Card title="Operationen" icon="rocket" href="/l/de/developers/extend/apps/operations/overview">
|
||||
CLI, Tests, Fernbedienungen und CI.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Reference in New Issue
Block a user