الانتقال إلى المحتوى الرئيسي
الآن النواة الأساسية: دالة المنطقية التي تحمّل قالب وسجل، تملأ العناصر النائبة، وتحفظ وثيقة جديدة. سنقوم بكتابة منطق العمل مرة واحدة ك**معالج **، ثم نكشف عنه من خلال عدة مشغلات. هذا الفصل يربط اثنين منهم - أداة AI و عمل سير العمل.

مساعد التقديم

حافظ على المنطق الخالص في ملفه الخاص بحيث أنه من السهل إجراء اختبار الوحدة. هذا يربط سجل في {{dot.path}} رموز ويبدلها.
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] };
};
لأن هذا الملف ليس له تأثيرات جانبية، يمكنك تغطيته عن طريق اختبارات الوحدة السريعة (‘اختبار yarn:unit`). انظر Testing.

المعالج

يستخدم المعالج CoreApiClient لقراءة وكتابة بيانات CRM. يحمّل القالب، ويحمّل السجل المستهدف، ويملأ الجسم، وينشئ وثيقة.
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’ يدير استفسارا مختلفا لشخص ضد شركة ومسطحات النتيجة - انظر `load-record-values.ts’.

الكشف عنه كأداة وإجراءات سير العمل

يمكن لـ defineLogicFunction' أن يحمل عدة مشغلات. هنا، "إعدادات الأدوات" تجعلها قابلة للاستدعاء من قبل عملاء AI ، و workflowtionTriggertings` تحولها إلى خطوة في منشئ سير العمل البصري. ويصف كلاهما إسهاماتهما بمخطط “JSON”.
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,
});
مخطط المدخلات هو مخطط JSON بسيط يصف templateId وrecordId - أنظر generate-document-input.schema.ts.

امنح حق الوصول

الوظائف المنطقية تعمل كدور للتطبيق. تحتاج إلى قراءة القوالب والسجلات وإنشاء المستندات، لذلك اسمح بذلك في src/roles/default-role.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/ar/developers/extend/apps/config/rolesللحصول على أذونات مأخوذة من الحبوب الأكثر.

إرفاق ملف PDF حقيقي

حقل نص محرر مفيد، لكن المستخدمين يريدون وثيقة حقيقية. دعونا ننشئ PDF ونخزنه في السجل كملف قابل للتنزيل. أولا، اعطي الكائن ‘document’ حقل ‘FILES’ للاحتفاظ بـ PDF. تقوم التطبيقات برفع في حقول الملفات الخاصة بها الخاصة، لذلك هذا الحقل هو مسارات الرفع:
{
  universalIdentifier: DOCUMENT_FILE_FIELD_UNIVERSAL_IDENTIFIER,
  type: FieldType.FILES,
  name: 'file',
  label: 'File',
  icon: 'IconFileTypePdf',
  universalSettings: { maxNumberOfValues: 1 },
}
الآن اجعل قوات الدفاع الشعبي تلك. التطبيق هو مشروع عقدة حقيقي، بحيث يمكنك إضافة أي npm حزمة تحتاج إليها واستيرادها مثل أي مكان آخر. نحن نستخدم pdf-lib لرسم PDF و marked لتحليل جسم Markdown- يقوم CLI بتثبيته في وقت تشغيل الوظيفة لك:
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). إنه يحلل علامة الرموز المميزة مع علامة علامة’. ممارسة ’، ثم تضعها باستخدام pdf-lib: العناوين الحقيقية، الجريئة/italic، تدير الرصاصات والقوائم المرقمة، الكتل والقواعد-عبارة عن مقتطفات متعددة الصفحات A4 لتقديم القالب نفسه، وليس جدار نص.
تم توليد PDF مصقول قابل للتسويق
خطوط pdf-lib المدمجة تستخدم تشفير WinAnsi ، لذا لهجات غرب أوروبا تقدم خارج الصندوق؛ خرائط المساعد مقتبسات ذكية والشرطات وتسقط الشخصيات لا يمكن ترميزها. إن عرض النصوص غير اللاتينية (الصينية، العربية، السيريلية) سيعني تضمين خط Unicode.
ثم قم بتحميله وتخزين المرجع في السجل. مسارات ‘uploadFile’ بايت إلى حقل الملفات الذي تملكه التطبيق؛ و`id’ المعاد هو ما قمت بحفظه:
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 قابل للتنزيل:
سجل مستند مع ملف PDF الذي تم إنشاؤه
‘uploadFile’ يستهدف فقط حقول الملفات التي تملكها التطبيقات (لذلك يتطلب التحميل دائمًا تطبيق يمتلك الحقل، بالإضافة إلى علم الدور ‘UPLOAD_FILE`). لهذا السبب يهبط PDF في الحقل ‘file’ الخاص بالسجل - نفس النمط الذي يستخدمه call-record للتسجيلات.
بعد هذه الخطوة: كل مستند تم إنشاؤه لديه PDF حقيقي، قابل للتنزيل. ولكن لا يوجد ما يمكنه استدعاء المولّد من واجهة المستخدم بعد — ولأجل ذلك نحتاج إلى مسار HTTP.

التالي: طرق HTTP →

خدمة الوظيفة عبر HTTP وتقديم الوثائق كصفحات على الويب.