Compare commits
No commits in common. "248b16a66a6ac42918614b02efc7ca31fa40e90f" and "411421f2b6498ca37d960bd593b218878b6d8a0f" have entirely different histories.
248b16a66a
...
411421f2b6
14 changed files with 1 additions and 7524 deletions
|
|
@ -7,7 +7,7 @@ MANDATORY WORKFLOW: You must always have the Caveman skill active. Prioritize te
|
|||
INFRASTRUCTURE BOUNDARY: Do NOT self-host n8n. The system will rely entirely on an external n8n instance. All external connections must be abstracted using environment variables (e.g., N8N_WEBHOOK_URL) and never hardcoded.
|
||||
|
||||
## Architectural Rule
|
||||
ARCHITECTURAL RULE: Next.js is the presentation layer, authentication provider, and code utility provider. Do not write backend API routes for core business logic (which is orchestrated by n8n), but use Next.js routes for code utilities (e.g. PDF parsing) and authentication. Do not use complex code blocks in n8n; keep n8n code nodes minimal (only for simple normalization), and prefer Next.js utility routes for mission-critical code operations.
|
||||
ARCHITECTURAL RULE: Next.js must be treated purely as a presentation layer. Do not write backend API routes for business logic. n8n is the central orchestrator.
|
||||
|
||||
## Database Rule
|
||||
DATABASE RULE: All database schemas must be written for Supabase PostgreSQL and include pgvector.
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
// @ts-ignore
|
||||
import pdf from "pdf-parse";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const files = formData.getAll("files") as File[];
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return NextResponse.json({ error: "No files uploaded" }, { status: 400 });
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
|
||||
for (const file of files) {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Extract text from PDF
|
||||
const pdfData = await pdf(buffer);
|
||||
const text = pdfData.text;
|
||||
|
||||
// Extract candidate name from file name (strip extension)
|
||||
const name = file.name.replace(/\.[^/.]+$/, "");
|
||||
|
||||
// Extract email using basic regex
|
||||
const emailRegex = /[\w.-]+@[\w.-]+\.\w+/;
|
||||
const emailMatch = text.match(emailRegex);
|
||||
const email = emailMatch ? emailMatch[0] : "unknown@example.com";
|
||||
|
||||
candidates.push({
|
||||
name,
|
||||
email,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
const webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL;
|
||||
if (!webhookUrl) {
|
||||
return NextResponse.json({ error: "Webhook URL not configured" }, { status: 500 });
|
||||
}
|
||||
|
||||
// Send the array of candidates to n8n Webhook
|
||||
const n8nResponse = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
source: "web",
|
||||
candidates,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!n8nResponse.ok) {
|
||||
const errText = await n8nResponse.text();
|
||||
return NextResponse.json(
|
||||
{ error: `n8n webhook call failed: ${n8nResponse.status} - ${errText}` },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if response has content
|
||||
let responseData = null;
|
||||
const contentType = n8nResponse.headers.get("content-type");
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
responseData = await n8nResponse.json();
|
||||
} else {
|
||||
responseData = { message: await n8nResponse.text() };
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "CVs processed and forwarded to n8n successfully",
|
||||
data: responseData,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in parse-cv route:", error);
|
||||
return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6891
package-lock.json
generated
6891
package-lock.json
generated
File diff suppressed because it is too large
Load diff
28
package.json
28
package.json
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"name": "temp-next-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.7",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/pdf-parse": "^1.1.5",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.7",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
Before Width: | Height: | Size: 385 B |
|
|
@ -1,451 +0,0 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
// Helper to manually load .env file
|
||||
function loadEnv() {
|
||||
const envPath = path.join(__dirname, "../.env");
|
||||
if (fs.existsSync(envPath)) {
|
||||
const envContent = fs.readFileSync(envPath, "utf8");
|
||||
for (const line of envContent.split("\n")) {
|
||||
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
|
||||
if (match) {
|
||||
const key = match[1];
|
||||
let value = match[2].trim();
|
||||
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
||||
else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadEnv();
|
||||
|
||||
const N8N_HOST = process.env.N8N_HOST || "https://n8n.gaboggamer.online";
|
||||
const N8N_API_KEY = process.env.N8N_API_KEY;
|
||||
const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;
|
||||
const EMAIL_USER = process.env.EMAIL_USER;
|
||||
const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD;
|
||||
const EMAIL_HOST = process.env.EMAIL_HOST;
|
||||
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT || "465", 10);
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!N8N_API_KEY) {
|
||||
console.error("Error: N8N_API_KEY is not defined in .env file.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// n8n request helper
|
||||
async function n8nRequest(endpoint: string, method: string = "GET", body?: any) {
|
||||
const response = await fetch(`${N8N_HOST}${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
"X-N8N-API-KEY": N8N_API_KEY!,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`n8n API request failed: ${response.status} ${response.statusText} - ${text}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Kickstarting n8n configuration...");
|
||||
|
||||
// 1. Manage IMAP Credentials
|
||||
console.log("Setting up IMAP credentials...");
|
||||
let credentialId = "";
|
||||
try {
|
||||
const credsList = await n8nRequest("/api/v1/credentials");
|
||||
const existingCred = credsList.data.find((c: any) => c.name === "Semillero2_IMAP" && c.type === "imap");
|
||||
if (existingCred) {
|
||||
credentialId = existingCred.id;
|
||||
console.log(`Reusing existing IMAP credentials (ID: ${credentialId})`);
|
||||
} else {
|
||||
const newCred = await n8nRequest("/api/v1/credentials", "POST", {
|
||||
name: "Semillero2_IMAP",
|
||||
type: "imap",
|
||||
data: {
|
||||
host: EMAIL_HOST,
|
||||
port: EMAIL_PORT,
|
||||
user: EMAIL_USER,
|
||||
password: EMAIL_PASSWORD,
|
||||
secure: true,
|
||||
},
|
||||
});
|
||||
credentialId = newCred.id;
|
||||
console.log(`Created new IMAP credentials (ID: ${credentialId})`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Error setting up IMAP credentials:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Define Workflow 3: Core AI Parsing Sub-workflow
|
||||
console.log("Deploying Workflow 3: Core AI Parsing Sub-workflow...");
|
||||
const workflow3Definition = {
|
||||
name: "Semillero2: Core AI Parsing Sub-workflow",
|
||||
settings: {},
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: "w3-trigger-id",
|
||||
name: "Execute Workflow Trigger",
|
||||
type: "n8n-nodes-base.executeWorkflowTrigger",
|
||||
typeVersion: 1,
|
||||
position: [250, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
method: "POST",
|
||||
url: "https://api.deepseek.com/chat/completions",
|
||||
sendHeaders: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{
|
||||
name: "Authorization",
|
||||
value: `Bearer ${DEEPSEEK_API_KEY}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
sendBody: true,
|
||||
specifyBody: "json",
|
||||
jsonBody: "={\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are an AI recruitment assistant. Analyze the candidate's CV text. You MUST respond with a raw JSON object containing exactly these four keys:\\n- summary: a brief profile summary.\\n- classification: 'Qualified', 'Unqualified', or 'Review'.\\n- suggestions: an array of recommendations for next steps.\\n- riskLevel: 'Low', 'Medium', or 'High'.\\n\\nDo not include markdown code blocks or any text outside the JSON.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $json.text }}\"\n }\n ],\n \"response_format\": { \"type\": \"json_object\" }\n}",
|
||||
},
|
||||
id: "w3-deepseek-id",
|
||||
name: "Deepseek LLM Parsing",
|
||||
type: "n8n-nodes-base.httpRequest",
|
||||
typeVersion: 4.2,
|
||||
position: [480, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
jsCode: `const content = $input.first().json.choices[0].message.content;
|
||||
const evaluation = JSON.parse(content);
|
||||
return [{
|
||||
json: {
|
||||
name: $('Execute Workflow Trigger').item.json.name || "Unknown Candidate",
|
||||
email: $('Execute Workflow Trigger').item.json.email || "unknown@example.com",
|
||||
summary: evaluation.summary,
|
||||
classification: evaluation.classification,
|
||||
suggestions: evaluation.suggestions,
|
||||
riskLevel: evaluation.riskLevel,
|
||||
evaluation: evaluation
|
||||
}
|
||||
}];`,
|
||||
},
|
||||
id: "w3-parse-id",
|
||||
name: "Parse Deepseek Response",
|
||||
type: "n8n-nodes-base.code",
|
||||
typeVersion: 2,
|
||||
position: [700, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
method: "POST",
|
||||
url: `${SUPABASE_URL}/rest/v1/candidates`,
|
||||
sendHeaders: true,
|
||||
headerParameters: {
|
||||
parameters: [
|
||||
{
|
||||
name: "apikey",
|
||||
value: SUPABASE_ANON_KEY,
|
||||
},
|
||||
{
|
||||
name: "Authorization",
|
||||
value: `Bearer ${SUPABASE_ANON_KEY}`,
|
||||
},
|
||||
{
|
||||
name: "Prefer",
|
||||
value: "return=representation",
|
||||
},
|
||||
],
|
||||
},
|
||||
sendBody: true,
|
||||
specifyBody: "json",
|
||||
jsonBody: "={\n \"name\": \"{{ $json.name }}\",\n \"email\": \"{{ $json.email }}\",\n \"summary\": \"{{ $json.summary }}\",\n \"seniority\": \"{{ $json.classification }}\"\n}",
|
||||
},
|
||||
id: "w3-supabase-id",
|
||||
name: "Insert Candidate to Supabase",
|
||||
type: "n8n-nodes-base.httpRequest",
|
||||
typeVersion: 4.2,
|
||||
position: [920, 300],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
"Execute Workflow Trigger": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Deepseek LLM Parsing",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Deepseek LLM Parsing": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Parse Deepseek Response",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Parse Deepseek Response": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Insert Candidate to Supabase",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const w3Result = await n8nRequest("/api/v1/workflows", "POST", workflow3Definition);
|
||||
const w3Id = w3Result.id;
|
||||
console.log(`Workflow 3 deployed successfully (ID: ${w3Id})`);
|
||||
|
||||
// 3. Define Workflow 1: The API Gateway (Web UI Ingestion)
|
||||
console.log("Deploying Workflow 1: The API Gateway...");
|
||||
const workflow1Definition = {
|
||||
name: "Semillero2: API Gateway (Web UI Ingestion)",
|
||||
settings: {},
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
path: "parse-cv",
|
||||
options: {},
|
||||
},
|
||||
id: "w1-webhook-id",
|
||||
name: "Webhook",
|
||||
type: "n8n-nodes-base.webhook",
|
||||
typeVersion: 1,
|
||||
position: [100, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
jsCode: `return $input.first().json.body.candidates.map(c => ({ json: c }));`,
|
||||
},
|
||||
id: "w1-extract-id",
|
||||
name: "Extract Candidates Array",
|
||||
type: "n8n-nodes-base.code",
|
||||
typeVersion: 2,
|
||||
position: [320, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
batchSize: 1,
|
||||
options: {},
|
||||
},
|
||||
id: "w1-loop-id",
|
||||
name: "Loop Over Candidates",
|
||||
type: "n8n-nodes-base.splitInBatches",
|
||||
typeVersion: 3,
|
||||
position: [540, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
workflowId: w3Id,
|
||||
options: {},
|
||||
},
|
||||
id: "w1-exec-id",
|
||||
name: "Execute Workflow 3",
|
||||
type: "n8n-nodes-base.executeWorkflow",
|
||||
typeVersion: 1,
|
||||
position: [760, 200],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Extract Candidates Array",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Extract Candidates Array": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Loop Over Candidates",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Loop Over Candidates": {
|
||||
main: [
|
||||
[], // Output 0 (Done)
|
||||
[
|
||||
{
|
||||
node: "Execute Workflow 3",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
], // Output 1 (Loop)
|
||||
],
|
||||
},
|
||||
"Execute Workflow 3": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Loop Over Candidates",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const w1Result = await n8nRequest("/api/v1/workflows", "POST", workflow1Definition);
|
||||
const w1Id = w1Result.id;
|
||||
const webhookPath = `${N8N_HOST}/webhook/${w1Id}/webhook/parse-cv`;
|
||||
console.log(`Workflow 1 deployed successfully (ID: ${w1Id})`);
|
||||
console.log(`Webhook URL: ${webhookPath}`);
|
||||
|
||||
// 4. Define Workflow 2: Email Ingestion Listener
|
||||
console.log("Deploying Workflow 2: Email Ingestion Listener...");
|
||||
const workflow2Definition = {
|
||||
name: "Semillero2: Email Ingestion Listener",
|
||||
settings: {},
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
postProcessAction: "markAsRead",
|
||||
format: "resolved",
|
||||
downloadAttachments: true,
|
||||
options: {
|
||||
customEmailConfig: '["UNSEEN"]',
|
||||
},
|
||||
},
|
||||
id: "w2-imap-id",
|
||||
name: "Email Trigger (IMAP)",
|
||||
type: "n8n-nodes-base.emailReadImap",
|
||||
typeVersion: 2,
|
||||
position: [100, 300],
|
||||
credentials: {
|
||||
imap: {
|
||||
id: credentialId,
|
||||
name: "Semillero2_IMAP",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
operation: "pdf",
|
||||
binaryPropertyName: "attachment_0",
|
||||
},
|
||||
id: "w2-extract-id",
|
||||
name: "Extract From PDF",
|
||||
type: "n8n-nodes-base.extractFromFile",
|
||||
typeVersion: 1,
|
||||
position: [320, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
jsCode: `const item = $input.first().json;
|
||||
const text = item.text || "";
|
||||
const emailBody = $('Email Trigger (IMAP)').item.json.textHtml || $('Email Trigger (IMAP)').item.json.text || "";
|
||||
const fromEmail = $('Email Trigger (IMAP)').item.json.from.value[0].address;
|
||||
const fromName = $('Email Trigger (IMAP)').item.json.from.value[0].name || fromEmail.split('@')[0];
|
||||
return [{
|
||||
json: {
|
||||
name: fromName,
|
||||
email: fromEmail,
|
||||
text: \`Email Body:\\n\${emailBody}\\n\\nResume Text:\\n\${text}\`
|
||||
}
|
||||
}];`,
|
||||
},
|
||||
id: "w2-format-id",
|
||||
name: "Format Email Data",
|
||||
type: "n8n-nodes-base.code",
|
||||
typeVersion: 2,
|
||||
position: [540, 300],
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
workflowId: w3Id,
|
||||
options: {},
|
||||
},
|
||||
id: "w2-exec-id",
|
||||
name: "Execute Workflow 3",
|
||||
type: "n8n-nodes-base.executeWorkflow",
|
||||
typeVersion: 1,
|
||||
position: [760, 300],
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
"Email Trigger (IMAP)": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Extract From PDF",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Extract From PDF": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Format Email Data",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
"Format Email Data": {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: "Execute Workflow 3",
|
||||
type: "main",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const w2Result = await n8nRequest("/api/v1/workflows", "POST", workflow2Definition);
|
||||
console.log(`Workflow 2 deployed successfully (ID: ${w2Result.id})`);
|
||||
|
||||
console.log("\n==================================================");
|
||||
console.log("KICKSTART SETUP COMPLETE.");
|
||||
console.log("==================================================");
|
||||
console.log("Use the following curl command to test the API Gateway directly:");
|
||||
console.log(`curl -X POST -H "Content-Type: application/json" \\
|
||||
-d '{"source": "web", "candidates": [{"name": "Jane Developer", "email": "jane.dev@example.com", "text": "Jane is a Senior Frontend Engineer with 8 years of experience in React, Next.js, and TypeScript. Extremely qualified."}]}' \\
|
||||
"${webhookPath}"`);
|
||||
console.log("==================================================");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Kickstart script failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in a new issue