mirror of
https://github.com/xx254/linkedin_skills.git
synced 2026-06-15 17:54:56 +03:00
linkedin skills
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
function parseCsv(content) {
|
||||
const rows = [];
|
||||
let row = [];
|
||||
let field = "";
|
||||
let inQuotes = false;
|
||||
|
||||
// Character-level parser to correctly handle commas/newlines inside quotes.
|
||||
for (let i = 0; i < content.length; i += 1) {
|
||||
const char = content[i];
|
||||
const next = content[i + 1];
|
||||
|
||||
if (char === "\"") {
|
||||
if (inQuotes && next === "\"") {
|
||||
field += "\"";
|
||||
i += 1;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "," && !inQuotes) {
|
||||
row.push(field);
|
||||
field = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((char === "\n" || char === "\r") && !inQuotes) {
|
||||
if (char === "\r" && next === "\n") {
|
||||
i += 1;
|
||||
}
|
||||
row.push(field);
|
||||
field = "";
|
||||
if (row.length > 1 || (row.length === 1 && row[0] !== "")) {
|
||||
rows.push(row);
|
||||
}
|
||||
row = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
field += char;
|
||||
}
|
||||
|
||||
if (field.length || row.length) {
|
||||
row.push(field);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return { headers: [], records: [] };
|
||||
}
|
||||
|
||||
const headers = rows[0].map((h) => h.trim());
|
||||
const records = rows.slice(1).map((r) => {
|
||||
const obj = {};
|
||||
headers.forEach((h, idx) => {
|
||||
obj[h] = (r[idx] || "").trim();
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
return { headers, records };
|
||||
}
|
||||
|
||||
function escapeCsvValue(value) {
|
||||
const s = String(value ?? "");
|
||||
if (/[",\n\r]/.test(s)) {
|
||||
return `"${s.replace(/"/g, "\"\"")}"`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function toCsv(headers, records) {
|
||||
const lines = [headers.map(escapeCsvValue).join(",")];
|
||||
records.forEach((record) => {
|
||||
const row = headers.map((h) => escapeCsvValue(record[h] ?? ""));
|
||||
lines.push(row.join(","));
|
||||
});
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseCsv,
|
||||
toCsv
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function readJson(filePath, fallback = null) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return fallback;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
function writeJson(filePath, data) {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
function readText(filePath, fallback = "") {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return fallback;
|
||||
}
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
}
|
||||
|
||||
function writeText(filePath, content) {
|
||||
ensureDir(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
function formatDateKey(date = new Date()) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
return `${y}_${m}_${d}`;
|
||||
}
|
||||
|
||||
function getWeekKey(date = new Date()) {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureDir,
|
||||
readJson,
|
||||
writeJson,
|
||||
readText,
|
||||
writeText,
|
||||
formatDateKey,
|
||||
getWeekKey
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Heuristic classification for prospect replies (matches reply-handler SKILL types).
|
||||
*/
|
||||
|
||||
function normalize(text) {
|
||||
return String(text || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function classifyProspectReply(text) {
|
||||
const t = normalize(text);
|
||||
if (!t) {
|
||||
return { type: "irrelevant", confidence: "low" };
|
||||
}
|
||||
|
||||
if (
|
||||
/\b(unsubscribe|remove me|stop|not interested|no thanks|don't contact)\b/.test(t)
|
||||
) {
|
||||
return { type: "not_interested", confidence: "high" };
|
||||
}
|
||||
|
||||
if (/\b(we use|using|already have|competitor|vs\.?|compared to)\b/.test(t)) {
|
||||
return { type: "competitor_mention", confidence: "medium" };
|
||||
}
|
||||
|
||||
if (/\?|how much|pricing|cost|what is|can you|could you|tell me how/.test(t)) {
|
||||
return { type: "question", confidence: "medium" };
|
||||
}
|
||||
|
||||
if (
|
||||
/\b(too busy|not the right time|maybe later|not now|circle back|reach out later)\b/.test(
|
||||
t
|
||||
)
|
||||
) {
|
||||
return { type: "hesitant", confidence: "medium" };
|
||||
}
|
||||
|
||||
if (
|
||||
/\b(tell me more|sounds good|interested|curious|love to|let's|schedule|book)\b/.test(
|
||||
t
|
||||
)
|
||||
) {
|
||||
return { type: "interested", confidence: "medium" };
|
||||
}
|
||||
|
||||
if (/\b(interesting|cool|nice|thanks)\b/.test(t) && t.length < 120) {
|
||||
return { type: "positive_but_vague", confidence: "low" };
|
||||
}
|
||||
|
||||
return { type: "irrelevant", confidence: "low" };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
classifyProspectReply,
|
||||
normalize
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
const { getWeekKey } = require("./io");
|
||||
|
||||
const DEFAULT_KEYS = [
|
||||
"leads_processed",
|
||||
"qualified_count",
|
||||
"drafted_count",
|
||||
"reply_drafts_count",
|
||||
"replied_count",
|
||||
"rejected_count",
|
||||
"score3_conversion_count"
|
||||
];
|
||||
|
||||
/**
|
||||
* Ensures weeklyMetrics exists, resets when ISO week changes, backfills missing counters.
|
||||
*/
|
||||
function ensureWeekMetrics(state) {
|
||||
const weekKey = getWeekKey();
|
||||
const wm = state.weeklyMetrics;
|
||||
|
||||
if (!wm || wm.weekKey !== weekKey) {
|
||||
state.weeklyMetrics = {
|
||||
weekKey,
|
||||
leads_processed: 0,
|
||||
qualified_count: 0,
|
||||
drafted_count: 0,
|
||||
reply_drafts_count: 0,
|
||||
replied_count: 0,
|
||||
rejected_count: 0,
|
||||
score3_conversion_count: 0
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
DEFAULT_KEYS.forEach((k) => {
|
||||
if (typeof state.weeklyMetrics[k] !== "number" || Number.isNaN(state.weeklyMetrics[k])) {
|
||||
state.weeklyMetrics[k] = 0;
|
||||
}
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureWeekMetrics
|
||||
};
|
||||
Reference in New Issue
Block a user