Ana içeriğe atla
Şimdi esas kısma geçelim: bir şablon ve bir kaydı yükleyen, yer tutucuları dolduran ve yeni bir belge kaydeden bir mantık işlevi. İş mantığını handler olarak bir kez yazacağız, sonra bunu birkaç tetikleyici üzerinden kullanıma sunacağız. Bu bölüm bunlardan ikisini birleştiriyor — bir yapay zekâ aracı ve bir iş akışı eylemi.

Oluşturma yardımcısı

Saf mantığı kendi dosyasında tutun ki birim testi yapmak kolay olsun. Bu, bir kaydı {{dot.path}} belirteçlerine düzleştirir ve bunların yerine değerlerini koyar.
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] };
};
Bu dosyanın yan etkisi olmadığından, onu hızlı birim testleriyle kapsayabilirsiniz (yarn test:unit). Bkz. Testing.

İşleyici

Handler, CRM verilerini okumak ve yazmak için oluşturulmuş CoreApiClient kullanır. Şablonu yükler, hedef kaydı yükler, gövdeyi doldurur ve bir document oluşturur.
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, Kişi ile Şirket için farklı bir sorgu çalıştırır ve sonucu düzleştirir — bkz. load-record-values.ts.

Bunu bir araç ve iş akışı eylemi olarak kullanıma açın

Tek bir defineLogicFunction birden çok tetikleyici barındırabilir. Burada, toolTriggerSettings onu AI aracılarının çağırabileceği hâle getirir ve workflowActionTriggerSettings onu görsel iş akışı oluşturucusundaki bir adıma dönüştürür. Her ikisi de girdilerini bir JSON şemasıyla tanımlar.
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,
});
Girdi şeması, templateId ve recordId değerlerini tanımlayan basit bir JSON şemasıdır — bkz. generate-document-input.schema.ts.

Erişim verin

Mantık işlevleri uygulamanın rolü olarak çalışır. Şablonları ve kayıtları okuması ve belgeler oluşturması gerektiğinden, src/roles/default-role.ts içinde buna izin verin:
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, bir sonraki bölümde oluşturulan PDF’yi işlevin yüklemesine izin verir. Daha ayrıntılı izinler için bkz. Roles.

Gerçek bir PDF dosyası ekleyin

Oluşturulmuş bir metin alanı kullanışlıdır, ancak kullanıcılar gerçek bir belge ister. Bir PDF oluşturalım ve onu kayıt üzerinde indirilebilir bir dosya olarak saklayalım. Önce, PDF’yi tutmak için document nesnesine bir FILES alanı verin. Uygulamalar kendi dosya alanlarına yükleme yapar, dolayısıyla yüklemenin yönlendirildiği yer bu alandır:
{
  universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
  type: FieldType.FILES,
  name: 'file',
  label: 'File',
  icon: 'IconFileTypePdf',
  universalSettings: { maxNumberOfValues: 1 },
}
Şimdi o PDF’yi oluşturun. Bir uygulama gerçek bir Node projesidir, bu yüzden ihtiyacınız olan herhangi bir npm paketini ekleyebilir ve onu başka yerlerde olduğu gibi içe aktarabilirsiniz. Biz, PDF’yi çizmek için pdf-lib ve Markdown gövdesini ayrıştırmak için marked kullanıyoruz — CLI bunları işlevin çalışma zamanına sizin için kurar:
yarn add pdf-lib marked
Tam yardımcı işlev şuradadır: generate-document-pdf.ts. Bu, Markdown’ı marked.lexer ile belirteçlere ayrıştırır, ardından bunları pdf-lib ile yerleştirir: gerçek başlıklar, kalın/italik metinler, madde ve numaralı listeler, alıntılar ve çizgiler — şablonun kendisinin duvar gibi bir metin yerine cilalı, çok sayfalı A4 işlenmesi.
Cilalı, pazarlanabilir oluşturulmuş bir PDF
pdf-lib’in yerleşik yazı tipleri WinAnsi kodlaması kullanır, bu nedenle Batı Avrupa aksanları kutudan çıktığı gibi oluşturulur; yardımcı, akıllı tırnak işaretlerini ve tireleri eşler ve kodlayamadığı karakterleri atar. Latin olmayan yazı sistemlerini (Çince, Arapça, Kiril) işlemek, bir Unicode yazı tipi gömmeyi gerektirir.
Sonra onu yükleyin ve referansını kayıt üzerinde saklayın. uploadFile, baytları uygulamanıza ait dosya alanına yönlendirir; döndürülen id, kaydettiğiniz değerdir:
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,
  },
});
Oluşturulan belge artık indirilebilir bir PDF taşıyor:
Oluşturulmuş PDF dosyasına sahip bir belge kaydı
uploadFile yalnızca uygulamaya ait dosya alanlarını hedefler (bu nedenle yüklemeler her zaman alanın sahibi olan bir uygulama ve ayrıca UPLOAD_FILE rol bayrağı gerektirir). Bu nedenle PDF, kaydın kendi file alanına düşer — call-recorder app uygulamasının kayıtlar için kullandığı desenle aynıdır.
Bu adımdan sonra: her oluşturulan belgenin gerçek, indirilebilir bir PDF’si vardır. Ancak henüz hiçbir şey oluşturucuyu arayüzden çağıramıyor — bunun için bir HTTP rotasına ihtiyacımız var.

Sırada: HTTP rotaları →

Fonksiyonu HTTP üzerinden sunun ve belgeleri web sayfaları olarak oluşturun.