File size: 4,354 Bytes
8ca929f 95abc1b 8ca929f 95abc1b 8ca929f cdb6b12 8ca929f 04d92bf 8ca929f 04d92bf 8ca929f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import { execFile } from "child_process";
import crypto from "crypto";
import fs from "fs/promises";
import path from "path";
import { promisify } from "util";
import type { FitAnalysis } from "../analysis/types";
import type { CvEditPlan, CvGenerateRequest, CvGenerationRecord } from "./types";
import { prepareSafeEditPlan, validateFinalCvOutput } from "./finalContent";
import { ensureJobDir, writeJobMetadata } from "./storage";
const execFileAsync = promisify(execFile);
export function featureFlagEnabled(name: string, fallback = false): boolean {
const raw = process.env[name];
if (raw == null) return fallback;
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
}
export function createGenerationRecord(sourceFormat: "docx" | "pdf"): CvGenerationRecord {
const jobId = crypto.randomUUID();
const now = new Date().toISOString();
return {
jobId,
status: "queued",
sourceFormat,
warnings: [],
createdAt: now,
updatedAt: now,
};
}
export async function runCvGenerationWorker(payload: CvGenerateRequest, record: CvGenerationRecord): Promise<CvGenerationRecord> {
const safePlan = prepareSafeEditPlan(payload.editPlan);
const planValidationWarnings: string[] = [];
for (const instruction of safePlan.plan.instructions) {
if (!instruction.safeToApply) {
throw new Error(`Refusing to generate CV because instruction ${instruction.id} is not marked safeToApply.`);
}
if (instruction.replacementText) {
const validation = validateFinalCvOutput(instruction.replacementText, instruction.sourceLanguage as any);
if (!validation.valid) {
throw new Error(`Refusing to generate CV because replacement text still contains banned internal phrases: ${validation.violations.join(", ")}`);
}
}
}
planValidationWarnings.push(...safePlan.warnings);
const jobDir = await ensureJobDir(record.jobId);
const sourceBuffer = Buffer.from(payload.sourceDocument.base64, "base64");
const sourcePath = path.join(jobDir, payload.sourceDocument.fileName);
const requestPath = path.join(jobDir, "request.json");
const outputPath = path.join(jobDir, `updated-${payload.sourceDocument.fileName.replace(/\s+/g, "-")}`);
const redlinePath = path.join(jobDir, `change-report-${path.parse(payload.sourceDocument.fileName).name}.md`);
await fs.writeFile(sourcePath, sourceBuffer);
await fs.writeFile(
requestPath,
JSON.stringify(
{
sourcePath,
outputPath,
redlinePath,
sourceFormat: payload.sourceDocument.format,
sourceFileName: payload.sourceDocument.fileName,
resumeText: payload.resumeText,
analysis: payload.analysis,
editPlan: safePlan.plan,
},
null,
2,
),
"utf-8",
);
const pythonBin = process.env.PYTHON_EXECUTABLE || (process.platform === "win32" ? "python" : "python3");
const workerPath = path.join(process.cwd(), "scripts", "cv_apply_worker.py");
record.status = "processing";
record.updatedAt = new Date().toISOString();
await writeJobMetadata(record);
try {
const { stdout } = await execFileAsync(pythonBin, [workerPath, requestPath], {
cwd: process.cwd(),
timeout: 180000,
maxBuffer: 10 * 1024 * 1024,
});
const result = JSON.parse(stdout.trim());
const downloadPath = result.outputPath || outputPath;
const updatedRecord: CvGenerationRecord = {
...record,
status: "completed",
outputFileName: result.outputFileName,
outputMimeType: result.outputMimeType,
downloadPath,
redlinePath,
warnings: Array.from(new Set([...(result.warnings || []), ...planValidationWarnings])),
updatedAt: new Date().toISOString(),
};
await writeJobMetadata(updatedRecord);
return updatedRecord;
} catch (error: any) {
const updatedRecord: CvGenerationRecord = {
...record,
status: "failed",
error: error.stderr?.toString() || error.message || "Document generation failed.",
updatedAt: new Date().toISOString(),
};
await writeJobMetadata(updatedRecord);
throw updatedRecord;
}
}
export function buildChangeReportSeed(analysis: FitAnalysis, editPlan: CvEditPlan) {
return {
summary: analysis.profileSummary,
warnings: editPlan.warnings,
instructionCount: editPlan.instructions.length,
};
}
|