@infuro/cms-core 1.0.31 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/admin.cjs +929 -345
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.js +947 -362
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +839 -59
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +833 -53
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs +159 -14
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +37 -22
- package/dist/auth.d.ts +37 -22
- package/dist/auth.js +153 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +3 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/{index-Bda8D1Rm.d.ts → index-DWd5Gjc4.d.ts} +2 -2
- package/dist/{index-BcMRGNjS.d.cts → index-rZTnpK7y.d.cts} +2 -2
- package/dist/index.cjs +1141 -254
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -30
- package/dist/index.d.ts +41 -30
- package/dist/index.js +1135 -254
- package/dist/index.js.map +1 -1
- package/dist/migrations/1778660000003-AddQrTokenToOrders.ts +32 -0
- package/dist/migrations/1779100000000-ChatConversationLeadEmailSent.ts +16 -0
- package/dist/migrations/1779200000000-LlmAgentScope.ts +72 -0
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -1296,6 +1296,8 @@ __export(src_exports, {
|
|
|
1296
1296
|
hasEntityPermission: () => hasEntityPermission,
|
|
1297
1297
|
hashOtpCode: () => hashOtpCode,
|
|
1298
1298
|
hydrateVendorSessionUser: () => hydrateVendorSessionUser,
|
|
1299
|
+
isAuthDebugClientEnabled: () => isAuthDebugClientEnabled,
|
|
1300
|
+
isAuthDebugEnabled: () => isAuthDebugEnabled,
|
|
1299
1301
|
isCustomerTypeContact: () => isCustomerTypeContact,
|
|
1300
1302
|
isOpenEndpoint: () => isOpenEndpoint,
|
|
1301
1303
|
isPlatformAdministrator: () => isPlatformAdministrator,
|
|
@@ -1314,6 +1316,8 @@ __export(src_exports, {
|
|
|
1314
1316
|
loadPublicThemeSettings: () => loadPublicThemeSettings,
|
|
1315
1317
|
loadUserVendorContext: () => loadUserVendorContext,
|
|
1316
1318
|
localStoragePlugin: () => localStoragePlugin,
|
|
1319
|
+
logAuth: () => logAuth,
|
|
1320
|
+
logAuthClient: () => logAuthClient,
|
|
1317
1321
|
logEntityAccessDecision: () => logEntityAccessDecision,
|
|
1318
1322
|
logRbac: () => logRbac,
|
|
1319
1323
|
mergeEmailLayoutCompanyDetails: () => mergeEmailLayoutCompanyDetails,
|
|
@@ -1323,6 +1327,7 @@ __export(src_exports, {
|
|
|
1323
1327
|
metaPostPageFeed: () => metaPostPageFeed,
|
|
1324
1328
|
metaPostPagePhoto: () => metaPostPagePhoto,
|
|
1325
1329
|
metaResolvePageAccessToken: () => metaResolvePageAccessToken,
|
|
1330
|
+
nextAuthCookieDebugInfo: () => nextAuthCookieDebugInfo,
|
|
1326
1331
|
normalizePhoneE164: () => normalizePhoneE164,
|
|
1327
1332
|
parseBlogGeneratorAgentContent: () => parseBlogGeneratorAgentContent,
|
|
1328
1333
|
parseBlogGeneratorModelOutput: () => parseBlogGeneratorModelOutput,
|
|
@@ -1370,6 +1375,7 @@ __export(src_exports, {
|
|
|
1370
1375
|
smsPlugin: () => smsPlugin,
|
|
1371
1376
|
socialMediaPlugin: () => socialMediaPlugin,
|
|
1372
1377
|
summarizeEntityPerms: () => summarizeEntityPerms,
|
|
1378
|
+
summarizeSessionUserForLog: () => summarizeSessionUserForLog,
|
|
1373
1379
|
syncJobScheduleToPgBoss: () => syncJobScheduleToPgBoss,
|
|
1374
1380
|
truncateText: () => truncateText,
|
|
1375
1381
|
validateScheduleInput: () => validateScheduleInput,
|
|
@@ -2497,6 +2503,58 @@ function escapeHtml7(s) {
|
|
|
2497
2503
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2498
2504
|
}
|
|
2499
2505
|
|
|
2506
|
+
// src/plugins/email/templates/chatLead.ts
|
|
2507
|
+
function render10(ctx) {
|
|
2508
|
+
const {
|
|
2509
|
+
contactName,
|
|
2510
|
+
contactEmail,
|
|
2511
|
+
contactPhone,
|
|
2512
|
+
conversationId,
|
|
2513
|
+
latestMessage,
|
|
2514
|
+
intentCode,
|
|
2515
|
+
intentReason,
|
|
2516
|
+
transcript,
|
|
2517
|
+
companyDetails
|
|
2518
|
+
} = ctx;
|
|
2519
|
+
const intentDisplay = intentCode?.trim() ? `${intentCode.trim()}${intentReason?.trim() ? ` \u2014 ${intentReason.trim()}` : ""}` : intentReason;
|
|
2520
|
+
const subject = `Chat lead${intentCode?.trim() ? ` [${intentCode.trim()}]` : ""}: ${contactName || contactEmail || "Visitor"}`;
|
|
2521
|
+
const transcriptBlock = transcript?.trim() ? `<div style="margin-top:16px;padding:12px;background:#f9fafb;border-radius:6px;border:1px solid #e5e7eb;">
|
|
2522
|
+
<p style="margin:0 0 8px 0;font-size:13px;font-weight:600;color:#374151;">Recent conversation</p>
|
|
2523
|
+
<pre style="margin:0;font-size:12px;white-space:pre-wrap;font-family:inherit;color:#4b5563;">${escapeHtml8(transcript.trim())}</pre>
|
|
2524
|
+
</div>` : "";
|
|
2525
|
+
const bodyHtml = `<p style="margin:0 0 6px 0;font-size:18px;font-weight:600;color:#111;">New chat lead</p>
|
|
2526
|
+
<p style="margin:0 0 12px 0;font-size:14px;color:#374151;">A visitor showed interest based on your intent rules.</p>
|
|
2527
|
+
<table style="width:100%;border-collapse:collapse;font-size:14px;">
|
|
2528
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Name</td><td>${escapeHtml8(contactName || "\u2014")}</td></tr>
|
|
2529
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Email</td><td><a href="mailto:${escapeHtml8(contactEmail)}">${escapeHtml8(contactEmail)}</a></td></tr>
|
|
2530
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Phone</td><td>${escapeHtml8(contactPhone?.trim() || "\u2014")}</td></tr>
|
|
2531
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Conversation</td><td>#${conversationId}</td></tr>
|
|
2532
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Intent</td><td>${escapeHtml8(intentReason)}</td></tr>
|
|
2533
|
+
</table>
|
|
2534
|
+
<p style="margin:16px 0 6px 0;font-size:13px;font-weight:600;color:#374151;">Latest message</p>
|
|
2535
|
+
<p style="margin:0;font-size:14px;white-space:pre-wrap;">${escapeHtml8(latestMessage)}</p>
|
|
2536
|
+
${transcriptBlock}`;
|
|
2537
|
+
const text = [
|
|
2538
|
+
"New chat lead",
|
|
2539
|
+
`Name: ${contactName || "\u2014"}`,
|
|
2540
|
+
`Email: ${contactEmail}`,
|
|
2541
|
+
`Phone: ${contactPhone?.trim() || "\u2014"}`,
|
|
2542
|
+
`Conversation: #${conversationId}`,
|
|
2543
|
+
`Intent: ${intentDisplay}`,
|
|
2544
|
+
"",
|
|
2545
|
+
"Latest message:",
|
|
2546
|
+
latestMessage,
|
|
2547
|
+
transcript?.trim() ? `
|
|
2548
|
+
Recent conversation:
|
|
2549
|
+
${transcript.trim()}` : ""
|
|
2550
|
+
].join("\n");
|
|
2551
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
2552
|
+
return { subject, html, text };
|
|
2553
|
+
}
|
|
2554
|
+
function escapeHtml8(s) {
|
|
2555
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2500
2558
|
// src/plugins/email/templates/index.ts
|
|
2501
2559
|
var templateRenderMap = {
|
|
2502
2560
|
signup: render,
|
|
@@ -2507,7 +2565,8 @@ var templateRenderMap = {
|
|
|
2507
2565
|
shippingUpdate: render6,
|
|
2508
2566
|
invite: render7,
|
|
2509
2567
|
formSubmission: render8,
|
|
2510
|
-
otp: render9
|
|
2568
|
+
otp: render9,
|
|
2569
|
+
chatLead: render10
|
|
2511
2570
|
};
|
|
2512
2571
|
function getTemplateRenderer(name) {
|
|
2513
2572
|
return templateRenderMap[name];
|
|
@@ -2524,11 +2583,11 @@ function renderEmail(templateName, ctx, options) {
|
|
|
2524
2583
|
return { subject: custom.subject, html, text: custom.text };
|
|
2525
2584
|
}
|
|
2526
2585
|
}
|
|
2527
|
-
const
|
|
2528
|
-
if (!
|
|
2586
|
+
const render11 = getTemplateRenderer(templateName);
|
|
2587
|
+
if (!render11) {
|
|
2529
2588
|
throw new Error(`Unknown email template: ${templateName}`);
|
|
2530
2589
|
}
|
|
2531
|
-
return
|
|
2590
|
+
return render11(ctx);
|
|
2532
2591
|
}
|
|
2533
2592
|
|
|
2534
2593
|
// src/plugins/email/email-service.ts
|
|
@@ -2600,6 +2659,10 @@ var EmailService = class {
|
|
|
2600
2659
|
renderTemplate(templateName, ctx) {
|
|
2601
2660
|
return renderEmail(templateName, ctx, this.templateOptions);
|
|
2602
2661
|
}
|
|
2662
|
+
/** Default notification recipient from plugin config (SMTP_TO / plugin `to`). */
|
|
2663
|
+
getDefaultTo() {
|
|
2664
|
+
return this.config.to;
|
|
2665
|
+
}
|
|
2603
2666
|
};
|
|
2604
2667
|
var emailTemplates = {
|
|
2605
2668
|
formSubmission: (data) => ({
|
|
@@ -2630,6 +2693,434 @@ This link expires in 1 hour.`
|
|
|
2630
2693
|
|
|
2631
2694
|
// src/plugins/email/index.ts
|
|
2632
2695
|
init_email_queue();
|
|
2696
|
+
|
|
2697
|
+
// src/plugins/email/chat-lead-email.ts
|
|
2698
|
+
init_email_queue();
|
|
2699
|
+
|
|
2700
|
+
// src/lib/email-recipients.ts
|
|
2701
|
+
function parseEmailRecipientsFromConfig(raw) {
|
|
2702
|
+
if (raw == null || raw === "") return [];
|
|
2703
|
+
const trimmed = raw.trim();
|
|
2704
|
+
if (trimmed.startsWith("[")) {
|
|
2705
|
+
try {
|
|
2706
|
+
const parsed = JSON.parse(trimmed);
|
|
2707
|
+
if (Array.isArray(parsed)) {
|
|
2708
|
+
return parsed.map((e) => String(e).trim()).filter(Boolean);
|
|
2709
|
+
}
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
2714
|
+
}
|
|
2715
|
+
function serializeEmailRecipients(emails) {
|
|
2716
|
+
return JSON.stringify(emails);
|
|
2717
|
+
}
|
|
2718
|
+
function joinRecipientsForSend(emails) {
|
|
2719
|
+
if (!emails.length) return null;
|
|
2720
|
+
return emails.join(", ");
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// src/plugins/llm/chat-email-intent.ts
|
|
2724
|
+
var NONE_INTENT = "NONE";
|
|
2725
|
+
var CHAT_EMAIL_LOG = "[chat-email-tool]";
|
|
2726
|
+
function logChatEmail(step, data) {
|
|
2727
|
+
if (data && Object.keys(data).length > 0) {
|
|
2728
|
+
console.info(CHAT_EMAIL_LOG, step, data);
|
|
2729
|
+
} else {
|
|
2730
|
+
console.info(CHAT_EMAIL_LOG, step);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
function normalizeIntentKey(raw) {
|
|
2734
|
+
return raw.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2735
|
+
}
|
|
2736
|
+
function parseIntentsJson(raw) {
|
|
2737
|
+
if (!raw?.trim()) return null;
|
|
2738
|
+
try {
|
|
2739
|
+
const parsed = JSON.parse(raw);
|
|
2740
|
+
if (!Array.isArray(parsed)) return null;
|
|
2741
|
+
const out = [];
|
|
2742
|
+
for (const row of parsed) {
|
|
2743
|
+
if (!row || typeof row !== "object") continue;
|
|
2744
|
+
const o = row;
|
|
2745
|
+
const intent = normalizeIntentKey(String(o.intent ?? o.id ?? ""));
|
|
2746
|
+
const description = String(o.description ?? "").trim();
|
|
2747
|
+
const emailTo = String(o.emailTo ?? o.email ?? "").trim();
|
|
2748
|
+
if (!intent || !description || !emailTo) continue;
|
|
2749
|
+
out.push({ intent, description, emailTo });
|
|
2750
|
+
}
|
|
2751
|
+
return out.length > 0 ? out : null;
|
|
2752
|
+
} catch {
|
|
2753
|
+
return null;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function dedupeIntents(intents) {
|
|
2757
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2758
|
+
const out = [];
|
|
2759
|
+
for (const row of intents) {
|
|
2760
|
+
const key = normalizeIntentKey(row.intent);
|
|
2761
|
+
if (!key || seen.has(key)) continue;
|
|
2762
|
+
seen.add(key);
|
|
2763
|
+
out.push({
|
|
2764
|
+
intent: key,
|
|
2765
|
+
description: row.description.trim(),
|
|
2766
|
+
emailTo: row.emailTo.trim()
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
return out;
|
|
2770
|
+
}
|
|
2771
|
+
function parseChatEmailToolSettings(map) {
|
|
2772
|
+
const fromJson = parseIntentsJson(map.emailIntents);
|
|
2773
|
+
const intents = dedupeIntents(fromJson ?? []);
|
|
2774
|
+
const legacyPositive = map.emailIntentPrompt?.trim() ?? "";
|
|
2775
|
+
const legacyNegative = map.emailNegativeIntentPrompt?.trim() ?? "";
|
|
2776
|
+
let classifierInstructions = map.emailClassifierInstructions?.trim() ?? "";
|
|
2777
|
+
if (!classifierInstructions && (legacyPositive || legacyNegative)) {
|
|
2778
|
+
const parts = [];
|
|
2779
|
+
if (legacyPositive) parts.push(`Legacy positive signals:
|
|
2780
|
+
${legacyPositive}`);
|
|
2781
|
+
if (legacyNegative) parts.push(`Do NOT assign an intent when:
|
|
2782
|
+
${legacyNegative}`);
|
|
2783
|
+
classifierInstructions = parts.join("\n\n");
|
|
2784
|
+
}
|
|
2785
|
+
return {
|
|
2786
|
+
enabled: map.emailToolEnabled === "true",
|
|
2787
|
+
intents,
|
|
2788
|
+
classifierInstructions,
|
|
2789
|
+
toolPrompt: map.emailToolPrompt ?? ""
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
var EMAIL_TOOL_VALIDATION_KEY = "emailTool";
|
|
2793
|
+
function parseEmailToolObject(raw) {
|
|
2794
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2795
|
+
const o = raw;
|
|
2796
|
+
const intentsRaw = o.intents;
|
|
2797
|
+
let intents;
|
|
2798
|
+
if (Array.isArray(intentsRaw)) {
|
|
2799
|
+
const parsed = parseIntentsJson(JSON.stringify(intentsRaw));
|
|
2800
|
+
if (parsed?.length) intents = parsed;
|
|
2801
|
+
}
|
|
2802
|
+
return {
|
|
2803
|
+
enabled: o.enabled === true,
|
|
2804
|
+
classifierInstructions: typeof o.classifierInstructions === "string" ? o.classifierInstructions.trim() : void 0,
|
|
2805
|
+
toolPrompt: typeof o.toolPrompt === "string" ? o.toolPrompt.trim() : void 0,
|
|
2806
|
+
intents
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
function parseEmailToolFromAgentValidationRules(validationRulesText) {
|
|
2810
|
+
const raw = validationRulesText?.trim();
|
|
2811
|
+
if (!raw) return null;
|
|
2812
|
+
try {
|
|
2813
|
+
const parsed = JSON.parse(raw);
|
|
2814
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
2815
|
+
const emailTool = parsed[EMAIL_TOOL_VALIDATION_KEY];
|
|
2816
|
+
return parseEmailToolObject(emailTool);
|
|
2817
|
+
} catch {
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
function resolveChatEmailToolSettings(configMap, sources) {
|
|
2822
|
+
const fromConfig = parseChatEmailToolSettings(configMap);
|
|
2823
|
+
const resolved = typeof sources === "string" || sources == null ? { chatbotValidationRules: typeof sources === "string" ? sources : null } : sources;
|
|
2824
|
+
const fromChatbot = parseEmailToolFromAgentValidationRules(resolved.chatbotValidationRules);
|
|
2825
|
+
const fromNotify = resolved.notifyAgent ? parseEmailToolFromAgentValidationRules(resolved.notifyAgent.validationRules) : null;
|
|
2826
|
+
const notifySystem = resolved.notifyAgent?.systemInstruction?.trim() ?? "";
|
|
2827
|
+
const intents = fromNotify?.intents?.length ? fromNotify.intents : fromChatbot?.intents?.length ? fromChatbot.intents : fromConfig.intents;
|
|
2828
|
+
const classifierParts = [
|
|
2829
|
+
notifySystem,
|
|
2830
|
+
fromNotify?.classifierInstructions?.trim(),
|
|
2831
|
+
fromChatbot?.classifierInstructions?.trim(),
|
|
2832
|
+
fromConfig.classifierInstructions.trim()
|
|
2833
|
+
].filter(Boolean);
|
|
2834
|
+
const toolPrompt = fromNotify?.toolPrompt?.trim() || fromChatbot?.toolPrompt?.trim() || fromConfig.toolPrompt?.trim() || "";
|
|
2835
|
+
return {
|
|
2836
|
+
enabled: fromConfig.enabled,
|
|
2837
|
+
intents,
|
|
2838
|
+
classifierInstructions: classifierParts.join("\n\n"),
|
|
2839
|
+
toolPrompt
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
function intentByKey(intents, key) {
|
|
2843
|
+
if (!key) return null;
|
|
2844
|
+
const norm3 = normalizeIntentKey(key);
|
|
2845
|
+
return intents.find((i) => i.intent === norm3) ?? null;
|
|
2846
|
+
}
|
|
2847
|
+
function buildIntentTableForPrompt(intents) {
|
|
2848
|
+
return intents.map((i) => `- ${i.intent}: ${i.description} \u2192 notify ${i.emailTo}`).join("\n");
|
|
2849
|
+
}
|
|
2850
|
+
function buildIntentClassifierSystem(config, agentClassifierInstructions) {
|
|
2851
|
+
const table = buildIntentTableForPrompt(config.intents);
|
|
2852
|
+
const extra = [agentClassifierInstructions?.trim(), config.classifierInstructions.trim()].filter(Boolean).join("\n\n");
|
|
2853
|
+
const intentKeys = config.intents.map((i) => i.intent).join(", ");
|
|
2854
|
+
return `You classify a chat visitor into exactly one lead intent for email routing.
|
|
2855
|
+
|
|
2856
|
+
Available intents (pick the best match):
|
|
2857
|
+
${table}
|
|
2858
|
+
|
|
2859
|
+
If the visitor is not ready for a lead email (general FAQ, greetings only, spam, off-topic, no clear business need), use intent "${NONE_INTENT}".
|
|
2860
|
+
|
|
2861
|
+
Allowed intent values: ${intentKeys}, or ${NONE_INTENT}.
|
|
2862
|
+
|
|
2863
|
+
${extra ? `Additional instructions:
|
|
2864
|
+
${extra}
|
|
2865
|
+
` : ""}
|
|
2866
|
+
Reply with ONLY valid JSON, no markdown:
|
|
2867
|
+
{"intent":"INTENT_KEY","reason":"short explanation"}
|
|
2868
|
+
Use "${NONE_INTENT}" when no intent applies.`;
|
|
2869
|
+
}
|
|
2870
|
+
function buildChatEmailAgentContext(settings) {
|
|
2871
|
+
if (!settings.enabled) return "";
|
|
2872
|
+
const classifier = settings.classifierInstructions.trim();
|
|
2873
|
+
const extra = settings.toolPrompt?.trim() ?? "";
|
|
2874
|
+
const parts = [];
|
|
2875
|
+
if (settings.intents.length > 0) {
|
|
2876
|
+
parts.push(
|
|
2877
|
+
"## Lead email routing",
|
|
2878
|
+
"When a visitor message clearly matches an intent below, the system sends one team notification email (per conversation). Continue the conversation normally in all cases."
|
|
2879
|
+
);
|
|
2880
|
+
if (classifier) parts.push(`### Routing rules
|
|
2881
|
+
${classifier}`);
|
|
2882
|
+
parts.push(`### Intent catalog
|
|
2883
|
+
${buildIntentTableForPrompt(settings.intents)}`);
|
|
2884
|
+
} else if (classifier) {
|
|
2885
|
+
parts.push(`## Lead email routing
|
|
2886
|
+
${classifier}`);
|
|
2887
|
+
}
|
|
2888
|
+
if (extra) parts.push(`### Assistant behavior
|
|
2889
|
+
${extra}`);
|
|
2890
|
+
return parts.join("\n\n");
|
|
2891
|
+
}
|
|
2892
|
+
function buildChatbotSystemPromptWithEmailTool(baseSystemInstruction, guardrailsForPrompt, emailSettings) {
|
|
2893
|
+
let prompt = (baseSystemInstruction ?? "").trim();
|
|
2894
|
+
const guard = guardrailsForPrompt?.trim();
|
|
2895
|
+
if (guard) prompt = [prompt, guard].filter(Boolean).join("\n\n");
|
|
2896
|
+
if (emailSettings.enabled) {
|
|
2897
|
+
const emailCtx = buildChatEmailAgentContext(emailSettings);
|
|
2898
|
+
if (emailCtx) prompt = [prompt, emailCtx].filter(Boolean).join("\n\n");
|
|
2899
|
+
}
|
|
2900
|
+
return prompt;
|
|
2901
|
+
}
|
|
2902
|
+
function formatTranscript(history, latestMessage, maxChars = 4e3) {
|
|
2903
|
+
const lines = [];
|
|
2904
|
+
for (const m of history) {
|
|
2905
|
+
const role = m.role === "assistant" ? "Assistant" : m.role === "user" ? "Visitor" : "System";
|
|
2906
|
+
lines.push(`${role}: ${m.content}`);
|
|
2907
|
+
}
|
|
2908
|
+
const last = history[history.length - 1];
|
|
2909
|
+
if (!last || last.role !== "user" || last.content !== latestMessage) {
|
|
2910
|
+
lines.push(`Visitor: ${latestMessage}`);
|
|
2911
|
+
}
|
|
2912
|
+
let text = lines.join("\n");
|
|
2913
|
+
if (text.length > maxChars) text = text.slice(-maxChars);
|
|
2914
|
+
return text;
|
|
2915
|
+
}
|
|
2916
|
+
function parseIntentJson(raw, intents) {
|
|
2917
|
+
const trimmed = raw.trim();
|
|
2918
|
+
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
2919
|
+
const candidate = (fence?.[1] ?? trimmed).trim();
|
|
2920
|
+
const tryParse = (s) => {
|
|
2921
|
+
const o = JSON.parse(s);
|
|
2922
|
+
let intentRaw = null;
|
|
2923
|
+
if (typeof o.intent === "string") intentRaw = o.intent;
|
|
2924
|
+
else if (o.interested === false) intentRaw = NONE_INTENT;
|
|
2925
|
+
else if (o.interested === true && intents[0]) intentRaw = intents[0].intent;
|
|
2926
|
+
if (intentRaw == null) return null;
|
|
2927
|
+
const reason = typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : "Classified from conversation";
|
|
2928
|
+
return { intent: normalizeIntentKey(intentRaw), reason };
|
|
2929
|
+
};
|
|
2930
|
+
try {
|
|
2931
|
+
return tryParse(candidate);
|
|
2932
|
+
} catch {
|
|
2933
|
+
const m = candidate.match(/\{[\s\S]*\}/);
|
|
2934
|
+
if (!m) return null;
|
|
2935
|
+
try {
|
|
2936
|
+
return tryParse(m[0]);
|
|
2937
|
+
} catch {
|
|
2938
|
+
return null;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
async function detectChatLeadIntent(llm, params) {
|
|
2943
|
+
if (!params.settings.intents.length) {
|
|
2944
|
+
logChatEmail("classify skipped", { reason: "no_intents_configured" });
|
|
2945
|
+
return {
|
|
2946
|
+
intent: null,
|
|
2947
|
+
intentLabel: "NONE",
|
|
2948
|
+
reason: "No lead intents configured",
|
|
2949
|
+
emailTo: null
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
logChatEmail("classify start", {
|
|
2953
|
+
intentCount: params.settings.intents.length,
|
|
2954
|
+
intentKeys: params.settings.intents.map((i) => i.intent),
|
|
2955
|
+
model: params.model?.trim() || "(gateway default)",
|
|
2956
|
+
messageChars: params.message.length,
|
|
2957
|
+
historyTurns: params.history.length,
|
|
2958
|
+
hasClassifierInstructions: Boolean(
|
|
2959
|
+
params.agentClassifierInstructions?.trim() || params.settings.classifierInstructions.trim()
|
|
2960
|
+
)
|
|
2961
|
+
});
|
|
2962
|
+
const transcript = formatTranscript(params.history, params.message);
|
|
2963
|
+
const contactLines = [
|
|
2964
|
+
params.contact.name?.trim() ? `Name: ${params.contact.name.trim()}` : null,
|
|
2965
|
+
params.contact.email?.trim() ? `Email: ${params.contact.email.trim()}` : null,
|
|
2966
|
+
params.contact.phone?.trim() ? `Phone: ${params.contact.phone.trim()}` : null
|
|
2967
|
+
].filter(Boolean).join("\n");
|
|
2968
|
+
const userPrompt = `Customer:
|
|
2969
|
+
${contactLines || "(unknown)"}
|
|
2970
|
+
|
|
2971
|
+
Conversation:
|
|
2972
|
+
${transcript}
|
|
2973
|
+
|
|
2974
|
+
Pick the single best intent for the visitor's latest message.`;
|
|
2975
|
+
const res = await llm.chatAgent({
|
|
2976
|
+
systemPrompt: buildIntentClassifierSystem(params.settings, params.agentClassifierInstructions),
|
|
2977
|
+
userPrompt,
|
|
2978
|
+
temperature: 0.1,
|
|
2979
|
+
max_tokens: 256,
|
|
2980
|
+
...params.model?.trim() ? { model: params.model.trim() } : {}
|
|
2981
|
+
});
|
|
2982
|
+
const rawContent = res.content ?? "";
|
|
2983
|
+
logChatEmail("classify llm response", {
|
|
2984
|
+
responseChars: rawContent.length,
|
|
2985
|
+
responsePreview: rawContent.slice(0, 280) + (rawContent.length > 280 ? "\u2026" : "")
|
|
2986
|
+
});
|
|
2987
|
+
const parsed = parseIntentJson(rawContent, params.settings.intents);
|
|
2988
|
+
if (!parsed) {
|
|
2989
|
+
logChatEmail("classify result", {
|
|
2990
|
+
matched: false,
|
|
2991
|
+
outcome: "parse_failed",
|
|
2992
|
+
emailWillSend: false
|
|
2993
|
+
});
|
|
2994
|
+
return {
|
|
2995
|
+
intent: null,
|
|
2996
|
+
intentLabel: "Unclassified",
|
|
2997
|
+
reason: "Could not parse intent classifier response",
|
|
2998
|
+
emailTo: null
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
if (parsed.intent === NONE_INTENT) {
|
|
3002
|
+
logChatEmail("classify result", {
|
|
3003
|
+
matched: false,
|
|
3004
|
+
outcome: NONE_INTENT,
|
|
3005
|
+
reason: parsed.reason,
|
|
3006
|
+
emailWillSend: false
|
|
3007
|
+
});
|
|
3008
|
+
return {
|
|
3009
|
+
intent: null,
|
|
3010
|
+
intentLabel: NONE_INTENT,
|
|
3011
|
+
reason: parsed.reason,
|
|
3012
|
+
emailTo: null
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
const matched = intentByKey(params.settings.intents, parsed.intent);
|
|
3016
|
+
if (!matched) {
|
|
3017
|
+
logChatEmail("classify result", {
|
|
3018
|
+
matched: false,
|
|
3019
|
+
outcome: "unknown_intent",
|
|
3020
|
+
parsedIntent: parsed.intent,
|
|
3021
|
+
reason: parsed.reason,
|
|
3022
|
+
emailWillSend: false
|
|
3023
|
+
});
|
|
3024
|
+
return {
|
|
3025
|
+
intent: null,
|
|
3026
|
+
intentLabel: parsed.intent,
|
|
3027
|
+
reason: `Unknown intent "${parsed.intent}": ${parsed.reason}`,
|
|
3028
|
+
emailTo: null
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
logChatEmail("classify result", {
|
|
3032
|
+
matched: true,
|
|
3033
|
+
outcome: "intent_found",
|
|
3034
|
+
intent: matched.intent,
|
|
3035
|
+
emailTo: matched.emailTo,
|
|
3036
|
+
reason: parsed.reason,
|
|
3037
|
+
emailWillSend: true
|
|
3038
|
+
});
|
|
3039
|
+
return {
|
|
3040
|
+
intent: matched.intent,
|
|
3041
|
+
intentLabel: `${matched.intent} \u2014 ${matched.description}`,
|
|
3042
|
+
reason: parsed.reason,
|
|
3043
|
+
emailTo: matched.emailTo
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
function buildTranscriptForLeadEmail(history, latestMessage) {
|
|
3047
|
+
return formatTranscript(history, latestMessage);
|
|
3048
|
+
}
|
|
3049
|
+
function parseIntentRecipientEmails(emailTo) {
|
|
3050
|
+
return emailTo.split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 0 && s.includes("@"));
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// src/plugins/email/chat-lead-email.ts
|
|
3054
|
+
function resolveLeadRecipients(emailPlugin2, emailSettings) {
|
|
3055
|
+
const fromCrm = parseEmailRecipientsFromConfig(
|
|
3056
|
+
emailSettings.crmEmails ?? emailSettings.crmEmail ?? ""
|
|
3057
|
+
);
|
|
3058
|
+
if (fromCrm.length > 0) return fromCrm;
|
|
3059
|
+
const fallback = emailPlugin2.getDefaultTo?.() ?? "";
|
|
3060
|
+
if (fallback.trim()) return [fallback.trim()];
|
|
3061
|
+
return [];
|
|
3062
|
+
}
|
|
3063
|
+
async function sendChatLeadEmail(cms, emailSettings, brandingSettings, input) {
|
|
3064
|
+
console.info(CHAT_EMAIL_LOG, "send start", {
|
|
3065
|
+
conversationId: input.conversationId,
|
|
3066
|
+
intentCode: input.intentCode ?? null,
|
|
3067
|
+
contactEmail: input.contactEmail,
|
|
3068
|
+
contactName: input.contactName,
|
|
3069
|
+
explicitRecipients: input.recipients?.length ?? 0
|
|
3070
|
+
});
|
|
3071
|
+
const email = cms.getPlugin("email");
|
|
3072
|
+
if (!email?.send || !email.renderTemplate) {
|
|
3073
|
+
console.warn(CHAT_EMAIL_LOG, "send failed", {
|
|
3074
|
+
conversationId: input.conversationId,
|
|
3075
|
+
reason: "email_plugin_disabled"
|
|
3076
|
+
});
|
|
3077
|
+
return { sent: false, recipients: [], error: "Email plugin is not enabled" };
|
|
3078
|
+
}
|
|
3079
|
+
const recipients = input.recipients?.filter((r) => r.trim().includes("@")).map((r) => r.trim()) ?? resolveLeadRecipients(email, emailSettings);
|
|
3080
|
+
if (recipients.length === 0) {
|
|
3081
|
+
console.warn(CHAT_EMAIL_LOG, "send failed", {
|
|
3082
|
+
conversationId: input.conversationId,
|
|
3083
|
+
reason: "no_recipients"
|
|
3084
|
+
});
|
|
3085
|
+
return {
|
|
3086
|
+
sent: false,
|
|
3087
|
+
recipients: [],
|
|
3088
|
+
error: "No lead email recipients (configure intent Email To, CRM emails, or SMTP default To)"
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
console.info(CHAT_EMAIL_LOG, "send recipients resolved", {
|
|
3092
|
+
conversationId: input.conversationId,
|
|
3093
|
+
recipients,
|
|
3094
|
+
source: input.recipients?.length ? "intent_email_to" : "crm_or_smtp_fallback"
|
|
3095
|
+
});
|
|
3096
|
+
const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
|
|
3097
|
+
const ctx = {
|
|
3098
|
+
...input,
|
|
3099
|
+
companyDetails: input.companyDetails ?? companyDetails
|
|
3100
|
+
};
|
|
3101
|
+
let anySent = false;
|
|
3102
|
+
for (const to of recipients) {
|
|
3103
|
+
await queueEmail(cms, {
|
|
3104
|
+
to,
|
|
3105
|
+
templateName: "chatLead",
|
|
3106
|
+
ctx
|
|
3107
|
+
});
|
|
3108
|
+
console.info(CHAT_EMAIL_LOG, "send queued", {
|
|
3109
|
+
conversationId: input.conversationId,
|
|
3110
|
+
to,
|
|
3111
|
+
template: "chatLead"
|
|
3112
|
+
});
|
|
3113
|
+
anySent = true;
|
|
3114
|
+
}
|
|
3115
|
+
console.info(CHAT_EMAIL_LOG, "send complete", {
|
|
3116
|
+
conversationId: input.conversationId,
|
|
3117
|
+
sent: anySent,
|
|
3118
|
+
recipientCount: recipients.length
|
|
3119
|
+
});
|
|
3120
|
+
return { sent: anySent, recipients };
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
// src/plugins/email/index.ts
|
|
2633
3124
|
function emailPlugin(config) {
|
|
2634
3125
|
return {
|
|
2635
3126
|
name: "email",
|
|
@@ -3773,6 +4264,56 @@ ${context}`;
|
|
|
3773
4264
|
}
|
|
3774
4265
|
};
|
|
3775
4266
|
|
|
4267
|
+
// src/plugins/llm/llm-agent-scope.ts
|
|
4268
|
+
var LLM_AGENT_SCOPE_CHATBOT = "chatbot";
|
|
4269
|
+
var LLM_AGENT_SCOPE_BLOG_CREATION = "blog_creation";
|
|
4270
|
+
var LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST = "social_media_post";
|
|
4271
|
+
var LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT = "email_intent_chatbot";
|
|
4272
|
+
var LLM_AGENT_SCOPE_BLOG_METADATA = "blog_metadata";
|
|
4273
|
+
var LLM_AGENT_SCOPES = [
|
|
4274
|
+
LLM_AGENT_SCOPE_CHATBOT,
|
|
4275
|
+
LLM_AGENT_SCOPE_BLOG_CREATION,
|
|
4276
|
+
LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST,
|
|
4277
|
+
LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
|
|
4278
|
+
LLM_AGENT_SCOPE_BLOG_METADATA
|
|
4279
|
+
];
|
|
4280
|
+
var LLM_AGENT_DEFAULT_SLUG_BY_SCOPE = {
|
|
4281
|
+
[LLM_AGENT_SCOPE_CHATBOT]: "site-chat-assistant",
|
|
4282
|
+
[LLM_AGENT_SCOPE_BLOG_CREATION]: "blog-generator",
|
|
4283
|
+
[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "blog-generator-social",
|
|
4284
|
+
[LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "email-intent-chatbot",
|
|
4285
|
+
[LLM_AGENT_SCOPE_BLOG_METADATA]: "blog-generator-metadata"
|
|
4286
|
+
};
|
|
4287
|
+
var LLM_AGENT_DEFAULT_NAME_BY_SCOPE = {
|
|
4288
|
+
[LLM_AGENT_SCOPE_CHATBOT]: "Site Chat Assistant",
|
|
4289
|
+
[LLM_AGENT_SCOPE_BLOG_CREATION]: "Blog Generator",
|
|
4290
|
+
[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]: "Blog Generator (Social)",
|
|
4291
|
+
[LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT]: "Chat Lead Intent Classifier",
|
|
4292
|
+
[LLM_AGENT_SCOPE_BLOG_METADATA]: "Blog Generator (Metadata)"
|
|
4293
|
+
};
|
|
4294
|
+
var SITE_CHAT_ASSISTANT_SLUG = LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
|
|
4295
|
+
var SITE_CHAT_ASSISTANT_NAME = LLM_AGENT_DEFAULT_NAME_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
|
|
4296
|
+
function isLlmAgentScope(value) {
|
|
4297
|
+
if (!value?.trim()) return false;
|
|
4298
|
+
return LLM_AGENT_SCOPES.includes(value.trim());
|
|
4299
|
+
}
|
|
4300
|
+
var BLOG_LLM_AGENT_SLUGS = [
|
|
4301
|
+
LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_CREATION],
|
|
4302
|
+
LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_BLOG_METADATA],
|
|
4303
|
+
LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST]
|
|
4304
|
+
];
|
|
4305
|
+
|
|
4306
|
+
// src/plugins/llm/find-llm-agent-by-scope.ts
|
|
4307
|
+
async function findLlmAgentByScope(dataSource, entityMap, scope, options = {}) {
|
|
4308
|
+
const { enabledOnly = true } = options;
|
|
4309
|
+
const entity = entityMap.llm_agents;
|
|
4310
|
+
if (!entity) return null;
|
|
4311
|
+
const repo = dataSource.getRepository(entity);
|
|
4312
|
+
const where = { scope, deleted: false };
|
|
4313
|
+
if (enabledOnly) where.enabled = true;
|
|
4314
|
+
return repo.findOne({ where });
|
|
4315
|
+
}
|
|
4316
|
+
|
|
3776
4317
|
// src/plugins/llm/index.ts
|
|
3777
4318
|
function normalizeEmbeddingProvider(raw) {
|
|
3778
4319
|
if (!raw) return "openai";
|
|
@@ -4142,6 +4683,7 @@ var LlmAgent = class {
|
|
|
4142
4683
|
id;
|
|
4143
4684
|
name;
|
|
4144
4685
|
slug;
|
|
4686
|
+
scope;
|
|
4145
4687
|
systemInstruction;
|
|
4146
4688
|
model;
|
|
4147
4689
|
temperature;
|
|
@@ -4165,6 +4707,9 @@ __decorateClass([
|
|
|
4165
4707
|
__decorateClass([
|
|
4166
4708
|
(0, import_typeorm3.Column)("varchar")
|
|
4167
4709
|
], LlmAgent.prototype, "slug", 2);
|
|
4710
|
+
__decorateClass([
|
|
4711
|
+
(0, import_typeorm3.Column)("varchar", { nullable: true })
|
|
4712
|
+
], LlmAgent.prototype, "scope", 2);
|
|
4168
4713
|
__decorateClass([
|
|
4169
4714
|
(0, import_typeorm3.Column)("text", { name: "system_instruction", default: "" })
|
|
4170
4715
|
], LlmAgent.prototype, "systemInstruction", 2);
|
|
@@ -4361,155 +4906,6 @@ async function persistAllGeneratedBlogDrafts(dataSource, maps, params) {
|
|
|
4361
4906
|
return out;
|
|
4362
4907
|
}
|
|
4363
4908
|
|
|
4364
|
-
// src/plugins/blog-generator/blog-generator-agent-defaults.ts
|
|
4365
|
-
var BLOG_GENERATOR_AGENT_NAME = "Blog Generator Agent";
|
|
4366
|
-
var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
|
|
4367
|
-
var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
|
|
4368
|
-
var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
|
|
4369
|
-
|
|
4370
|
-
Your task is to generate completely original, high-quality blog article(s) from the latest RSS-derived source material in the user message (one block per feed). The user message is factual input only.
|
|
4371
|
-
|
|
4372
|
-
IMPORTANT RULES:
|
|
4373
|
-
|
|
4374
|
-
1. NEVER copy the RSS article directly.
|
|
4375
|
-
2. Rewrite the information into a fresh, human-like article.
|
|
4376
|
-
3. Expand the topic with professional insights and explanations.
|
|
4377
|
-
4. Maintain a natural editorial tone.
|
|
4378
|
-
5. Make the article SEO-friendly.
|
|
4379
|
-
6. Use engaging headings and subheadings.
|
|
4380
|
-
7. Avoid robotic AI phrasing.
|
|
4381
|
-
8. Do not mention that the content came from RSS or feeds.
|
|
4382
|
-
9. Preserve factual accuracy from the source material.
|
|
4383
|
-
10. The output should feel like a professionally written industry blog.
|
|
4384
|
-
|
|
4385
|
-
MULTIPLE FEEDS:
|
|
4386
|
-
- If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
|
|
4387
|
-
- If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
|
|
4388
|
-
- You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
|
|
4389
|
-
|
|
4390
|
-
WRITING STYLE:
|
|
4391
|
-
- Professional
|
|
4392
|
-
- Clear
|
|
4393
|
-
- Informative
|
|
4394
|
-
- Business-focused
|
|
4395
|
-
- Human sounding
|
|
4396
|
-
- Modern editorial style
|
|
4397
|
-
|
|
4398
|
-
ARTICLE STRUCTURE (each article \u2014 express in HTML):
|
|
4399
|
-
1. Engaging introduction
|
|
4400
|
-
2. Industry context
|
|
4401
|
-
3. Main developments
|
|
4402
|
-
4. Key implications
|
|
4403
|
-
5. Expert/business analysis
|
|
4404
|
-
6. Conclusion
|
|
4405
|
-
|
|
4406
|
-
HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
|
|
4407
|
-
- Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
|
|
4408
|
-
- Wrap each complete article in a single root: <article class="blog-post"> ... </article>
|
|
4409
|
-
- Inside <article>, use this outline:
|
|
4410
|
-
- <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
|
|
4411
|
-
- <section class="blog-post-body"> for all following content
|
|
4412
|
-
- Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
|
|
4413
|
-
- Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
|
|
4414
|
-
- Use <ul>/<ol> with <li> for lists where appropriate.
|
|
4415
|
-
- Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
|
|
4416
|
-
- Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
|
|
4417
|
-
- Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
|
|
4418
|
-
- Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
|
|
4419
|
-
|
|
4420
|
-
CONTENT REQUIREMENTS:
|
|
4421
|
-
- Minimum 800 words per article unless source material is very small.
|
|
4422
|
-
- Add context around industry trends where appropriate.
|
|
4423
|
-
- Explain why the topic matters.
|
|
4424
|
-
- Include practical implications for businesses/professionals.
|
|
4425
|
-
|
|
4426
|
-
IF RSS CONTENT IS LIMITED:
|
|
4427
|
-
- Expand intelligently using general industry knowledge.
|
|
4428
|
-
- Keep the article relevant to the original topic.
|
|
4429
|
-
- Do not invent fake facts or statistics.
|
|
4430
|
-
|
|
4431
|
-
OUTPUT FORMAT (STRICT \u2014 HTML only):
|
|
4432
|
-
- Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
|
|
4433
|
-
- If you output more than one article, output multiple <article class="blog-post">\u2026</article> blocks in sequence, and between articles put a single line containing exactly this (and nothing else on that line):
|
|
4434
|
-
${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
|
|
4435
|
-
- If you output a single article, use one <article> only and do not use that separator line.`;
|
|
4436
|
-
var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
4437
|
-
{
|
|
4438
|
-
maxUserChars: 5e5,
|
|
4439
|
-
guardrails: `Your reply must be semantic HTML blog article(s) only, as in the system OUTPUT FORMAT (<article class="blog-post">\u2026). No JSON, no Markdown, no explanations before or after the HTML. If multiple articles, separate them with a line containing exactly ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR} alone. Do not mention RSS, feeds, or scraping. Preserve factual accuracy; do not invent statistics or quotes.`
|
|
4440
|
-
},
|
|
4441
|
-
null,
|
|
4442
|
-
2
|
|
4443
|
-
);
|
|
4444
|
-
|
|
4445
|
-
// src/plugins/blog-generator/blog-generator-metadata-defaults.ts
|
|
4446
|
-
var BLOG_METADATA_ENRICHER_AGENT_NAME = "Blog Generator Metadata";
|
|
4447
|
-
var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
|
|
4448
|
-
var BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a careful CMS metadata assistant for a publishing system.
|
|
4449
|
-
|
|
4450
|
-
You receive ONE complete blog post as HTML (title is usually in <h1 class="blog-post-title"> or the first <h1>). Your job is to propose structured fields so an editor can save the post: category, URL slug, SEO block, and topical tags.
|
|
4451
|
-
|
|
4452
|
-
RULES:
|
|
4453
|
-
1. Read the whole article and infer the primary topic and audience.
|
|
4454
|
-
2. categoryName must be EXACTLY one string from AVAILABLE_BLOG_CATEGORIES in the user message, or "" if none fit.
|
|
4455
|
-
3. blogSlug: lowercase kebab-case, ASCII letters/digits/hyphens only, no leading/trailing hyphens, max ~80 chars. Derive from the main topic or title.
|
|
4456
|
-
4. seo.title: concise meta title (~50\u201360 characters when reasonable).
|
|
4457
|
-
5. seo.description: meta description (~150\u2013160 characters when reasonable), plain text.
|
|
4458
|
-
6. seo.keywords: comma-separated phrases, no stuffing.
|
|
4459
|
-
7. seo.ogTitle / seo.ogDescription: may mirror title/description or be slightly adapted for social.
|
|
4460
|
-
8. tags: 4\u201310 short labels (1\u20133 words each). Prefer exact matches from EXISTING_TAG_NAMES when they truly apply; otherwise invent precise new labels (they may be created in the CMS later).
|
|
4461
|
-
9. Do not invent statistics, quotes, or URLs not implied by the article.
|
|
4462
|
-
10. Output JSON only \u2014 no markdown outside the JSON, no commentary.
|
|
4463
|
-
|
|
4464
|
-
OUTPUT FORMAT (STRICT):
|
|
4465
|
-
Return a single JSON object only. Do not wrap it in a markdown code fence.
|
|
4466
|
-
|
|
4467
|
-
{
|
|
4468
|
-
"categoryName": "string \u2014 exact match from list or empty string",
|
|
4469
|
-
"blogSlug": "string \u2014 url-safe kebab-case",
|
|
4470
|
-
"seo": {
|
|
4471
|
-
"title": "string or empty",
|
|
4472
|
-
"description": "string or empty",
|
|
4473
|
-
"keywords": "string or empty",
|
|
4474
|
-
"ogTitle": "string or empty",
|
|
4475
|
-
"ogDescription": "string or empty"
|
|
4476
|
-
},
|
|
4477
|
-
"tags": ["tag-one", "tag-two"]
|
|
4478
|
-
}`;
|
|
4479
|
-
var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
4480
|
-
{
|
|
4481
|
-
maxUserChars: 5e5,
|
|
4482
|
-
guardrails: "Reply with one valid JSON object only (see system OUTPUT FORMAT). No markdown fences, no text before or after JSON. Keys: categoryName, blogSlug, seo { title, description, keywords, ogTitle, ogDescription }, tags (array of strings)."
|
|
4483
|
-
},
|
|
4484
|
-
null,
|
|
4485
|
-
2
|
|
4486
|
-
);
|
|
4487
|
-
|
|
4488
|
-
// src/plugins/blog-generator/blog-generator-social-defaults.ts
|
|
4489
|
-
var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
|
|
4490
|
-
var BLOG_SOCIAL_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a social copywriter for LinkedIn-style posts.
|
|
4491
|
-
|
|
4492
|
-
You receive ONE blog post as HTML (title may appear in <h1> or the first heading). Write a short post body that can be published as the main text on LinkedIn: engaging, accurate, no clickbait, no invented facts or quotes.
|
|
4493
|
-
|
|
4494
|
-
RULES:
|
|
4495
|
-
1. Plain text only in the JSON value (no HTML tags in socialMediaContent). Line breaks allowed as \\n if helpful.
|
|
4496
|
-
2. Length: aim for roughly 600\u20131300 characters (hard max 2900 characters) so it fits typical social feeds.
|
|
4497
|
-
3. Summarize the core insight; you may include 1\u20132 short hooks, but stay faithful to the article.
|
|
4498
|
-
4. Do not add URLs unless they appear explicitly in the article.
|
|
4499
|
-
5. Output JSON only \u2014 no markdown fences, no commentary outside JSON.
|
|
4500
|
-
|
|
4501
|
-
OUTPUT FORMAT (STRICT):
|
|
4502
|
-
Return a single JSON object only:
|
|
4503
|
-
{ "socialMediaContent": "string \u2014 the post text" }`;
|
|
4504
|
-
var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
4505
|
-
{
|
|
4506
|
-
maxUserChars: 5e5,
|
|
4507
|
-
guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
|
|
4508
|
-
},
|
|
4509
|
-
null,
|
|
4510
|
-
2
|
|
4511
|
-
);
|
|
4512
|
-
|
|
4513
4909
|
// src/api/rss-feed-blog-api.ts
|
|
4514
4910
|
function deriveRssFeedDisplayName(rssUrl) {
|
|
4515
4911
|
try {
|
|
@@ -4620,18 +5016,14 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
|
|
|
4620
5016
|
let socialLlmAgentChatOptions;
|
|
4621
5017
|
let metadataRow = null;
|
|
4622
5018
|
let socialRow = null;
|
|
5019
|
+
let blogCreationRow = null;
|
|
4623
5020
|
if (entityMap.llm_agents) {
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
});
|
|
4628
|
-
if (agentRow) {
|
|
4629
|
-
const o = llmAgentToChatAgentOptions(agentRow);
|
|
5021
|
+
blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
|
|
5022
|
+
if (blogCreationRow) {
|
|
5023
|
+
const o = llmAgentToChatAgentOptions(blogCreationRow);
|
|
4630
5024
|
llmAgentChatOptions = { model: o.model, temperature: o.temperature, max_tokens: o.max_tokens };
|
|
4631
5025
|
}
|
|
4632
|
-
metadataRow = await
|
|
4633
|
-
where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
4634
|
-
});
|
|
5026
|
+
metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
|
|
4635
5027
|
if (metadataRow) {
|
|
4636
5028
|
const mo = llmAgentToChatAgentOptions(metadataRow);
|
|
4637
5029
|
metadataLlmAgentChatOptions = {
|
|
@@ -4640,9 +5032,7 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
|
|
|
4640
5032
|
max_tokens: mo.max_tokens
|
|
4641
5033
|
};
|
|
4642
5034
|
}
|
|
4643
|
-
socialRow = await
|
|
4644
|
-
where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
4645
|
-
});
|
|
5035
|
+
socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
|
|
4646
5036
|
if (socialRow) {
|
|
4647
5037
|
const so = llmAgentToChatAgentOptions(socialRow);
|
|
4648
5038
|
socialLlmAgentChatOptions = {
|
|
@@ -4687,6 +5077,8 @@ async function runBlogGenerateFromSchedule(dataSource, entityMap, schedule, cms,
|
|
|
4687
5077
|
const out = await svc.generateBlogMarkdownFromRss({
|
|
4688
5078
|
llm,
|
|
4689
5079
|
rssUrls,
|
|
5080
|
+
systemInstruction: blogCreationRow?.systemInstruction?.trim() || void 0,
|
|
5081
|
+
validationRules: blogCreationRow?.validationRules?.trim() || void 0,
|
|
4690
5082
|
categoryNamesHint: categoryRows.map((c) => c.name),
|
|
4691
5083
|
tagNamesHint: tagNames,
|
|
4692
5084
|
llmAgentChatOptions,
|
|
@@ -6782,16 +7174,19 @@ function normalizeChatModeSetting(raw) {
|
|
|
6782
7174
|
if (raw === "external" || raw === "llm") return raw;
|
|
6783
7175
|
return "whatsapp";
|
|
6784
7176
|
}
|
|
6785
|
-
async function
|
|
7177
|
+
async function loadSettingsGroupMap(dataSource, entityMap, group) {
|
|
6786
7178
|
if (!entityMap.configs) return {};
|
|
6787
7179
|
const repo = dataSource.getRepository(entityMap.configs);
|
|
6788
|
-
const rows = await repo.find({ where: { settings:
|
|
7180
|
+
const rows = await repo.find({ where: { settings: group, deleted: false } });
|
|
6789
7181
|
const out = {};
|
|
6790
7182
|
for (const row of rows) {
|
|
6791
7183
|
out[row.key] = row.value;
|
|
6792
7184
|
}
|
|
6793
7185
|
return out;
|
|
6794
7186
|
}
|
|
7187
|
+
async function loadLlmSettingsMap(dataSource, entityMap) {
|
|
7188
|
+
return loadSettingsGroupMap(dataSource, entityMap, "llm");
|
|
7189
|
+
}
|
|
6795
7190
|
function createChatHandlers(config) {
|
|
6796
7191
|
const { dataSource, entityMap, json, getCms } = config;
|
|
6797
7192
|
const contactRepo = () => dataSource.getRepository(entityMap.contacts);
|
|
@@ -6803,10 +7198,13 @@ function createChatHandlers(config) {
|
|
|
6803
7198
|
try {
|
|
6804
7199
|
const map = await loadLlmSettingsMap(dataSource, entityMap);
|
|
6805
7200
|
const mode = normalizeChatModeSetting(map.chatMode);
|
|
7201
|
+
const chatbotAgent = mode === "llm" && entityMap.llm_agents ? await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT, {
|
|
7202
|
+
enabledOnly: false
|
|
7203
|
+
}) : null;
|
|
6806
7204
|
const body = {
|
|
6807
7205
|
enabled: map.enabled !== "false",
|
|
6808
7206
|
chatMode: mode,
|
|
6809
|
-
agentSlug: mode === "llm" ?
|
|
7207
|
+
agentSlug: mode === "llm" ? chatbotAgent?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT] : "",
|
|
6810
7208
|
botName: map.botName ?? "",
|
|
6811
7209
|
icon: map.icon ?? "",
|
|
6812
7210
|
iconImageUrl: map.iconImageUrl ?? "",
|
|
@@ -6892,24 +7290,26 @@ function createChatHandlers(config) {
|
|
|
6892
7290
|
if (!llm?.chat) return json({ error: "LLM not configured" }, { status: 503 });
|
|
6893
7291
|
const llmSettings = await loadLlmSettingsMap(dataSource, entityMap);
|
|
6894
7292
|
const supportMode = normalizeChatModeSetting(llmSettings.chatMode);
|
|
6895
|
-
|
|
6896
|
-
if (!effectiveSlug && supportMode === "llm" && entityMap.llm_agents) {
|
|
6897
|
-
effectiveSlug = (llmSettings.attachedAgentSlug ?? "").trim();
|
|
6898
|
-
}
|
|
7293
|
+
const bodyAgentSlug = (body?.agentSlug ?? "").trim();
|
|
6899
7294
|
let agentRow = null;
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
7295
|
+
let effectiveSlug = bodyAgentSlug;
|
|
7296
|
+
if (entityMap.llm_agents) {
|
|
7297
|
+
if (bodyAgentSlug) {
|
|
7298
|
+
const agentRepo = dataSource.getRepository(
|
|
7299
|
+
entityMap.llm_agents
|
|
7300
|
+
);
|
|
7301
|
+
agentRow = await agentRepo.findOne({
|
|
7302
|
+
where: { slug: bodyAgentSlug, deleted: false, enabled: true }
|
|
7303
|
+
});
|
|
7304
|
+
if (!agentRow) {
|
|
7305
|
+
return json({ error: "Agent not found or disabled", agentSlug: bodyAgentSlug }, { status: 404 });
|
|
7306
|
+
}
|
|
7307
|
+
} else if (supportMode === "llm") {
|
|
7308
|
+
agentRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_CHATBOT);
|
|
7309
|
+
effectiveSlug = agentRow?.slug?.trim() || LLM_AGENT_DEFAULT_SLUG_BY_SCOPE[LLM_AGENT_SCOPE_CHATBOT];
|
|
6912
7310
|
}
|
|
7311
|
+
} else if (bodyAgentSlug) {
|
|
7312
|
+
return json({ error: "LLM agents are not configured on this deployment" }, { status: 400 });
|
|
6913
7313
|
}
|
|
6914
7314
|
console.info(RAG_LOG, "step 1 | resolve agent", {
|
|
6915
7315
|
agentSlug: effectiveSlug || "(none)",
|
|
@@ -7005,8 +7405,141 @@ function createChatHandlers(config) {
|
|
|
7005
7405
|
});
|
|
7006
7406
|
}
|
|
7007
7407
|
}
|
|
7008
|
-
const historyRaw = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()).map((m) => ({ role: m.role, content: m.content }));
|
|
7009
|
-
const history = historyBeforeCurrentUser(historyRaw, message);
|
|
7408
|
+
const historyRaw = (conv.messages ?? []).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()).map((m) => ({ role: m.role, content: m.content }));
|
|
7409
|
+
const history = historyBeforeCurrentUser(historyRaw, message);
|
|
7410
|
+
const notifyEmailAgent = await findLlmAgentByScope(
|
|
7411
|
+
dataSource,
|
|
7412
|
+
entityMap,
|
|
7413
|
+
LLM_AGENT_SCOPE_EMAIL_INTENT_CHATBOT,
|
|
7414
|
+
{ enabledOnly: false }
|
|
7415
|
+
);
|
|
7416
|
+
const emailTool = resolveChatEmailToolSettings(llmSettings, {
|
|
7417
|
+
chatbotValidationRules: agentRow?.validationRules ?? null,
|
|
7418
|
+
notifyAgent: notifyEmailAgent ? {
|
|
7419
|
+
systemInstruction: notifyEmailAgent.systemInstruction,
|
|
7420
|
+
validationRules: notifyEmailAgent.validationRules
|
|
7421
|
+
} : null
|
|
7422
|
+
});
|
|
7423
|
+
const emailPlugin2 = cms.getPlugin("email");
|
|
7424
|
+
const convLeadSent = conv.leadEmailSentAt;
|
|
7425
|
+
const chatAgentFn = llm.chatAgent?.bind(llm);
|
|
7426
|
+
console.info(CHAT_EMAIL_LOG, "pipeline check", {
|
|
7427
|
+
conversationId,
|
|
7428
|
+
enabled: emailTool.enabled,
|
|
7429
|
+
intentCount: emailTool.intents.length,
|
|
7430
|
+
chatbotAgentId: agentRow?.id ?? null,
|
|
7431
|
+
chatbotAgentSlug: agentRow?.slug ?? null,
|
|
7432
|
+
notifyEmailAgentId: notifyEmailAgent?.id ?? null,
|
|
7433
|
+
notifyEmailAgentSlug: notifyEmailAgent?.slug ?? null,
|
|
7434
|
+
emailPluginPresent: Boolean(emailPlugin2),
|
|
7435
|
+
chatAgentFnPresent: Boolean(chatAgentFn),
|
|
7436
|
+
leadEmailAlreadySent: Boolean(convLeadSent),
|
|
7437
|
+
mergedPromptIncludesNotify: Boolean(notifyEmailAgent?.systemInstruction?.trim())
|
|
7438
|
+
});
|
|
7439
|
+
if (!emailTool.enabled) {
|
|
7440
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7441
|
+
conversationId,
|
|
7442
|
+
reason: "email_tool_disabled"
|
|
7443
|
+
});
|
|
7444
|
+
} else if (emailTool.intents.length === 0) {
|
|
7445
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7446
|
+
conversationId,
|
|
7447
|
+
reason: "no_intents_configured"
|
|
7448
|
+
});
|
|
7449
|
+
} else if (!emailPlugin2) {
|
|
7450
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7451
|
+
conversationId,
|
|
7452
|
+
reason: "email_plugin_missing"
|
|
7453
|
+
});
|
|
7454
|
+
} else if (!chatAgentFn) {
|
|
7455
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7456
|
+
conversationId,
|
|
7457
|
+
reason: "llm_chat_agent_unavailable"
|
|
7458
|
+
});
|
|
7459
|
+
} else if (convLeadSent) {
|
|
7460
|
+
console.info(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7461
|
+
conversationId,
|
|
7462
|
+
reason: "lead_email_already_sent",
|
|
7463
|
+
leadEmailSentAt: convLeadSent
|
|
7464
|
+
});
|
|
7465
|
+
} else {
|
|
7466
|
+
const contactId = conv.contactId;
|
|
7467
|
+
const contactRow = await contactRepo().findOne({
|
|
7468
|
+
where: { id: contactId }
|
|
7469
|
+
});
|
|
7470
|
+
if (!contactRow) {
|
|
7471
|
+
console.warn(CHAT_EMAIL_LOG, "pipeline skipped", {
|
|
7472
|
+
conversationId,
|
|
7473
|
+
reason: "contact_not_found",
|
|
7474
|
+
contactId
|
|
7475
|
+
});
|
|
7476
|
+
} else {
|
|
7477
|
+
const c = contactRow;
|
|
7478
|
+
try {
|
|
7479
|
+
const intent = await detectChatLeadIntent({ chatAgent: chatAgentFn }, {
|
|
7480
|
+
settings: emailTool,
|
|
7481
|
+
message,
|
|
7482
|
+
history,
|
|
7483
|
+
contact: { name: c.name, email: c.email, phone: c.phone },
|
|
7484
|
+
model: agentRow?.model?.trim() || notifyEmailAgent?.model?.trim() || void 0
|
|
7485
|
+
});
|
|
7486
|
+
if (!intent.intent || !intent.emailTo) {
|
|
7487
|
+
console.info(CHAT_EMAIL_LOG, "no email sent", {
|
|
7488
|
+
conversationId,
|
|
7489
|
+
reason: intent.intent ? "missing_email_to" : "no_intent_match",
|
|
7490
|
+
intentLabel: intent.intentLabel,
|
|
7491
|
+
classifierReason: intent.reason
|
|
7492
|
+
});
|
|
7493
|
+
} else {
|
|
7494
|
+
const intentRecipients = parseIntentRecipientEmails(intent.emailTo);
|
|
7495
|
+
console.info(CHAT_EMAIL_LOG, "intent matched \u2014 sending", {
|
|
7496
|
+
conversationId,
|
|
7497
|
+
intent: intent.intent,
|
|
7498
|
+
emailTo: intent.emailTo,
|
|
7499
|
+
parsedRecipientCount: intentRecipients.length,
|
|
7500
|
+
recipients: intentRecipients
|
|
7501
|
+
});
|
|
7502
|
+
const emailSettings = await loadSettingsGroupMap(dataSource, entityMap, "email");
|
|
7503
|
+
const brandingSettings = await loadSettingsGroupMap(dataSource, entityMap, "branding");
|
|
7504
|
+
const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
|
|
7505
|
+
const leadResult = await sendChatLeadEmail(cms, emailSettings, brandingSettings, {
|
|
7506
|
+
contactName: String(c.name ?? "").trim() || "Visitor",
|
|
7507
|
+
contactEmail: String(c.email ?? "").trim(),
|
|
7508
|
+
contactPhone: c.phone ?? null,
|
|
7509
|
+
conversationId,
|
|
7510
|
+
latestMessage: message,
|
|
7511
|
+
intentCode: intent.intent,
|
|
7512
|
+
intentReason: intent.intentLabel || intent.reason,
|
|
7513
|
+
transcript: buildTranscriptForLeadEmail(history, message),
|
|
7514
|
+
companyDetails,
|
|
7515
|
+
recipients: intentRecipients.length > 0 ? intentRecipients : void 0
|
|
7516
|
+
});
|
|
7517
|
+
if (leadResult.sent) {
|
|
7518
|
+
await convRepo().update(conversationId, {
|
|
7519
|
+
leadEmailSentAt: /* @__PURE__ */ new Date()
|
|
7520
|
+
});
|
|
7521
|
+
console.info(CHAT_EMAIL_LOG, "lead email recorded", {
|
|
7522
|
+
conversationId,
|
|
7523
|
+
sent: true,
|
|
7524
|
+
recipients: leadResult.recipients
|
|
7525
|
+
});
|
|
7526
|
+
} else {
|
|
7527
|
+
console.warn(CHAT_EMAIL_LOG, "lead email not sent", {
|
|
7528
|
+
conversationId,
|
|
7529
|
+
sent: false,
|
|
7530
|
+
error: leadResult.error ?? "unknown",
|
|
7531
|
+
recipients: leadResult.recipients
|
|
7532
|
+
});
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
} catch (intentErr) {
|
|
7536
|
+
console.warn(CHAT_EMAIL_LOG, "pipeline error", {
|
|
7537
|
+
conversationId,
|
|
7538
|
+
error: intentErr instanceof Error ? intentErr.message : String(intentErr)
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7010
7543
|
let content;
|
|
7011
7544
|
const ragContext = contextParts.length > 0 ? contextParts.join("\n\n") : void 0;
|
|
7012
7545
|
console.info(RAG_LOG, "step 7 | final context", {
|
|
@@ -7017,9 +7550,10 @@ function createChatHandlers(config) {
|
|
|
7017
7550
|
});
|
|
7018
7551
|
if (agentRow && llm.chatAgent) {
|
|
7019
7552
|
const fromAgent = llmAgentToChatAgentOptions(agentRow);
|
|
7020
|
-
const systemPrompt =
|
|
7553
|
+
const systemPrompt = buildChatbotSystemPromptWithEmailTool(
|
|
7021
7554
|
fromAgent.systemPrompt,
|
|
7022
|
-
parsedValidation.guardrailsForPrompt
|
|
7555
|
+
parsedValidation.guardrailsForPrompt,
|
|
7556
|
+
emailTool
|
|
7023
7557
|
);
|
|
7024
7558
|
const res = await llm.chatAgent({
|
|
7025
7559
|
...fromAgent,
|
|
@@ -7037,11 +7571,12 @@ ${contextParts.join("\n\n")}` : "";
|
|
|
7037
7571
|
const defaultSystem = "You are a helpful assistant for the company. If you do not have specific information, say so.";
|
|
7038
7572
|
let systemContent;
|
|
7039
7573
|
if (agentRow) {
|
|
7040
|
-
const
|
|
7041
|
-
systemContent =
|
|
7042
|
-
|
|
7043
|
-
parsedValidation.guardrailsForPrompt
|
|
7044
|
-
|
|
7574
|
+
const mergedBase = [agentRow.systemInstruction?.trim(), ragSystem].filter(Boolean).join("\n\n");
|
|
7575
|
+
systemContent = buildChatbotSystemPromptWithEmailTool(
|
|
7576
|
+
mergedBase || defaultSystem,
|
|
7577
|
+
parsedValidation.guardrailsForPrompt,
|
|
7578
|
+
emailTool
|
|
7579
|
+
) || defaultSystem;
|
|
7045
7580
|
} else {
|
|
7046
7581
|
systemContent = ragSystem || defaultSystem;
|
|
7047
7582
|
}
|
|
@@ -7068,6 +7603,154 @@ ${contextParts.join("\n\n")}` : "";
|
|
|
7068
7603
|
};
|
|
7069
7604
|
}
|
|
7070
7605
|
|
|
7606
|
+
// src/plugins/blog-generator/blog-generator-agent-defaults.ts
|
|
7607
|
+
var BLOG_GENERATOR_AGENT_NAME = "Blog Generator Agent";
|
|
7608
|
+
var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
|
|
7609
|
+
var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
|
|
7610
|
+
var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
|
|
7611
|
+
|
|
7612
|
+
Your task is to generate completely original, high-quality blog article(s) from the latest RSS-derived source material in the user message (one block per feed). The user message is factual input only.
|
|
7613
|
+
|
|
7614
|
+
IMPORTANT RULES:
|
|
7615
|
+
|
|
7616
|
+
1. NEVER copy the RSS article directly.
|
|
7617
|
+
2. Rewrite the information into a fresh, human-like article.
|
|
7618
|
+
3. Expand the topic with professional insights and explanations.
|
|
7619
|
+
4. Maintain a natural editorial tone.
|
|
7620
|
+
5. Make the article SEO-friendly.
|
|
7621
|
+
6. Use engaging headings and subheadings.
|
|
7622
|
+
7. Avoid robotic AI phrasing.
|
|
7623
|
+
8. Do not mention that the content came from RSS or feeds.
|
|
7624
|
+
9. Preserve factual accuracy from the source material.
|
|
7625
|
+
10. The output should feel like a professionally written industry blog.
|
|
7626
|
+
|
|
7627
|
+
MULTIPLE FEEDS:
|
|
7628
|
+
- If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
|
|
7629
|
+
- If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
|
|
7630
|
+
- You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
|
|
7631
|
+
|
|
7632
|
+
WRITING STYLE:
|
|
7633
|
+
- Professional
|
|
7634
|
+
- Clear
|
|
7635
|
+
- Informative
|
|
7636
|
+
- Business-focused
|
|
7637
|
+
- Human sounding
|
|
7638
|
+
- Modern editorial style
|
|
7639
|
+
|
|
7640
|
+
ARTICLE STRUCTURE (each article \u2014 express in HTML):
|
|
7641
|
+
1. Engaging introduction
|
|
7642
|
+
2. Industry context
|
|
7643
|
+
3. Main developments
|
|
7644
|
+
4. Key implications
|
|
7645
|
+
5. Expert/business analysis
|
|
7646
|
+
6. Conclusion
|
|
7647
|
+
|
|
7648
|
+
HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
|
|
7649
|
+
- Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
|
|
7650
|
+
- Wrap each complete article in a single root: <article class="blog-post"> ... </article>
|
|
7651
|
+
- Inside <article>, use this outline:
|
|
7652
|
+
- <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
|
|
7653
|
+
- <section class="blog-post-body"> for all following content
|
|
7654
|
+
- Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
|
|
7655
|
+
- Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
|
|
7656
|
+
- Use <ul>/<ol> with <li> for lists where appropriate.
|
|
7657
|
+
- Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
|
|
7658
|
+
- Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
|
|
7659
|
+
- Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
|
|
7660
|
+
- Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
|
|
7661
|
+
|
|
7662
|
+
CONTENT REQUIREMENTS:
|
|
7663
|
+
- Minimum 800 words per article unless source material is very small.
|
|
7664
|
+
- Add context around industry trends where appropriate.
|
|
7665
|
+
- Explain why the topic matters.
|
|
7666
|
+
- Include practical implications for businesses/professionals.
|
|
7667
|
+
|
|
7668
|
+
IF RSS CONTENT IS LIMITED:
|
|
7669
|
+
- Expand intelligently using general industry knowledge.
|
|
7670
|
+
- Keep the article relevant to the original topic.
|
|
7671
|
+
- Do not invent fake facts or statistics.
|
|
7672
|
+
|
|
7673
|
+
OUTPUT FORMAT (STRICT \u2014 HTML only):
|
|
7674
|
+
- Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
|
|
7675
|
+
- If you output more than one article, output multiple <article class="blog-post">\u2026</article> blocks in sequence, and between articles put a single line containing exactly this (and nothing else on that line):
|
|
7676
|
+
${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
|
|
7677
|
+
- If you output a single article, use one <article> only and do not use that separator line.`;
|
|
7678
|
+
var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7679
|
+
{
|
|
7680
|
+
maxUserChars: 5e5,
|
|
7681
|
+
guardrails: `Your reply must be semantic HTML blog article(s) only, as in the system OUTPUT FORMAT (<article class="blog-post">\u2026). No JSON, no Markdown, no explanations before or after the HTML. If multiple articles, separate them with a line containing exactly ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR} alone. Do not mention RSS, feeds, or scraping. Preserve factual accuracy; do not invent statistics or quotes.`
|
|
7682
|
+
},
|
|
7683
|
+
null,
|
|
7684
|
+
2
|
|
7685
|
+
);
|
|
7686
|
+
|
|
7687
|
+
// src/plugins/blog-generator/blog-generator-metadata-defaults.ts
|
|
7688
|
+
var BLOG_METADATA_ENRICHER_AGENT_NAME = "Blog Generator Metadata";
|
|
7689
|
+
var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
|
|
7690
|
+
var BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a careful CMS metadata assistant for a publishing system.
|
|
7691
|
+
|
|
7692
|
+
You receive ONE complete blog post as HTML (title is usually in <h1 class="blog-post-title"> or the first <h1>). Your job is to propose structured fields so an editor can save the post: category, URL slug, SEO block, and topical tags.
|
|
7693
|
+
|
|
7694
|
+
RULES:
|
|
7695
|
+
1. Read the whole article and infer the primary topic and audience.
|
|
7696
|
+
2. categoryName must be EXACTLY one string from AVAILABLE_BLOG_CATEGORIES in the user message, or "" if none fit.
|
|
7697
|
+
3. blogSlug: lowercase kebab-case, ASCII letters/digits/hyphens only, no leading/trailing hyphens, max ~80 chars. Derive from the main topic or title.
|
|
7698
|
+
4. seo.title: concise meta title (~50\u201360 characters when reasonable).
|
|
7699
|
+
5. seo.description: meta description (~150\u2013160 characters when reasonable), plain text.
|
|
7700
|
+
6. seo.keywords: comma-separated phrases, no stuffing.
|
|
7701
|
+
7. seo.ogTitle / seo.ogDescription: may mirror title/description or be slightly adapted for social.
|
|
7702
|
+
8. tags: 4\u201310 short labels (1\u20133 words each). Prefer exact matches from EXISTING_TAG_NAMES when they truly apply; otherwise invent precise new labels (they may be created in the CMS later).
|
|
7703
|
+
9. Do not invent statistics, quotes, or URLs not implied by the article.
|
|
7704
|
+
10. Output JSON only \u2014 no markdown outside the JSON, no commentary.
|
|
7705
|
+
|
|
7706
|
+
OUTPUT FORMAT (STRICT):
|
|
7707
|
+
Return a single JSON object only. Do not wrap it in a markdown code fence.
|
|
7708
|
+
|
|
7709
|
+
{
|
|
7710
|
+
"categoryName": "string \u2014 exact match from list or empty string",
|
|
7711
|
+
"blogSlug": "string \u2014 url-safe kebab-case",
|
|
7712
|
+
"seo": {
|
|
7713
|
+
"title": "string or empty",
|
|
7714
|
+
"description": "string or empty",
|
|
7715
|
+
"keywords": "string or empty",
|
|
7716
|
+
"ogTitle": "string or empty",
|
|
7717
|
+
"ogDescription": "string or empty"
|
|
7718
|
+
},
|
|
7719
|
+
"tags": ["tag-one", "tag-two"]
|
|
7720
|
+
}`;
|
|
7721
|
+
var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7722
|
+
{
|
|
7723
|
+
maxUserChars: 5e5,
|
|
7724
|
+
guardrails: "Reply with one valid JSON object only (see system OUTPUT FORMAT). No markdown fences, no text before or after JSON. Keys: categoryName, blogSlug, seo { title, description, keywords, ogTitle, ogDescription }, tags (array of strings)."
|
|
7725
|
+
},
|
|
7726
|
+
null,
|
|
7727
|
+
2
|
|
7728
|
+
);
|
|
7729
|
+
|
|
7730
|
+
// src/plugins/blog-generator/blog-generator-social-defaults.ts
|
|
7731
|
+
var BLOG_SOCIAL_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION = `You are a social copywriter for LinkedIn-style posts.
|
|
7732
|
+
|
|
7733
|
+
You receive ONE blog post as HTML (title may appear in <h1> or the first heading). Write a short post body that can be published as the main text on LinkedIn: engaging, accurate, no clickbait, no invented facts or quotes.
|
|
7734
|
+
|
|
7735
|
+
RULES:
|
|
7736
|
+
1. Plain text only in the JSON value (no HTML tags in socialMediaContent). Line breaks allowed as \\n if helpful.
|
|
7737
|
+
2. Length: aim for roughly 600\u20131300 characters (hard max 2900 characters) so it fits typical social feeds.
|
|
7738
|
+
3. Summarize the core insight; you may include 1\u20132 short hooks, but stay faithful to the article.
|
|
7739
|
+
4. Do not add URLs unless they appear explicitly in the article.
|
|
7740
|
+
5. Output JSON only \u2014 no markdown fences, no commentary outside JSON.
|
|
7741
|
+
|
|
7742
|
+
OUTPUT FORMAT (STRICT):
|
|
7743
|
+
Return a single JSON object only:
|
|
7744
|
+
{ "socialMediaContent": "string \u2014 the post text" }`;
|
|
7745
|
+
var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7746
|
+
{
|
|
7747
|
+
maxUserChars: 5e5,
|
|
7748
|
+
guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
|
|
7749
|
+
},
|
|
7750
|
+
null,
|
|
7751
|
+
2
|
|
7752
|
+
);
|
|
7753
|
+
|
|
7071
7754
|
// src/plugins/blog-generator/blog-generator-service.ts
|
|
7072
7755
|
var parser = new import_rss_parser.default({
|
|
7073
7756
|
defaultRSS: 2
|
|
@@ -8803,29 +9486,6 @@ async function resolvePublicMetadata(args) {
|
|
|
8803
9486
|
return out;
|
|
8804
9487
|
}
|
|
8805
9488
|
|
|
8806
|
-
// src/lib/email-recipients.ts
|
|
8807
|
-
function parseEmailRecipientsFromConfig(raw) {
|
|
8808
|
-
if (raw == null || raw === "") return [];
|
|
8809
|
-
const trimmed = raw.trim();
|
|
8810
|
-
if (trimmed.startsWith("[")) {
|
|
8811
|
-
try {
|
|
8812
|
-
const parsed = JSON.parse(trimmed);
|
|
8813
|
-
if (Array.isArray(parsed)) {
|
|
8814
|
-
return parsed.map((e) => String(e).trim()).filter(Boolean);
|
|
8815
|
-
}
|
|
8816
|
-
} catch {
|
|
8817
|
-
}
|
|
8818
|
-
}
|
|
8819
|
-
return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
8820
|
-
}
|
|
8821
|
-
function serializeEmailRecipients(emails) {
|
|
8822
|
-
return JSON.stringify(emails);
|
|
8823
|
-
}
|
|
8824
|
-
function joinRecipientsForSend(emails) {
|
|
8825
|
-
if (!emails.length) return null;
|
|
8826
|
-
return emails.join(", ");
|
|
8827
|
-
}
|
|
8828
|
-
|
|
8829
9489
|
// src/lib/otp-challenge.ts
|
|
8830
9490
|
var import_crypto3 = require("crypto");
|
|
8831
9491
|
var import_typeorm8 = require("typeorm");
|
|
@@ -10461,6 +11121,7 @@ var Order = class {
|
|
|
10461
11121
|
id;
|
|
10462
11122
|
vendorId;
|
|
10463
11123
|
orderNumber;
|
|
11124
|
+
qrToken;
|
|
10464
11125
|
orderKind;
|
|
10465
11126
|
parentOrderId;
|
|
10466
11127
|
contactId;
|
|
@@ -10498,6 +11159,9 @@ __decorateClass([
|
|
|
10498
11159
|
__decorateClass([
|
|
10499
11160
|
(0, import_typeorm26.Column)("varchar")
|
|
10500
11161
|
], Order.prototype, "orderNumber", 2);
|
|
11162
|
+
__decorateClass([
|
|
11163
|
+
(0, import_typeorm26.Column)("varchar", { unique: true, nullable: true })
|
|
11164
|
+
], Order.prototype, "qrToken", 2);
|
|
10501
11165
|
__decorateClass([
|
|
10502
11166
|
(0, import_typeorm26.Column)("varchar", { default: "sale" })
|
|
10503
11167
|
], Order.prototype, "orderKind", 2);
|
|
@@ -10726,6 +11390,7 @@ var ChatConversation = class {
|
|
|
10726
11390
|
contactId;
|
|
10727
11391
|
createdAt;
|
|
10728
11392
|
updatedAt;
|
|
11393
|
+
leadEmailSentAt;
|
|
10729
11394
|
contact;
|
|
10730
11395
|
messages;
|
|
10731
11396
|
};
|
|
@@ -10741,6 +11406,9 @@ __decorateClass([
|
|
|
10741
11406
|
__decorateClass([
|
|
10742
11407
|
(0, import_typeorm29.Column)({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
10743
11408
|
], ChatConversation.prototype, "updatedAt", 2);
|
|
11409
|
+
__decorateClass([
|
|
11410
|
+
(0, import_typeorm29.Column)({ type: "timestamp", nullable: true })
|
|
11411
|
+
], ChatConversation.prototype, "leadEmailSentAt", 2);
|
|
10744
11412
|
__decorateClass([
|
|
10745
11413
|
(0, import_typeorm29.ManyToOne)(() => Contact, (c) => c.chatConversations, { onDelete: "CASCADE" }),
|
|
10746
11414
|
(0, import_typeorm29.JoinColumn)({ name: "contactId" })
|
|
@@ -12951,6 +13619,59 @@ async function seedAdministratorPermissions(dataSource, entityMap) {
|
|
|
12951
13619
|
}
|
|
12952
13620
|
}
|
|
12953
13621
|
|
|
13622
|
+
// src/auth/middleware.ts
|
|
13623
|
+
var import_jwt = require("next-auth/jwt");
|
|
13624
|
+
|
|
13625
|
+
// src/auth/auth-debug.ts
|
|
13626
|
+
var LOG_PREFIX2 = "[cms-auth]";
|
|
13627
|
+
function isAuthDebugEnabled() {
|
|
13628
|
+
const v = process.env.CMS_AUTH_DEBUG?.trim().toLowerCase();
|
|
13629
|
+
if (v === "1" || v === "true" || v === "yes") return true;
|
|
13630
|
+
if (v === "0" || v === "false" || v === "no") return false;
|
|
13631
|
+
return process.env.NODE_ENV === "development";
|
|
13632
|
+
}
|
|
13633
|
+
function isAuthDebugClientEnabled() {
|
|
13634
|
+
if (typeof window === "undefined") return false;
|
|
13635
|
+
const v = process.env.NEXT_PUBLIC_CMS_AUTH_DEBUG?.trim().toLowerCase();
|
|
13636
|
+
if (v === "1" || v === "true" || v === "yes") return true;
|
|
13637
|
+
if (v === "0" || v === "false" || v === "no") return false;
|
|
13638
|
+
return process.env.NODE_ENV === "development";
|
|
13639
|
+
}
|
|
13640
|
+
function logAuth(message, data) {
|
|
13641
|
+
if (!isAuthDebugEnabled()) return;
|
|
13642
|
+
if (data) console.info(LOG_PREFIX2, message, data);
|
|
13643
|
+
else console.info(LOG_PREFIX2, message);
|
|
13644
|
+
}
|
|
13645
|
+
function logAuthClient(message, data) {
|
|
13646
|
+
if (!isAuthDebugClientEnabled()) return;
|
|
13647
|
+
if (data) console.info(LOG_PREFIX2, message, data);
|
|
13648
|
+
else console.info(LOG_PREFIX2, message);
|
|
13649
|
+
}
|
|
13650
|
+
function summarizeSessionUserForLog(user) {
|
|
13651
|
+
if (!user || typeof user !== "object") return { present: false };
|
|
13652
|
+
const u = user;
|
|
13653
|
+
return {
|
|
13654
|
+
present: true,
|
|
13655
|
+
email: u.email ?? null,
|
|
13656
|
+
id: u.id ?? null,
|
|
13657
|
+
adminAccess: u.adminAccess ?? null,
|
|
13658
|
+
isRBACAdmin: u.isRBACAdmin ?? null,
|
|
13659
|
+
isVendorPortal: u.isVendorPortal ?? null,
|
|
13660
|
+
groupName: u.groupName ?? null
|
|
13661
|
+
};
|
|
13662
|
+
}
|
|
13663
|
+
function nextAuthCookieDebugInfo() {
|
|
13664
|
+
const nextAuthUrl = process.env.NEXTAUTH_URL ?? "(unset)";
|
|
13665
|
+
const isHttps = nextAuthUrl.startsWith("https");
|
|
13666
|
+
return {
|
|
13667
|
+
nextAuthUrl,
|
|
13668
|
+
hasSecret: Boolean(process.env.NEXTAUTH_SECRET),
|
|
13669
|
+
cookieName: isHttps ? "__Secure-next-auth.session-token" : "next-auth.session-token",
|
|
13670
|
+
cookieSecure: isHttps,
|
|
13671
|
+
nodeEnv: process.env.NODE_ENV ?? "(unset)"
|
|
13672
|
+
};
|
|
13673
|
+
}
|
|
13674
|
+
|
|
12954
13675
|
// src/auth/middleware.ts
|
|
12955
13676
|
var defaultPublicApiMethods = {
|
|
12956
13677
|
"/api/contacts": ["POST"],
|
|
@@ -12963,8 +13684,21 @@ var defaultPublicApiMethods = {
|
|
|
12963
13684
|
"/api/users/set-password": ["POST"],
|
|
12964
13685
|
"/api/users/invite": ["POST"]
|
|
12965
13686
|
};
|
|
12966
|
-
function
|
|
12967
|
-
|
|
13687
|
+
function legacyGetSessionToken(request) {
|
|
13688
|
+
const secure = request.cookies.get("__Secure-next-auth.session-token")?.value;
|
|
13689
|
+
const regular = request.cookies.get("next-auth.session-token")?.value;
|
|
13690
|
+
return secure ?? regular;
|
|
13691
|
+
}
|
|
13692
|
+
function sessionCookiePresence(request) {
|
|
13693
|
+
const secure = request.cookies.get("__Secure-next-auth.session-token")?.value;
|
|
13694
|
+
const regular = request.cookies.get("next-auth.session-token")?.value;
|
|
13695
|
+
const hasChunked = Boolean(request.cookies.get("next-auth.session-token.0")?.value) || Boolean(request.cookies.get("__Secure-next-auth.session-token.0")?.value);
|
|
13696
|
+
return {
|
|
13697
|
+
hasSecureToken: Boolean(secure),
|
|
13698
|
+
hasRegularToken: Boolean(regular),
|
|
13699
|
+
hasChunkedToken: hasChunked,
|
|
13700
|
+
resolved: Boolean(secure ?? regular)
|
|
13701
|
+
};
|
|
12968
13702
|
}
|
|
12969
13703
|
function isPublicMethod2(pathname, method, publicApiMethods) {
|
|
12970
13704
|
for (const [endpoint, methods] of Object.entries(publicApiMethods)) {
|
|
@@ -12972,31 +13706,59 @@ function isPublicMethod2(pathname, method, publicApiMethods) {
|
|
|
12972
13706
|
}
|
|
12973
13707
|
return false;
|
|
12974
13708
|
}
|
|
13709
|
+
async function hasValidSession(request, secret, legacyGetToken) {
|
|
13710
|
+
if (request.req) {
|
|
13711
|
+
const token = await (0, import_jwt.getToken)({ req: request.req, secret });
|
|
13712
|
+
return token != null;
|
|
13713
|
+
}
|
|
13714
|
+
return Boolean(legacyGetToken(request));
|
|
13715
|
+
}
|
|
12975
13716
|
function createCmsMiddleware(config = {}) {
|
|
12976
13717
|
const {
|
|
12977
13718
|
publicAdminPaths = ["/admin/signin", "/admin/forgot-password", "/admin/reset-password", "/admin/invite"],
|
|
12978
13719
|
publicApiMethods = defaultPublicApiMethods,
|
|
12979
13720
|
signInPath = "/admin/signin",
|
|
12980
|
-
getSessionToken =
|
|
13721
|
+
getSessionToken = legacyGetSessionToken,
|
|
13722
|
+
secret = process.env.NEXTAUTH_SECRET
|
|
12981
13723
|
} = config;
|
|
12982
|
-
return function cmsMiddleware(request) {
|
|
13724
|
+
return async function cmsMiddleware(request) {
|
|
12983
13725
|
const pathname = request.nextUrl.pathname;
|
|
12984
13726
|
const method = request.method;
|
|
12985
13727
|
if (publicAdminPaths.some((p) => pathname === p || pathname.startsWith(p + "/"))) {
|
|
12986
13728
|
return { type: "next" };
|
|
12987
13729
|
}
|
|
12988
13730
|
if (pathname.startsWith("/admin")) {
|
|
12989
|
-
const
|
|
12990
|
-
|
|
13731
|
+
const authenticated = await hasValidSession(request, secret, getSessionToken);
|
|
13732
|
+
const cookies = sessionCookiePresence(request);
|
|
13733
|
+
if (!authenticated) {
|
|
13734
|
+
logAuth("middleware: admin redirect to signin (no session)", {
|
|
13735
|
+
pathname,
|
|
13736
|
+
method,
|
|
13737
|
+
usesGetToken: Boolean(request.req),
|
|
13738
|
+
...cookies,
|
|
13739
|
+
expectedCookieName: process.env.NEXTAUTH_URL?.startsWith("https") ? "__Secure-next-auth.session-token" : "next-auth.session-token"
|
|
13740
|
+
});
|
|
12991
13741
|
return { type: "redirect", url: new URL(signInPath, request.url).toString() };
|
|
12992
13742
|
}
|
|
13743
|
+
logAuth("middleware: admin allowed", {
|
|
13744
|
+
pathname,
|
|
13745
|
+
method,
|
|
13746
|
+
usesGetToken: Boolean(request.req),
|
|
13747
|
+
...cookies
|
|
13748
|
+
});
|
|
12993
13749
|
}
|
|
12994
13750
|
if (pathname.startsWith("/api")) {
|
|
12995
13751
|
if (isPublicMethod2(pathname, method, publicApiMethods)) {
|
|
12996
13752
|
return { type: "next" };
|
|
12997
13753
|
}
|
|
12998
|
-
const
|
|
12999
|
-
if (!
|
|
13754
|
+
const authenticated = await hasValidSession(request, secret, getSessionToken);
|
|
13755
|
+
if (!authenticated) {
|
|
13756
|
+
logAuth("middleware: api 401 (no session)", {
|
|
13757
|
+
pathname,
|
|
13758
|
+
method,
|
|
13759
|
+
usesGetToken: Boolean(request.req),
|
|
13760
|
+
...sessionCookiePresence(request)
|
|
13761
|
+
});
|
|
13000
13762
|
return { type: "json", status: 401, body: { error: "Unauthorized" } };
|
|
13001
13763
|
}
|
|
13002
13764
|
}
|
|
@@ -13051,6 +13813,7 @@ function getNextAuthOptions(config) {
|
|
|
13051
13813
|
enableOtpLogin = false,
|
|
13052
13814
|
authorizeOtp
|
|
13053
13815
|
} = config;
|
|
13816
|
+
logAuth("getNextAuthOptions init", nextAuthCookieDebugInfo());
|
|
13054
13817
|
const providers = [];
|
|
13055
13818
|
if (enablePasswordLogin) {
|
|
13056
13819
|
providers.push(
|
|
@@ -13061,15 +13824,43 @@ function getNextAuthOptions(config) {
|
|
|
13061
13824
|
password: { label: "Password", type: "password" }
|
|
13062
13825
|
},
|
|
13063
13826
|
async authorize(credentials) {
|
|
13064
|
-
|
|
13827
|
+
const email = credentials?.email?.trim() ?? "";
|
|
13828
|
+
if (!email || !credentials?.password) {
|
|
13829
|
+
logAuth("authorize(credentials) rejected", { reason: "missing_email_or_password" });
|
|
13830
|
+
return null;
|
|
13831
|
+
}
|
|
13065
13832
|
try {
|
|
13066
|
-
const user = await getUserByEmail(
|
|
13067
|
-
if (!user
|
|
13833
|
+
const user = await getUserByEmail(email);
|
|
13834
|
+
if (!user) {
|
|
13835
|
+
logAuth("authorize(credentials) rejected", { reason: "user_not_found", email });
|
|
13836
|
+
return null;
|
|
13837
|
+
}
|
|
13838
|
+
if (user.blocked) {
|
|
13839
|
+
logAuth("authorize(credentials) rejected", { reason: "blocked", email, userId: user.id });
|
|
13840
|
+
return null;
|
|
13841
|
+
}
|
|
13842
|
+
if (user.deleted) {
|
|
13843
|
+
logAuth("authorize(credentials) rejected", { reason: "deleted", email, userId: user.id });
|
|
13844
|
+
return null;
|
|
13845
|
+
}
|
|
13846
|
+
if (!user.password) {
|
|
13847
|
+
logAuth("authorize(credentials) rejected", { reason: "no_password", email, userId: user.id });
|
|
13848
|
+
return null;
|
|
13849
|
+
}
|
|
13068
13850
|
const valid = await comparePassword(credentials.password, user.password);
|
|
13069
|
-
if (!valid)
|
|
13070
|
-
|
|
13851
|
+
if (!valid) {
|
|
13852
|
+
logAuth("authorize(credentials) rejected", { reason: "invalid_password", email, userId: user.id });
|
|
13853
|
+
return null;
|
|
13854
|
+
}
|
|
13855
|
+
const sessionUser = sessionUserFromNextAuthUser(user);
|
|
13856
|
+
logAuth("authorize(credentials) ok", summarizeSessionUserForLog(sessionUser));
|
|
13857
|
+
return sessionUser;
|
|
13071
13858
|
} catch (err) {
|
|
13072
13859
|
console.error("[cms-auth] authorize error (credentials):", err instanceof Error ? err.message : err);
|
|
13860
|
+
logAuth("authorize(credentials) error", {
|
|
13861
|
+
email,
|
|
13862
|
+
message: err instanceof Error ? err.message : String(err)
|
|
13863
|
+
});
|
|
13073
13864
|
return null;
|
|
13074
13865
|
}
|
|
13075
13866
|
}
|
|
@@ -13122,6 +13913,10 @@ function getNextAuthOptions(config) {
|
|
|
13122
13913
|
callbacks: {
|
|
13123
13914
|
async jwt({ token, user, trigger, session }) {
|
|
13124
13915
|
if (user) {
|
|
13916
|
+
logAuth("jwt callback: new sign-in", {
|
|
13917
|
+
trigger,
|
|
13918
|
+
user: summarizeSessionUserForLog(user)
|
|
13919
|
+
});
|
|
13125
13920
|
const u = user;
|
|
13126
13921
|
token.id = u.id;
|
|
13127
13922
|
token.groupId = u.groupId;
|
|
@@ -13136,6 +13931,7 @@ function getNextAuthOptions(config) {
|
|
|
13136
13931
|
token.isVendorOwner = u.isVendorOwner;
|
|
13137
13932
|
}
|
|
13138
13933
|
if (trigger === "update" && session && typeof session === "object") {
|
|
13934
|
+
logAuth("jwt callback: session update", { trigger });
|
|
13139
13935
|
const s = session;
|
|
13140
13936
|
const t = token;
|
|
13141
13937
|
if (typeof s.name === "string") t.name = s.name;
|
|
@@ -13145,8 +13941,13 @@ function getNextAuthOptions(config) {
|
|
|
13145
13941
|
return token;
|
|
13146
13942
|
},
|
|
13147
13943
|
async session({ session, token }) {
|
|
13944
|
+
const t = token;
|
|
13945
|
+
logAuth("session callback", {
|
|
13946
|
+
tokenHasId: t.id != null,
|
|
13947
|
+
tokenEmail: typeof t.email === "string" ? t.email : null,
|
|
13948
|
+
sessionUser: summarizeSessionUserForLog(session.user)
|
|
13949
|
+
});
|
|
13148
13950
|
if (session.user) {
|
|
13149
|
-
const t = token;
|
|
13150
13951
|
if (typeof t.name === "string") session.user.name = t.name;
|
|
13151
13952
|
if (typeof t.email === "string") session.user.email = t.email;
|
|
13152
13953
|
session.user.id = t.id;
|
|
@@ -13309,6 +14110,26 @@ function validateAndNormalizeAddressRow(row) {
|
|
|
13309
14110
|
return null;
|
|
13310
14111
|
}
|
|
13311
14112
|
|
|
14113
|
+
// src/api/crud.ts
|
|
14114
|
+
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
14115
|
+
|
|
14116
|
+
// src/plugins/llm/llm-agent-scope-crud.ts
|
|
14117
|
+
async function validateLlmAgentScopeForWrite(dataSource, entityMap, scope, excludeId) {
|
|
14118
|
+
if (scope == null || scope === "") return null;
|
|
14119
|
+
if (typeof scope !== "string" || !isLlmAgentScope(scope)) {
|
|
14120
|
+
return `Invalid agent scope. Allowed: chatbot, blog_creation, social_media_post, email_intent_chatbot, blog_metadata.`;
|
|
14121
|
+
}
|
|
14122
|
+
const entity = entityMap.llm_agents;
|
|
14123
|
+
if (!entity) return null;
|
|
14124
|
+
const repo = dataSource.getRepository(entity);
|
|
14125
|
+
const existing = await repo.findOne({
|
|
14126
|
+
where: { scope, deleted: false }
|
|
14127
|
+
});
|
|
14128
|
+
if (!existing) return null;
|
|
14129
|
+
if (excludeId != null && existing.id === excludeId) return null;
|
|
14130
|
+
return `An active agent already uses scope "${scope}".`;
|
|
14131
|
+
}
|
|
14132
|
+
|
|
13312
14133
|
// src/api/crud.ts
|
|
13313
14134
|
var CRUD_LOG = "[cms-crud]";
|
|
13314
14135
|
function logCrudClientError(op, detail) {
|
|
@@ -13525,6 +14346,7 @@ function buildListFilterAndFromSearchParams(repo, searchParams) {
|
|
|
13525
14346
|
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
13526
14347
|
if (!isListStringColumn(col)) continue;
|
|
13527
14348
|
if (Object.prototype.hasOwnProperty.call(and, name)) continue;
|
|
14349
|
+
if (name === "scope") continue;
|
|
13528
14350
|
const raw = searchParams.get(name)?.trim();
|
|
13529
14351
|
if (!raw) continue;
|
|
13530
14352
|
and[name] = (0, import_typeorm61.ILike)(`%${raw}%`);
|
|
@@ -13554,6 +14376,10 @@ function buildExactListParamWhere(repo, searchParams) {
|
|
|
13554
14376
|
extraWhere[name] = raw === "true";
|
|
13555
14377
|
}
|
|
13556
14378
|
}
|
|
14379
|
+
const scopeParam = searchParams.get("scope")?.trim();
|
|
14380
|
+
if (scopeParam && columnNames.has("scope")) {
|
|
14381
|
+
extraWhere.scope = scopeParam;
|
|
14382
|
+
}
|
|
13557
14383
|
return extraWhere;
|
|
13558
14384
|
}
|
|
13559
14385
|
function mergeDeletedFalseWhere(repo, where) {
|
|
@@ -13765,6 +14591,12 @@ function discountEvaluateRule(rule, cartLines, cartTotal) {
|
|
|
13765
14591
|
case "minAmount":
|
|
13766
14592
|
return discountCompare(cartTotal, rule.comparisonOperator, ruleValue);
|
|
13767
14593
|
case "quantity": {
|
|
14594
|
+
if (rule.subType === "productId" && rule.value?.productId) {
|
|
14595
|
+
const targetProductId = Number(rule.value.productId);
|
|
14596
|
+
const requiredQty = Number(rule.value.v);
|
|
14597
|
+
const productQty = cartLines.filter((l) => l.productId === targetProductId).reduce((s, l) => s + l.quantity, 0);
|
|
14598
|
+
return discountCompare(productQty, rule.comparisonOperator, requiredQty);
|
|
14599
|
+
}
|
|
13768
14600
|
const totalQty = cartLines.reduce((s, l) => s + l.quantity, 0);
|
|
13769
14601
|
return discountCompare(totalQty, rule.comparisonOperator, ruleValue);
|
|
13770
14602
|
}
|
|
@@ -13828,6 +14660,10 @@ function discountExtractBuyProductId(nodes) {
|
|
|
13828
14660
|
const n = Number(v);
|
|
13829
14661
|
if (Number.isFinite(n) && n > 0) return n;
|
|
13830
14662
|
}
|
|
14663
|
+
if (node.conditionType === "rule" && node.type === "quantity" && node.subType === "productId") {
|
|
14664
|
+
const n = Number(node.value?.productId);
|
|
14665
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
14666
|
+
}
|
|
13831
14667
|
if (node.children?.length) {
|
|
13832
14668
|
const found = discountExtractBuyProductId(node.children);
|
|
13833
14669
|
if (found !== null) return found;
|
|
@@ -14538,6 +15374,25 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
14538
15374
|
if (pe) return pe;
|
|
14539
15375
|
return null;
|
|
14540
15376
|
}
|
|
15377
|
+
async function tryAssignSingleVendorOnAdminCreate(resource, scope, persistBody) {
|
|
15378
|
+
if (!resourceUsesVendorScope(resource) || scope.type !== "all") return;
|
|
15379
|
+
const currentVendorId = Number(persistBody.vendorId);
|
|
15380
|
+
if (Number.isFinite(currentVendorId) && currentVendorId > 0) return;
|
|
15381
|
+
if (!entityMap.vendors) return;
|
|
15382
|
+
const vendorRepo = dataSource.getRepository(entityMap.vendors);
|
|
15383
|
+
const where = mergeDeletedFalseWhere(vendorRepo, {});
|
|
15384
|
+
const rows = await vendorRepo.find({
|
|
15385
|
+
where,
|
|
15386
|
+
order: { id: "ASC" },
|
|
15387
|
+
take: 2
|
|
15388
|
+
});
|
|
15389
|
+
if (rows.length === 1) {
|
|
15390
|
+
const onlyVendorId = Number(rows[0].id);
|
|
15391
|
+
if (Number.isFinite(onlyVendorId) && onlyVendorId > 0) {
|
|
15392
|
+
persistBody.vendorId = onlyVendorId;
|
|
15393
|
+
}
|
|
15394
|
+
}
|
|
15395
|
+
}
|
|
14541
15396
|
return {
|
|
14542
15397
|
async GET(req, resource) {
|
|
14543
15398
|
const authError = await authz(req, resource, "read");
|
|
@@ -15053,6 +15908,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
15053
15908
|
const couponCode = String(body.couponCode).trim().toUpperCase();
|
|
15054
15909
|
const persistBody2 = pickColumnUpdates(repo2, { ...body, couponCode });
|
|
15055
15910
|
const scopeDiscount = await resolveScope();
|
|
15911
|
+
await tryAssignSingleVendorOnAdminCreate(resource, scopeDiscount, persistBody2);
|
|
15056
15912
|
const vendorIdCheck2 = requireVendorIdForScopedCreate(
|
|
15057
15913
|
resource,
|
|
15058
15914
|
persistBody2,
|
|
@@ -15130,6 +15986,10 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
15130
15986
|
});
|
|
15131
15987
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
15132
15988
|
}
|
|
15989
|
+
if (resource === "llm_agents" && "scope" in persistBody) {
|
|
15990
|
+
const scopeErr = await validateLlmAgentScopeForWrite(dataSource, entityMap, persistBody.scope);
|
|
15991
|
+
if (scopeErr) return json({ error: scopeErr }, { status: 400 });
|
|
15992
|
+
}
|
|
15133
15993
|
if (resource === "products") {
|
|
15134
15994
|
if ("sku" in persistBody) {
|
|
15135
15995
|
const skuNorm = normalizeProductSku(persistBody.sku);
|
|
@@ -15245,6 +16105,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
15245
16105
|
);
|
|
15246
16106
|
}
|
|
15247
16107
|
const scopeCreate = await resolveScope();
|
|
16108
|
+
await tryAssignSingleVendorOnAdminCreate(resource, scopeCreate, persistBody);
|
|
15248
16109
|
const vendorIdCheck = requireVendorIdForScopedCreate(resource, persistBody, scopeCreate, repo, body);
|
|
15249
16110
|
if (!vendorIdCheck.ok) {
|
|
15250
16111
|
return json({ error: vendorIdCheck.error }, { status: vendorIdCheck.status });
|
|
@@ -15287,6 +16148,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
15287
16148
|
const randomPart = Math.floor(1e4 + Math.random() * 9e4);
|
|
15288
16149
|
persistBody.contactId = contact.id;
|
|
15289
16150
|
persistBody.orderNumber = `ORD00${randomPart}`;
|
|
16151
|
+
persistBody.qrToken = import_crypto4.default.randomBytes(16).toString("hex");
|
|
15290
16152
|
}
|
|
15291
16153
|
created = await repo.save(repo.create(persistBody));
|
|
15292
16154
|
if (resource === "orders") {
|
|
@@ -16055,6 +16917,15 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
16055
16917
|
const t = updatePayload.type;
|
|
16056
16918
|
if (t === "" || t === "none" || t == null) updatePayload.type = null;
|
|
16057
16919
|
}
|
|
16920
|
+
if (resource === "llm_agents" && "scope" in updatePayload) {
|
|
16921
|
+
const scopeErr = await validateLlmAgentScopeForWrite(
|
|
16922
|
+
dataSource,
|
|
16923
|
+
entityMap,
|
|
16924
|
+
updatePayload.scope,
|
|
16925
|
+
numericId
|
|
16926
|
+
);
|
|
16927
|
+
if (scopeErr) return json({ error: scopeErr }, { status: 400 });
|
|
16928
|
+
}
|
|
16058
16929
|
if ((resource === "orders" || resource === "payments") && "contactId" in updatePayload && updatePayload.contactId != null && entityMap.vendor_customers) {
|
|
16059
16930
|
const existingRow = await repo.findOne({
|
|
16060
16931
|
where: { id: numericId }
|
|
@@ -16254,8 +17125,8 @@ function createForgotPasswordHandler(config) {
|
|
|
16254
17125
|
const user = await userRepo.findOne({ where: { email }, select: ["email"] });
|
|
16255
17126
|
const msg = "If an account exists with this email, you will receive a reset link shortly.";
|
|
16256
17127
|
if (!user) return json({ message: msg }, { status: 200 });
|
|
16257
|
-
const
|
|
16258
|
-
const token =
|
|
17128
|
+
const crypto4 = await import("crypto");
|
|
17129
|
+
const token = crypto4.randomBytes(32).toString("hex");
|
|
16259
17130
|
const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
|
|
16260
17131
|
const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
|
|
16261
17132
|
await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
|
|
@@ -18193,28 +19064,24 @@ function createCmsApiHandler(config) {
|
|
|
18193
19064
|
let socialLlmAgentResolution = null;
|
|
18194
19065
|
let metadataRow = null;
|
|
18195
19066
|
let socialRow = null;
|
|
19067
|
+
let blogCreationRow = null;
|
|
18196
19068
|
if (entityMap.llm_agents) {
|
|
18197
|
-
|
|
18198
|
-
|
|
18199
|
-
|
|
18200
|
-
});
|
|
18201
|
-
if (agentRow) {
|
|
18202
|
-
const o = llmAgentToChatAgentOptions(agentRow);
|
|
19069
|
+
blogCreationRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_CREATION);
|
|
19070
|
+
if (blogCreationRow) {
|
|
19071
|
+
const o = llmAgentToChatAgentOptions(blogCreationRow);
|
|
18203
19072
|
llmAgentChatOptions = {
|
|
18204
19073
|
model: o.model,
|
|
18205
19074
|
temperature: o.temperature,
|
|
18206
19075
|
max_tokens: o.max_tokens
|
|
18207
19076
|
};
|
|
18208
19077
|
llmAgentResolution = {
|
|
18209
|
-
slug:
|
|
18210
|
-
model:
|
|
18211
|
-
temperature:
|
|
18212
|
-
maxTokens:
|
|
19078
|
+
slug: blogCreationRow.slug,
|
|
19079
|
+
model: blogCreationRow.model?.trim() || null,
|
|
19080
|
+
temperature: blogCreationRow.temperature ?? null,
|
|
19081
|
+
maxTokens: blogCreationRow.maxTokens ?? null
|
|
18213
19082
|
};
|
|
18214
19083
|
}
|
|
18215
|
-
metadataRow = await
|
|
18216
|
-
where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
18217
|
-
});
|
|
19084
|
+
metadataRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_BLOG_METADATA);
|
|
18218
19085
|
if (metadataRow) {
|
|
18219
19086
|
const mo = llmAgentToChatAgentOptions(metadataRow);
|
|
18220
19087
|
metadataLlmAgentChatOptions = {
|
|
@@ -18223,15 +19090,13 @@ function createCmsApiHandler(config) {
|
|
|
18223
19090
|
max_tokens: mo.max_tokens
|
|
18224
19091
|
};
|
|
18225
19092
|
metadataLlmAgentResolution = {
|
|
18226
|
-
slug:
|
|
19093
|
+
slug: metadataRow.slug,
|
|
18227
19094
|
model: metadataRow.model?.trim() || null,
|
|
18228
19095
|
temperature: metadataRow.temperature ?? null,
|
|
18229
19096
|
maxTokens: metadataRow.maxTokens ?? null
|
|
18230
19097
|
};
|
|
18231
19098
|
}
|
|
18232
|
-
socialRow = await
|
|
18233
|
-
where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
18234
|
-
});
|
|
19099
|
+
socialRow = await findLlmAgentByScope(dataSource, entityMap, LLM_AGENT_SCOPE_SOCIAL_MEDIA_POST);
|
|
18235
19100
|
if (socialRow) {
|
|
18236
19101
|
const so = llmAgentToChatAgentOptions(socialRow);
|
|
18237
19102
|
socialLlmAgentChatOptions = {
|
|
@@ -18240,7 +19105,7 @@ function createCmsApiHandler(config) {
|
|
|
18240
19105
|
max_tokens: so.max_tokens
|
|
18241
19106
|
};
|
|
18242
19107
|
socialLlmAgentResolution = {
|
|
18243
|
-
slug:
|
|
19108
|
+
slug: socialRow.slug,
|
|
18244
19109
|
model: socialRow.model?.trim() || null,
|
|
18245
19110
|
temperature: socialRow.temperature ?? null,
|
|
18246
19111
|
maxTokens: socialRow.maxTokens ?? null
|
|
@@ -18264,8 +19129,8 @@ function createCmsApiHandler(config) {
|
|
|
18264
19129
|
const out = await svc.generateBlogMarkdownFromRss({
|
|
18265
19130
|
llm,
|
|
18266
19131
|
rssUrls,
|
|
18267
|
-
systemInstruction,
|
|
18268
|
-
validationRules,
|
|
19132
|
+
systemInstruction: systemInstruction ?? blogCreationRow?.systemInstruction?.trim() ?? void 0,
|
|
19133
|
+
validationRules: validationRules ?? blogCreationRow?.validationRules?.trim() ?? void 0,
|
|
18269
19134
|
categoryNamesHint: categoryRows.map((c) => c.name),
|
|
18270
19135
|
tagNamesHint: tagNames,
|
|
18271
19136
|
llmAgentChatOptions,
|
|
@@ -18750,6 +19615,22 @@ function createCmsApiHandler(config) {
|
|
|
18750
19615
|
return config.json({ error: message }, { status: 500 });
|
|
18751
19616
|
}
|
|
18752
19617
|
}
|
|
19618
|
+
if (path2[0] === "track" && path2.length === 2 && m === "GET") {
|
|
19619
|
+
const token = path2[1];
|
|
19620
|
+
if (!token) return config.json({ error: "Token required" }, { status: 400 });
|
|
19621
|
+
try {
|
|
19622
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
19623
|
+
const order = await orderRepo.findOne({
|
|
19624
|
+
where: { qrToken: token, deleted: false },
|
|
19625
|
+
relations: ["contact", "items", "items.product"]
|
|
19626
|
+
});
|
|
19627
|
+
if (!order) return config.json({ error: "Order not found" }, { status: 404 });
|
|
19628
|
+
return config.json(order);
|
|
19629
|
+
} catch (err) {
|
|
19630
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19631
|
+
return config.json({ error: message }, { status: 500 });
|
|
19632
|
+
}
|
|
19633
|
+
}
|
|
18753
19634
|
if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
18754
19635
|
const resource = resolveResource(path2[0]);
|
|
18755
19636
|
if (!crudResources.includes(resource)) {
|
|
@@ -19618,8 +20499,8 @@ function createStorefrontApiHandler(config) {
|
|
|
19618
20499
|
let emailVerificationSent = false;
|
|
19619
20500
|
if (requireEmailVerification && getCms) {
|
|
19620
20501
|
try {
|
|
19621
|
-
const
|
|
19622
|
-
const rawToken =
|
|
20502
|
+
const crypto4 = await import("crypto");
|
|
20503
|
+
const rawToken = crypto4.randomBytes(32).toString("hex");
|
|
19623
20504
|
const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
|
|
19624
20505
|
await tokenRepo().save(
|
|
19625
20506
|
tokenRepo().create({ email, token: rawToken, expiresAt })
|
|
@@ -20388,6 +21269,8 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
|
|
|
20388
21269
|
hasEntityPermission,
|
|
20389
21270
|
hashOtpCode,
|
|
20390
21271
|
hydrateVendorSessionUser,
|
|
21272
|
+
isAuthDebugClientEnabled,
|
|
21273
|
+
isAuthDebugEnabled,
|
|
20391
21274
|
isCustomerTypeContact,
|
|
20392
21275
|
isOpenEndpoint,
|
|
20393
21276
|
isPlatformAdministrator,
|
|
@@ -20406,6 +21289,8 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
|
|
|
20406
21289
|
loadPublicThemeSettings,
|
|
20407
21290
|
loadUserVendorContext,
|
|
20408
21291
|
localStoragePlugin,
|
|
21292
|
+
logAuth,
|
|
21293
|
+
logAuthClient,
|
|
20409
21294
|
logEntityAccessDecision,
|
|
20410
21295
|
logRbac,
|
|
20411
21296
|
mergeEmailLayoutCompanyDetails,
|
|
@@ -20415,6 +21300,7 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
|
|
|
20415
21300
|
metaPostPageFeed,
|
|
20416
21301
|
metaPostPagePhoto,
|
|
20417
21302
|
metaResolvePageAccessToken,
|
|
21303
|
+
nextAuthCookieDebugInfo,
|
|
20418
21304
|
normalizePhoneE164,
|
|
20419
21305
|
parseBlogGeneratorAgentContent,
|
|
20420
21306
|
parseBlogGeneratorModelOutput,
|
|
@@ -20462,6 +21348,7 @@ console.log("\u{1F525} USING LOCAL CMS CORE(index.ts loaded) \u{1F525}");
|
|
|
20462
21348
|
smsPlugin,
|
|
20463
21349
|
socialMediaPlugin,
|
|
20464
21350
|
summarizeEntityPerms,
|
|
21351
|
+
summarizeSessionUserForLog,
|
|
20465
21352
|
syncJobScheduleToPgBoss,
|
|
20466
21353
|
truncateText,
|
|
20467
21354
|
validateScheduleInput,
|