linkedin skills

This commit is contained in:
xx254
2026-04-01 21:59:13 +08:00
commit 743a87d9cf
33 changed files with 3372 additions and 0 deletions
+85
View File
@@ -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
};
+56
View File
@@ -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
};
+57
View File
@@ -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
};
+44
View File
@@ -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
};