2026-08-26 22:55:08 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 每日筛选汇总生成器 v1.0(2026-08-26)
|
|
|
|
|
|
* 从 recruitment.db 查询当日数据,生成 Markdown 日报。
|
|
|
|
|
|
* 用法:node daily-report.js [YYYY-MM-DD] [--db /path/to.db]
|
|
|
|
|
|
* 输出:Markdown 文本到 stdout(cron 任务用 hermes send -f 发送)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const Database = require('better-sqlite3');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
const dateArg = args.find(a => /^\d{4}-\d{2}-\d{2}$/.test(a));
|
|
|
|
|
|
const dbArgIdx = args.indexOf('--db');
|
|
|
|
|
|
const DB_PATH = dbArgIdx >= 0 ? args[dbArgIdx + 1] : '/opt/data/recruitment-data/recruitment-live.db';
|
|
|
|
|
|
|
|
|
|
|
|
const day = dateArg || new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
const dayStart = `${day}T00:00`;
|
|
|
|
|
|
const dayEnd = `${day}T23:59`;
|
|
|
|
|
|
|
|
|
|
|
|
const db = new Database(DB_PATH, { readonly: true });
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 数据查询 ----------
|
|
|
|
|
|
const rows = db.prepare(
|
|
|
|
|
|
`SELECT account, name, age, education, experience, school, score, score_level, role_tag,
|
|
|
|
|
|
greeted, greeted_at, notes
|
|
|
|
|
|
FROM candidates WHERE scraped_at >= ? AND scraped_at <= ?`
|
|
|
|
|
|
).all(dayStart, dayEnd);
|
|
|
|
|
|
|
|
|
|
|
|
// 账号聚合
|
|
|
|
|
|
const byAcct = {};
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
|
const a = byAcct[r.account || '未知'] || (byAcct[r.account || '未知'] = { n: 0, rec: 0, wait: 0, irr: 0, greeted: 0 });
|
|
|
|
|
|
a.n++;
|
|
|
|
|
|
if (r.score_level === '推荐') a.rec++;
|
|
|
|
|
|
else if (r.score_level === '待定') a.wait++;
|
|
|
|
|
|
else a.irr++;
|
|
|
|
|
|
if (r.greeted) a.greeted++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const total = { n: rows.length, rec: 0, wait: 0, irr: 0, greeted: 0 };
|
|
|
|
|
|
for (const a of Object.values(byAcct)) { total.rec += a.rec; total.wait += a.wait; total.irr += a.irr; total.greeted += a.greeted; }
|
|
|
|
|
|
|
|
|
|
|
|
// 打招呼明细(当日实际打过招呼的人)
|
|
|
|
|
|
const greetedRows = rows.filter(r => r.greeted && r.greeted_at && r.greeted_at.startsWith(day))
|
|
|
|
|
|
.sort((x, y) => (y.score || 0) - (x.score || 0));
|
|
|
|
|
|
|
|
|
|
|
|
// 历史累计
|
|
|
|
|
|
const histTotal = db.prepare('SELECT COUNT(*) c FROM candidates').get().c;
|
|
|
|
|
|
|
|
|
|
|
|
// 最近一次聊天剩余(从最近 scrape_log 无记录,需运行时传入;此处用环境变量或显示待补)
|
|
|
|
|
|
const chatLeft = process.env.CHAT_LEFT_HR1 ? `hr1 剩 ${process.env.CHAT_LEFT_HR1}` : 'hr1 剩 ?';
|
|
|
|
|
|
const chatLeft2 = process.env.CHAT_LEFT_HR2 ? `hr2 剩 ${process.env.CHAT_LEFT_HR2}` : 'hr2 剩 ?';
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- Markdown 渲染 ----------
|
|
|
|
|
|
const WEEK = ['日', '一', '二', '三', '四', '五', '六'];
|
|
|
|
|
|
const wk = WEEK[new Date(day + 'T12:00:00Z').getUTCDay()];
|
|
|
|
|
|
|
|
|
|
|
|
const ACCT_META = {
|
|
|
|
|
|
hr1: { label: 'hr1(Chrome)', chatLeft: () => process.env.CHAT_LEFT_HR1 ? `剩 ${process.env.CHAT_LEFT_HR1} 次` : '—', status: () => process.env.HR1_STATUS || '✅ 在线' },
|
|
|
|
|
|
hr2: { label: 'hr2(Edge)', chatLeft: () => process.env.CHAT_LEFT_HR2 ? `剩 ${process.env.CHAT_LEFT_HR2} 次` : '—', status: () => process.env.HR2_STATUS || '✅ 在线' },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-11 13:00:04 +00:00
|
|
|
|
// ---------- 开工判断(2026-09-02 用户定稿)----------
|
|
|
|
|
|
// 未开工 = 当日无任何筛选数据。两个都没开工 → 输出空串(不发日报);
|
|
|
|
|
|
// 只有一个开工 → 日报只包含该账号,未开工的整块剔除。
|
|
|
|
|
|
const activeAccts = ['hr1', 'hr2'].filter(a => (byAcct[a]?.n || 0) > 0);
|
|
|
|
|
|
|
2026-08-26 22:55:08 +08:00
|
|
|
|
const md = [];
|
2026-09-11 13:00:04 +00:00
|
|
|
|
if (activeAccts.length > 0) {
|
2026-08-26 22:55:08 +08:00
|
|
|
|
md.push(`# 📊 智联简历筛选日报`);
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
md.push(`**${day} 周${wk}** · 岗位:二手车出口外贸业务员`);
|
|
|
|
|
|
md.push('---');
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
|
2026-09-11 13:00:04 +00:00
|
|
|
|
// 总览(只列开工账号;单账号时省略对比表)
|
2026-08-26 22:55:08 +08:00
|
|
|
|
md.push(`## ■ 今日总览`);
|
|
|
|
|
|
md.push('');
|
2026-09-11 13:00:04 +00:00
|
|
|
|
if (activeAccts.length === 2) {
|
|
|
|
|
|
md.push(`| 指标 | hr1 Chrome | hr2 Edge | 合计 |`);
|
|
|
|
|
|
md.push(`|:---|---:|---:|---:|`);
|
|
|
|
|
|
md.push(`| 新增筛选 | ${byAcct.hr1?.n || 0} | ${byAcct.hr2?.n || 0} | **${total.n}** |`);
|
|
|
|
|
|
md.push(`| 打招呼 | ${byAcct.hr1?.greeted || 0} | ${byAcct.hr2?.greeted || 0} | **${total.greeted}** |`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const a = activeAccts[0];
|
|
|
|
|
|
md.push(`| 指标 | ${ACCT_META[a].label} |`);
|
|
|
|
|
|
md.push(`|:---|---:|`);
|
|
|
|
|
|
md.push(`| 新增筛选 | **${byAcct[a].n}** |`);
|
|
|
|
|
|
md.push(`| 打招呼 | **${byAcct[a].greeted}** |`);
|
|
|
|
|
|
}
|
2026-08-26 22:55:08 +08:00
|
|
|
|
md.push('');
|
|
|
|
|
|
|
2026-09-11 13:00:04 +00:00
|
|
|
|
// 分账号小节(只列开工账号,2026-09-02)
|
|
|
|
|
|
for (const acct of activeAccts) {
|
2026-08-26 22:55:08 +08:00
|
|
|
|
const meta = ACCT_META[acct];
|
|
|
|
|
|
const v = byAcct[acct];
|
|
|
|
|
|
md.push(`## ■ ${meta.label}`);
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
const p = (n) => ((n / v.n) * 100).toFixed(1) + '%';
|
|
|
|
|
|
md.push(`| 结果 | 人数 | 占比 |`);
|
|
|
|
|
|
md.push(`|:---|---:|---:|`);
|
|
|
|
|
|
md.push(`| ✅ 推荐 | ${v.rec} | ${p(v.rec)} |`);
|
|
|
|
|
|
md.push(`| ⏳ 待定 | ${v.wait} | ${p(v.wait)} |`);
|
|
|
|
|
|
md.push(`| ⛔ 拦截 | ${v.irr} | ${p(v.irr)} |`);
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
const gr = greetedRows.filter(g => g.account === acct);
|
|
|
|
|
|
if (gr.length) {
|
|
|
|
|
|
md.push(`**打招呼画像(${gr.length} 人)**`);
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
md.push(`| 姓名 | 年龄 | 学历 | 经验 | 评分 | 级别 |`);
|
|
|
|
|
|
md.push(`|:---|---:|:---:|---:|---:|:---:|`);
|
|
|
|
|
|
const top = gr.slice(0, 5);
|
|
|
|
|
|
for (const g of top) {
|
|
|
|
|
|
const expRaw = (g.experience || '').toString();
|
|
|
|
|
|
const exp = /应届生|在校生/.test(expRaw) ? '应届' : (expRaw.match(/\d+年/) || [])[0] || ((expRaw.match(/^\d+$/)?.[0] || '') + '年' || '—');
|
|
|
|
|
|
md.push(`| ${g.name} | ${g.age || '—'} | ${g.education || '—'} | ${exp} | ${g.score || '—'} | ${g.score_level} |`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (gr.length > 5) md.push(`| …共 ${gr.length} 人 | | | | | |`);
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
md.push(`## ■ 资源与状态`);
|
|
|
|
|
|
md.push('');
|
|
|
|
|
|
md.push(`| 账号 | 聊天剩余 | 状态 |`);
|
|
|
|
|
|
md.push(`|:---|---:|:---|`);
|
2026-09-11 13:00:04 +00:00
|
|
|
|
for (const a of activeAccts) {
|
|
|
|
|
|
md.push(`| ${ACCT_META[a].label.replace('(', ' ').replace(')', '')} | ${ACCT_META[a].chatLeft()} | ${ACCT_META[a].status()} |`);
|
|
|
|
|
|
}
|
2026-08-26 22:55:08 +08:00
|
|
|
|
md.push('');
|
|
|
|
|
|
md.push(`- 历史累计:${histTotal.toLocaleString()} 人`);
|
2026-09-11 13:00:04 +00:00
|
|
|
|
} // end if (activeAccts.length > 0)
|
2026-08-26 22:55:08 +08:00
|
|
|
|
|
|
|
|
|
|
console.log(md.join('\n'));
|
|
|
|
|
|
db.close();
|