feat(deploy): make deploy tool non-interactive with cli flags
This commit is contained in:
parent
fb4611a4e0
commit
d74cdc445e
8 changed files with 344 additions and 94 deletions
|
|
@ -88,9 +88,15 @@ export async function POST(request: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
const isTest = formData.get("isTest") === "true";
|
||||
|
||||
// Call n8n webhook
|
||||
let n8nResponseData = null;
|
||||
const webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL;
|
||||
let webhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL;
|
||||
if (isTest && webhookUrl) {
|
||||
webhookUrl = webhookUrl.replace("/webhook/", "/webhook-test/");
|
||||
}
|
||||
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
const n8nResponse = await fetch(webhookUrl, {
|
||||
|
|
|
|||
145
app/webhook-test/route.ts
Normal file
145
app/webhook-test/route.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase";
|
||||
import { generateEmbedding } from "@/lib/embeddings";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const supabase = createServerSupabaseClient();
|
||||
const origin = request.nextUrl.origin;
|
||||
|
||||
// 1. Fetch or create a Job Vacancy for testing
|
||||
let job = null;
|
||||
const { data: existingJobs, error: jobsFetchError } = await supabase
|
||||
.from("jobs")
|
||||
.select("*")
|
||||
.limit(1);
|
||||
|
||||
if (jobsFetchError) {
|
||||
return NextResponse.json({ error: `Failed to fetch jobs: ${jobsFetchError.message}` }, { status: 500 });
|
||||
}
|
||||
|
||||
if (existingJobs && existingJobs.length > 0) {
|
||||
job = existingJobs[0];
|
||||
} else {
|
||||
// Create a default job if none exist
|
||||
const title = "Senior AI Research Engineer";
|
||||
const requirementsText = "We are seeking a Senior AI Research Engineer with expert knowledge in Large Language Models, PyTorch, LangChain, and agentic reasoning architectures.";
|
||||
const embedding = await generateEmbedding(requirementsText);
|
||||
|
||||
const { data: newJob, error: jobInsertError } = await supabase
|
||||
.from("jobs")
|
||||
.insert({
|
||||
title,
|
||||
requirements: { text: requirementsText },
|
||||
embedding,
|
||||
})
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
if (jobInsertError || !newJob) {
|
||||
return NextResponse.json({ error: `Failed to create mock job: ${jobInsertError?.message}` }, { status: 500 });
|
||||
}
|
||||
job = newJob;
|
||||
}
|
||||
|
||||
// 2. Read the Curriculum Vitae test asset
|
||||
const cvPath = path.join(process.cwd(), "test-assets", "curriculum-vitae-english.pdf");
|
||||
let fileBuffer;
|
||||
try {
|
||||
fileBuffer = await fs.readFile(cvPath);
|
||||
} catch (fsError) {
|
||||
return NextResponse.json({
|
||||
error: `Could not read test CV asset at ${cvPath}. Please ensure it exists. Detailed error: ${fsError instanceof Error ? fsError.message : fsError}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const testMode = request.nextUrl.searchParams.get("testMode") !== "false";
|
||||
|
||||
// 3. Prepare Form Data for parse-cv endpoint
|
||||
const formData = new FormData();
|
||||
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
|
||||
formData.append("file", fileBlob, "curriculum-vitae-english.pdf");
|
||||
formData.append("jobId", job.id);
|
||||
formData.append("isTest", testMode ? "true" : "false");
|
||||
|
||||
// 4. Send POST request to local parse-cv API
|
||||
const parseCvUrl = `${origin}/candidates/api/parse-cv`;
|
||||
let parseResult;
|
||||
try {
|
||||
const parseResponse = await fetch(parseCvUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!parseResponse.ok) {
|
||||
const errText = await parseResponse.text();
|
||||
return NextResponse.json({
|
||||
error: `Parse-CV API returned non-OK status: ${parseResponse.status} - ${errText}`
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
parseResult = await parseResponse.json();
|
||||
} catch (fetchError) {
|
||||
return NextResponse.json({
|
||||
error: `Failed to call local parse-cv route: ${fetchError instanceof Error ? fetchError.message : fetchError}`
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
const { candidateId, interviewId, n8nResponse } = parseResult;
|
||||
|
||||
// 5. Verification - Poll the Supabase `scores` table to verify n8n updated the DB
|
||||
let scoreRecord = null;
|
||||
let verificationAttempts = 0;
|
||||
const maxAttempts = 10;
|
||||
const delayMs = 1500;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
verificationAttempts++;
|
||||
// Wait for n8n execution to finish and write back
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
|
||||
const { data: score, error: scoreError } = await supabase
|
||||
.from("scores")
|
||||
.select("*")
|
||||
.eq("candidate_id", candidateId)
|
||||
.eq("interview_id", interviewId)
|
||||
.maybeSingle();
|
||||
|
||||
if (score && !scoreError) {
|
||||
scoreRecord = score;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
status: scoreRecord ? "success" : "completed_with_pending_evaluation",
|
||||
message: scoreRecord
|
||||
? "Pipeline test executed and verified successfully!"
|
||||
: "Pipeline executed but AI evaluation write-back is pending or failed.",
|
||||
testDetails: {
|
||||
jobUsed: {
|
||||
id: job.id,
|
||||
title: job.title,
|
||||
status: existingJobs && existingJobs.length > 0 ? "reused" : "created",
|
||||
},
|
||||
parseCvResponse: {
|
||||
candidateId,
|
||||
interviewId,
|
||||
n8nWebhookResponse: n8nResponse,
|
||||
},
|
||||
verification: {
|
||||
attempts: verificationAttempts,
|
||||
verified: !!scoreRecord,
|
||||
scoreData: scoreRecord,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("Error in webhook-test API:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "Internal Server Error";
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
@ -9,14 +9,14 @@ export async function generateEmbedding(text: string): Promise<number[]> {
|
|||
try {
|
||||
const start = Date.now();
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${apiKey}`,
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${apiKey}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "models/text-embedding-004",
|
||||
model: "models/gemini-embedding-001",
|
||||
content: {
|
||||
parts: [{ text }],
|
||||
},
|
||||
|
|
@ -41,15 +41,21 @@ export async function generateEmbedding(text: string): Promise<number[]> {
|
|||
originalDimension: embedding.length,
|
||||
}, Date.now() - start);
|
||||
|
||||
// Gemini text-embedding-004 outputs 768 dimensions.
|
||||
// Pad with zeros to fit database vector(1536) schema limit.
|
||||
// Adapt embedding dimensionality dynamically to fit the database vector(1536) schema limit.
|
||||
const targetDimension = 1536;
|
||||
const paddedEmbedding = [...embedding];
|
||||
while (paddedEmbedding.length < targetDimension) {
|
||||
paddedEmbedding.push(0.0);
|
||||
let finalEmbedding = [...embedding];
|
||||
|
||||
if (finalEmbedding.length > targetDimension) {
|
||||
// Truncate (Matryoshka Representation Learning allows this without loss of semantic meaning)
|
||||
finalEmbedding = finalEmbedding.slice(0, targetDimension);
|
||||
} else {
|
||||
// Pad with zeros if the embedding is smaller
|
||||
while (finalEmbedding.length < targetDimension) {
|
||||
finalEmbedding.push(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
return paddedEmbedding;
|
||||
return finalEmbedding;
|
||||
} catch (error) {
|
||||
Logger.error("Failed to generate embedding", error);
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -17,8 +17,15 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey);
|
|||
// Server-side admin/secret Supabase client
|
||||
export const createServerSupabaseClient = () => {
|
||||
const secretKey = process.env.SUPABASE_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("Missing env.SUPABASE_SECRET_KEY");
|
||||
const publishableKey = process.env.SUPABASE_PUBLISHABLE_KEY;
|
||||
|
||||
// Use secret key if available and not a placeholder; otherwise fall back to publishable key
|
||||
const activeKey = (secretKey && secretKey !== "sb_secret_your_secret_key")
|
||||
? secretKey
|
||||
: publishableKey;
|
||||
|
||||
if (!activeKey) {
|
||||
throw new Error("Missing env.SUPABASE_SECRET_KEY or SUPABASE_PUBLISHABLE_KEY");
|
||||
}
|
||||
return createClient(supabaseUrl, secretKey);
|
||||
return createClient(supabaseUrl, activeKey);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
serverExternalPackages: ["pdf-parse"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as readline from "readline";
|
||||
|
||||
// Load .env variables
|
||||
const envPath = path.join(__dirname, "../.env");
|
||||
|
|
@ -21,28 +20,16 @@ if (fs.existsSync(envPath)) {
|
|||
const N8N_HOST = process.env.N8N_HOST || "https://n8n.gaboggamer.online";
|
||||
const N8N_API_KEY = process.env.N8N_API_KEY;
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const SUPABASE_SECRET_KEY = process.env.SUPABASE_SECRET_KEY;
|
||||
const SUPABASE_PUBLISHABLE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY;
|
||||
const SUPABASE_SECRET_KEY = (process.env.SUPABASE_SECRET_KEY && process.env.SUPABASE_SECRET_KEY !== "sb_secret_your_secret_key")
|
||||
? process.env.SUPABASE_SECRET_KEY
|
||||
: process.env.SUPABASE_PUBLISHABLE_KEY;
|
||||
|
||||
if (!N8N_API_KEY) {
|
||||
console.error("Error: N8N_API_KEY is not defined in .env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Interactive prompt helper
|
||||
function askQuestion(query: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question(query, (ans) => {
|
||||
rl.close();
|
||||
resolve(ans.trim());
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function n8nRequest(endpoint: string, method: string = "GET", body?: any) {
|
||||
const response = await fetch(`${N8N_HOST}${endpoint}`, {
|
||||
method,
|
||||
|
|
@ -66,17 +53,17 @@ async function getOrCreateCredential(name: string, type: string, data: any) {
|
|||
const credsList = await n8nRequest("/api/v1/credentials");
|
||||
const existingCred = credsList.data.find((c: any) => c.name === name && c.type === type);
|
||||
if (existingCred) {
|
||||
console.log(`Reusing existing credential: ${name} (ID: ${existingCred.id})`);
|
||||
return existingCred.id;
|
||||
} else {
|
||||
const newCred = await n8nRequest("/api/v1/credentials", "POST", {
|
||||
name,
|
||||
type,
|
||||
data,
|
||||
});
|
||||
console.log(`Created new credential: ${name} (ID: ${newCred.id})`);
|
||||
return newCred.id;
|
||||
console.log(`Deleting existing credential: ${name} (ID: ${existingCred.id})...`);
|
||||
await n8nRequest(`/api/v1/credentials/${existingCred.id}`, "DELETE");
|
||||
}
|
||||
|
||||
const newCred = await n8nRequest("/api/v1/credentials", "POST", {
|
||||
name,
|
||||
type,
|
||||
data,
|
||||
});
|
||||
console.log(`Created new credential: ${name} (ID: ${newCred.id})`);
|
||||
return newCred.id;
|
||||
} catch (err: any) {
|
||||
console.error(`Error setting up credential ${name}:`, err.message);
|
||||
process.exit(1);
|
||||
|
|
@ -151,66 +138,115 @@ function getProviderConfig(provider: string, apiKey: string, modelName: string):
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeProvider(provider: string): string {
|
||||
const p = provider.trim().toLowerCase();
|
||||
if (p === "1" || p === "deepseek") return "1";
|
||||
if (p === "2" || p === "openai") return "2";
|
||||
if (p === "3" || p === "gemini" || p === "google") return "3";
|
||||
if (p === "4" || p === "anthropic" || p === "claude") return "4";
|
||||
throw new Error(`Invalid provider: "${provider}". Choose from: deepseek (1), openai (2), gemini (3), anthropic (4)`);
|
||||
}
|
||||
|
||||
function getDefaultModel(provider: string): string {
|
||||
if (provider === "1") return "deepseek-chat";
|
||||
if (provider === "2") return "gpt-4o-mini";
|
||||
if (provider === "3") return "gemini-1.5-flash";
|
||||
if (provider === "4") return "claude-3-5-sonnet-latest";
|
||||
throw new Error(`Invalid provider choice: ${provider}`);
|
||||
}
|
||||
|
||||
function getApiKeyFromEnv(provider: string): string {
|
||||
if (provider === "1") return process.env.DEEPSEEK_API_KEY || "";
|
||||
if (provider === "2") return process.env.OPENAI_API_KEY || "";
|
||||
if (provider === "3") return process.env.GEMINI_API_KEY || "";
|
||||
if (provider === "4") return process.env.ANTHROPIC_API_KEY || "";
|
||||
return "";
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Usage: npx tsx scripts/deploy-n8n-v2.ts [options]
|
||||
|
||||
Options:
|
||||
--primary-provider=<name|num> Primary LLM provider (1/deepseek, 2/openai, 3/gemini/google, 4/anthropic/claude) [default: gemini]
|
||||
--primary-model=<model_name> Primary model name [default based on provider]
|
||||
--primary-key=<api_key> Primary API key [default: loaded from environment]
|
||||
--fallback Enable fallback LLM model [default: false]
|
||||
--fallback-provider=<name|num> Fallback LLM provider (1/deepseek, 2/openai, 3/gemini/google, 4/anthropic/claude) [default: deepseek]
|
||||
--fallback-model=<model_name> Fallback model name [default based on provider]
|
||||
--fallback-key=<api_key> Fallback API key [default: loaded from environment]
|
||||
-h, --help Show this help message
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("\n==================================================");
|
||||
console.log("Welcome to interactive n8n workflow deployment");
|
||||
console.log("==================================================");
|
||||
// Parse command line arguments
|
||||
const args: any = {};
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
const arg = process.argv[i];
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
if (arg.startsWith("--")) {
|
||||
const parts = arg.slice(2).split("=");
|
||||
const key = parts[0];
|
||||
const val = parts.length > 1 ? parts[1] : true;
|
||||
args[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Ask for Primary Provider
|
||||
console.log("\nSelect Primary LLM Provider:");
|
||||
console.log("1. Deepseek (Native Node)");
|
||||
console.log("2. OpenAI (Standard)");
|
||||
console.log("3. Google Gemini");
|
||||
console.log("4. Anthropic");
|
||||
const primaryProviderChoice = (await askQuestion("Enter choice (1-4) [default: 3]: ")) || "3";
|
||||
console.log("Running in non-interactive mode using CLI flags.");
|
||||
|
||||
let defaultModel = "gemini-1.5-flash";
|
||||
if (primaryProviderChoice === "1") defaultModel = "deepseek-chat";
|
||||
else if (primaryProviderChoice === "2") defaultModel = "gpt-4o-mini";
|
||||
else if (primaryProviderChoice === "4") defaultModel = "claude-3-5-sonnet-latest";
|
||||
|
||||
const primaryModelName = (await askQuestion(`Enter primary model name [default: ${defaultModel}]: `)) || defaultModel;
|
||||
|
||||
let defaultKey = "";
|
||||
if (primaryProviderChoice === "1") defaultKey = process.env.DEEPSEEK_API_KEY || "";
|
||||
else if (primaryProviderChoice === "3") defaultKey = process.env.GEMINI_API_KEY || "";
|
||||
|
||||
const primaryApiKey = (await askQuestion(`Enter API key [default: ${defaultKey ? "Loaded from .env" : "None"}]: `)) || defaultKey;
|
||||
if (!primaryApiKey) {
|
||||
console.error("Primary API Key is required.");
|
||||
let primaryProviderChoice: string;
|
||||
try {
|
||||
primaryProviderChoice = normalizeProvider(String(args["primary-provider"] || "gemini"));
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Ask for Fallback Provider
|
||||
const configureFallback = ((await askQuestion("\nDo you want to configure a Fallback LLM Model? (y/n) [default: n]: ")) || "n").toLowerCase() === "y";
|
||||
const primaryModelName = String(args["primary-model"] || getDefaultModel(primaryProviderChoice));
|
||||
const primaryApiKey = String(args["primary-key"] || getApiKeyFromEnv(primaryProviderChoice));
|
||||
|
||||
if (!primaryApiKey) {
|
||||
console.error(`Error: API Key for primary provider (${primaryProviderChoice}) is required.`);
|
||||
console.error(`Please provide --primary-key=<key> or set the corresponding environment variable (e.g. GEMINI_API_KEY).`);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configureFallback = args["fallback"] === true || args["fallback"] === "true";
|
||||
let fallbackProviderChoice = "";
|
||||
let fallbackModelName = "";
|
||||
let fallbackApiKey = "";
|
||||
|
||||
if (configureFallback) {
|
||||
console.log("\nSelect Fallback LLM Provider:");
|
||||
console.log("1. Deepseek (Native Node)");
|
||||
console.log("2. OpenAI (Standard)");
|
||||
console.log("3. Google Gemini");
|
||||
console.log("4. Anthropic");
|
||||
fallbackProviderChoice = (await askQuestion("Enter choice (1-4) [default: 1]: ")) || "1";
|
||||
|
||||
let defaultFallbackModel = "deepseek-chat";
|
||||
if (fallbackProviderChoice === "2") defaultFallbackModel = "gpt-4o-mini";
|
||||
else if (fallbackProviderChoice === "3") defaultFallbackModel = "gemini-1.5-flash";
|
||||
else if (fallbackProviderChoice === "4") defaultFallbackModel = "claude-3-5-sonnet-latest";
|
||||
|
||||
fallbackModelName = (await askQuestion(`Enter fallback model name [default: ${defaultFallbackModel}]: `)) || defaultFallbackModel;
|
||||
|
||||
let defaultFallbackKey = "";
|
||||
if (fallbackProviderChoice === "1") defaultFallbackKey = process.env.DEEPSEEK_API_KEY || "";
|
||||
else if (fallbackProviderChoice === "3") defaultFallbackKey = process.env.GEMINI_API_KEY || "";
|
||||
|
||||
fallbackApiKey = (await askQuestion(`Enter fallback API key [default: ${defaultFallbackKey ? "Loaded from .env" : "None"}]: `)) || defaultFallbackKey;
|
||||
if (!fallbackApiKey) {
|
||||
console.error("Fallback API Key is required.");
|
||||
try {
|
||||
fallbackProviderChoice = normalizeProvider(String(args["fallback-provider"] || "deepseek"));
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fallbackModelName = String(args["fallback-model"] || getDefaultModel(fallbackProviderChoice));
|
||||
fallbackApiKey = String(args["fallback-key"] || getApiKeyFromEnv(fallbackProviderChoice));
|
||||
|
||||
if (!fallbackApiKey) {
|
||||
console.error(`Error: API Key for fallback provider (${fallbackProviderChoice}) is required when fallback is enabled.`);
|
||||
console.error(`Please provide --fallback-key=<key> or set the corresponding environment variable (e.g. DEEPSEEK_API_KEY).`);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Primary Provider: ${primaryProviderChoice} (${primaryModelName})`);
|
||||
if (configureFallback) {
|
||||
console.log(`Fallback Provider: ${fallbackProviderChoice} (${fallbackModelName})`);
|
||||
} else {
|
||||
console.log("Fallback Provider: Disabled");
|
||||
}
|
||||
|
||||
console.log("\nDeploying credentials to n8n...");
|
||||
|
|
@ -325,21 +361,21 @@ async function main() {
|
|||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
includeOtherFields: false, // Drop other fields to cleanly match schema
|
||||
},
|
||||
include: "none",
|
||||
options: {},
|
||||
},
|
||||
id: "format-data",
|
||||
name: "Format Evaluation Data",
|
||||
type: "n8n-nodes-base.set",
|
||||
typeVersion: 3,
|
||||
typeVersion: 3.4,
|
||||
position: [700, 300],
|
||||
};
|
||||
|
||||
const supabaseInsertNode = {
|
||||
parameters: {
|
||||
operation: "insert",
|
||||
table: "scores",
|
||||
operation: "create",
|
||||
tableId: "scores",
|
||||
dataToSend: "autoMapInputData",
|
||||
options: {},
|
||||
},
|
||||
id: "supabase-insert",
|
||||
|
|
@ -505,7 +541,7 @@ async function main() {
|
|||
console.log(`Activating workflow (ID: ${deployResult.id})...`);
|
||||
await n8nRequest(`/api/v1/workflows/${deployResult.id}/activate`, "POST");
|
||||
|
||||
const webhookUrl = `${N8N_HOST}/webhook/${deployResult.id}/webhook/evaluate-candidate`;
|
||||
const webhookUrl = `${N8N_HOST}/webhook/evaluate-candidate`;
|
||||
console.log("\n==============================================");
|
||||
console.log("DEPLOYMENT COMPLETE");
|
||||
console.log("==============================================");
|
||||
|
|
|
|||
50
scripts/get-deployed-workflow.ts
Normal file
50
scripts/get-deployed-workflow.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
// Load .env variables
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const N8N_HOST = process.env.N8N_HOST || "https://n8n.gaboggamer.online";
|
||||
const N8N_API_KEY = process.env.N8N_API_KEY;
|
||||
|
||||
if (!N8N_API_KEY) {
|
||||
console.error("Error: N8N_API_KEY is not defined in .env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const workflowId = "OOIXDZVnULjRBdjQ";
|
||||
const response = await fetch(`${N8N_HOST}/api/v1/workflows/${workflowId}`, {
|
||||
headers: {
|
||||
"X-N8N-API-KEY": N8N_API_KEY!,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
console.error(`Failed to fetch workflow: ${response.status} - ${text}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const workflow = await response.json();
|
||||
console.log("\n================ DEPLOYED NODES ================");
|
||||
console.log(JSON.stringify(workflow.nodes, null, 2));
|
||||
console.log("\n============= DEPLOYED CONNECTIONS =============");
|
||||
console.log(JSON.stringify(workflow.connections, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
BIN
test-assets/curriculum-vitae-english.pdf
Normal file
BIN
test-assets/curriculum-vitae-english.pdf
Normal file
Binary file not shown.
Loading…
Reference in a new issue