feat: adds aquisition extension to cms
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Some checks failed
Monorepo Pipeline / 🧪 Quality Assurance (push) Failing after 11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
50
packages/cms-infra/extensions/acquisition/build.js
Normal file
50
packages/cms-infra/extensions/acquisition/build.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { build } from 'esbuild';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const entryPoint = resolve(__dirname, 'src/index.ts');
|
||||
const outfile = resolve(__dirname, 'dist/index.js');
|
||||
|
||||
try {
|
||||
mkdirSync(dirname(outfile), { recursive: true });
|
||||
} catch (e) { }
|
||||
|
||||
console.log(`Building from ${entryPoint} to ${outfile}...`);
|
||||
|
||||
build({
|
||||
entryPoints: [entryPoint],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
outfile: outfile,
|
||||
format: 'esm',
|
||||
// Bundle everything, including Directus SDK, to avoid resolution issues in Docker
|
||||
external: [],
|
||||
plugins: [{
|
||||
name: 'mock-jquery',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^jquery$/ }, args => ({ path: args.path, namespace: 'mock-jquery' }));
|
||||
build.onLoad({ filter: /.*/, namespace: 'mock-jquery' }, () => ({ contents: 'export default {};', loader: 'js' }));
|
||||
}
|
||||
}, {
|
||||
name: 'mock-canvas',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^canvas$/ }, args => ({ path: args.path, namespace: 'mock-canvas' }));
|
||||
build.onLoad({ filter: /.*/, namespace: 'mock-canvas' }, () => ({ contents: 'export default {};', loader: 'js' }));
|
||||
}
|
||||
}, {
|
||||
name: 'mock-jsdom',
|
||||
setup(build) {
|
||||
return;
|
||||
}
|
||||
}]
|
||||
}).then(() => {
|
||||
console.log("Build succeeded!");
|
||||
}).catch((e) => {
|
||||
console.error("Build failed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
513
packages/cms-infra/extensions/acquisition/index.js
Normal file
513
packages/cms-infra/extensions/acquisition/index.js
Normal file
File diff suppressed because one or more lines are too long
27
packages/cms-infra/extensions/acquisition/package.json
Normal file
27
packages/cms-infra/extensions/acquisition/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "acquisition",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"directus:extension": {
|
||||
"type": "endpoint",
|
||||
"path": "dist/index.js",
|
||||
"source": "src/index.ts",
|
||||
"host": "^11.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"dev": "node build.js --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@directus/extensions-sdk": "11.0.2",
|
||||
"@mintel/acquisition": "workspace:*",
|
||||
"@mintel/mail": "workspace:*",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"jquery": "^3.7.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
}
|
||||
}
|
||||
172
packages/cms-infra/extensions/acquisition/src/index.ts
Normal file
172
packages/cms-infra/extensions/acquisition/src/index.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import "./shim";
|
||||
import { defineEndpoint } from "@directus/extensions-sdk";
|
||||
import { AcquisitionService, PdfEngine } from "@mintel/acquisition";
|
||||
import { render, SiteAuditTemplate, ProjectEstimateTemplate } from "@mintel/mail";
|
||||
import { createElement } from "react";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
export default defineEndpoint((router, { services, env }) => {
|
||||
const { ItemsService, MailService } = services;
|
||||
|
||||
router.get("/ping", (req, res) => res.send("pong"));
|
||||
|
||||
router.post("/audit/:id", async (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
|
||||
|
||||
try {
|
||||
const lead = await leadsService.readOne(id);
|
||||
if (!lead) return res.status(404).send({ error: "Lead not found" });
|
||||
|
||||
await leadsService.updateOne(id, { status: "auditing" });
|
||||
|
||||
const acqService = new AcquisitionService(env.OPENROUTER_API_KEY);
|
||||
const result = await acqService.runFullSequence(lead.website_url, lead.briefing, lead.comments);
|
||||
|
||||
await leadsService.updateOne(id, {
|
||||
status: "audit_ready",
|
||||
ai_state: result.state,
|
||||
audit_context: JSON.stringify(result.usage),
|
||||
});
|
||||
|
||||
res.send({ success: true, result });
|
||||
} catch (error: any) {
|
||||
console.error("Audit failed:", error);
|
||||
await leadsService.updateOne(id, { status: "new", comments: `Audit failed: ${error.message}` });
|
||||
res.status(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/audit-email/:id", async (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
|
||||
const peopleService = new ItemsService("people", { schema: req.schema, accountability: req.accountability });
|
||||
const mailService = new MailService({ schema: req.schema, accountability: req.accountability });
|
||||
|
||||
try {
|
||||
const lead = await leadsService.readOne(id);
|
||||
if (!lead || !lead.ai_state) return res.status(400).send({ error: "Lead or Audit not ready" });
|
||||
|
||||
let recipientEmail = lead.contact_email;
|
||||
let companyName = lead.company_name;
|
||||
|
||||
if (lead.contact_person) {
|
||||
const person = await peopleService.readOne(lead.contact_person);
|
||||
if (person && person.email) {
|
||||
recipientEmail = person.email;
|
||||
companyName = person.company || lead.company_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipientEmail) return res.status(400).send({ error: "No recipient email found" });
|
||||
|
||||
const auditHighlights = [
|
||||
`Projekt-Typ: ${lead.ai_state.projectType === "website" ? "Website" : "Web App"}`,
|
||||
...(lead.ai_state.sitemap || []).slice(0, 3).map((item: any) => `Potenzial in: ${item.category}`),
|
||||
];
|
||||
|
||||
const html = await render(createElement(SiteAuditTemplate, {
|
||||
companyName: companyName,
|
||||
websiteUrl: lead.website_url,
|
||||
auditHighlights
|
||||
}));
|
||||
|
||||
await mailService.send({
|
||||
to: recipientEmail,
|
||||
subject: `Analyse Ihrer Webpräsenz: ${companyName}`,
|
||||
html
|
||||
});
|
||||
|
||||
await leadsService.updateOne(id, {
|
||||
status: "contacted",
|
||||
last_contacted_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error("Audit Email failed:", error);
|
||||
res.status(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/estimate/:id", async (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
|
||||
|
||||
try {
|
||||
const lead = await leadsService.readOne(id);
|
||||
if (!lead || !lead.ai_state) return res.status(400).send({ error: "Lead or AI state not found" });
|
||||
|
||||
const pdfEngine = new PdfEngine();
|
||||
const filename = `estimate_${id}_${Date.now()}.pdf`;
|
||||
const storageRoot = env.STORAGE_LOCAL_ROOT || "./storage";
|
||||
const outputPath = path.join(storageRoot, filename);
|
||||
|
||||
await pdfEngine.generateEstimatePdf(lead.ai_state, outputPath);
|
||||
|
||||
await leadsService.updateOne(id, {
|
||||
audit_pdf_path: filename,
|
||||
});
|
||||
|
||||
res.send({ success: true, filename });
|
||||
} catch (error: any) {
|
||||
console.error("PDF Generation failed:", error);
|
||||
res.status(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/estimate-email/:id", async (req: any, res: any) => {
|
||||
const { id } = req.params;
|
||||
const leadsService = new ItemsService("leads", { schema: req.schema, accountability: req.accountability });
|
||||
const peopleService = new ItemsService("people", { schema: req.schema, accountability: req.accountability });
|
||||
const mailService = new MailService({ schema: req.schema, accountability: req.accountability });
|
||||
|
||||
try {
|
||||
const lead = await leadsService.readOne(id);
|
||||
if (!lead || !lead.audit_pdf_path) return res.status(400).send({ error: "PDF not generated" });
|
||||
|
||||
let recipientEmail = lead.contact_email;
|
||||
let companyName = lead.company_name;
|
||||
|
||||
if (lead.contact_person) {
|
||||
const person = await peopleService.readOne(lead.contact_person);
|
||||
if (person && person.email) {
|
||||
recipientEmail = person.email;
|
||||
companyName = person.company || lead.company_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipientEmail) return res.status(400).send({ error: "No recipient email found" });
|
||||
|
||||
const html = await render(createElement(ProjectEstimateTemplate, {
|
||||
companyName: companyName,
|
||||
}));
|
||||
|
||||
const storageRoot = env.STORAGE_LOCAL_ROOT || "./storage";
|
||||
const attachmentPath = path.join(storageRoot, lead.audit_pdf_path);
|
||||
|
||||
await mailService.send({
|
||||
to: recipientEmail,
|
||||
subject: `Ihre Projekt-Schätzung: ${companyName}`,
|
||||
html,
|
||||
attachments: [
|
||||
{
|
||||
filename: `Angebot_${companyName}.pdf`,
|
||||
content: fs.readFileSync(attachmentPath)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await leadsService.updateOne(id, {
|
||||
status: "contacted",
|
||||
last_contacted_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.send({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error("Estimate Email failed:", error);
|
||||
res.status(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
22
packages/cms-infra/extensions/acquisition/src/shim.ts
Normal file
22
packages/cms-infra/extensions/acquisition/src/shim.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
try {
|
||||
const url = import.meta?.url;
|
||||
// Hardcode fallback path for Directus Docker environment
|
||||
const fallbackPath = '/directus/extensions/acquisition/dist/index.js';
|
||||
const filename = url ? fileURLToPath(url) : fallbackPath;
|
||||
const dir = dirname(filename);
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.__filename = filename;
|
||||
// @ts-ignore
|
||||
globalThis.__dirname = dir;
|
||||
// @ts-ignore
|
||||
globalThis.require = createRequire(url || `file://${fallbackPath}`);
|
||||
|
||||
console.log(`[Shim] Loaded. __dirname: ${dir}`);
|
||||
} catch (e) {
|
||||
console.warn("[Shim] Failed to shim __dirname/require", e);
|
||||
}
|
||||
Reference in New Issue
Block a user