From 1c87c5514dc96bafc19b2d7c0482eeb5d0e2fdb5 Mon Sep 17 00:00:00 2001 From: hermes Date: Wed, 16 Sep 2026 10:18:04 +0000 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E6=8A=A5v1.5:=20=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=AE=9A=E7=A8=BF(=E5=91=BD=E4=B8=AD=E5=88=97=E6=B8=85?= =?UTF-8?q?=E6=B4=97=E8=AF=81=E4=B9=A6=E5=89=8D=E7=BC=80);=20=E6=8C=89?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=88=86=E5=BC=80=E5=8F=91=E9=80=81--account?= =?UTF-8?q?=E5=8F=82=E6=95=B0+=E6=94=B6=E9=98=9F=E9=80=90=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=90=84=E5=8F=91=E4=B8=80=E6=9D=A1,=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=90=88=E5=B9=B6=E5=90=88=E9=9B=86;=20=E6=97=A0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=B4=A6=E5=8F=B7=E8=87=AA=E5=8A=A8=E8=B7=B3?= =?UTF-8?q?=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/daily-report.js | 77 +++++++++++++++++---------------------- scripts/enforce-cutoff.js | 25 ++++++++++--- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/scripts/daily-report.js b/scripts/daily-report.js index c13480d..14634bd 100644 --- a/scripts/daily-report.js +++ b/scripts/daily-report.js @@ -1,18 +1,19 @@ #!/usr/bin/env node /** - * 每日筛选汇总生成器 v1.3(2026-09-15 密集型改版) + * 每日筛选汇总生成器 v1.4(2026-09-15 极简紧凑版) * 从 recruitment.db 查询当日数据,生成 Markdown 日报。 * 用法:node daily-report.js [YYYY-MM-DD] [--db /path/to.db] - * 输出:Markdown 文本到 stdout(cron 任务用 hermes send -f 发送) * - * v1.3 改版:表格密集化——多行小表合并为单行紧凑格式, - * 减少行数、去掉冗余表头,一屏读完。 + * v1.4:表格极限压缩——总览改单行文本流(无表头无对齐行), + * 画像表砍到3列。目标:整条消息一屏内、无空行浪费。 */ 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 acctArgIdx = args.indexOf('--account'); +const ONLY_ACCT = acctArgIdx >= 0 ? args[acctArgIdx + 1] : null; // v1.5: 单账号渲染(分账号发送用) const dbArgIdx = args.indexOf('--db'); const DB_PATH = dbArgIdx >= 0 ? args[dbArgIdx + 1] : '/opt/data/recruitment-data/recruitment-live.db'; @@ -50,69 +51,59 @@ const greetedRows = rows.filter(r => r.greeted && r.greeted_at && r.greeted_at.s // 历史累计 const histTotal = db.prepare('SELECT COUNT(*) c FROM candidates').get().c; -// ---------- Markdown 渲染 ---------- +// ---------- 渲染 ---------- const WEEK = ['日', '一', '二', '三', '四', '五', '六']; const wk = WEEK[new Date(day + 'T12:00:00Z').getUTCDay()]; const ACCT_META = { - hr1: { label: 'hr1(Chrome)', short: 'hr1·Chrome', chatLeft: () => process.env.CHAT_LEFT_HR1 || '—', status: () => process.env.HR1_STATUS || '✅ 在线' }, - hr2: { label: 'hr2(Edge)', short: 'hr2·Edge', chatLeft: () => process.env.CHAT_LEFT_HR2 || '—', status: () => process.env.HR2_STATUS || '✅ 在线' }, + hr1: { short: 'hr1', chatLeft: () => process.env.CHAT_LEFT_HR1 || '—', status: () => process.env.HR1_STATUS || '✅' }, + hr2: { short: 'hr2', chatLeft: () => process.env.CHAT_LEFT_HR2 || '—', status: () => process.env.HR2_STATUS || '✅' }, }; -// ---------- 开工判断(2026-09-02 用户定稿)---------- -// 未开工 = 当日无任何筛选数据。两个都没开工 → 输出空串(不发日报); -// 只有一个开工 → 日报只包含该账号,未开工的整块剔除。 +// ---------- 开工判断(v1.5: --account 时只渲染该账号,且标题带账号名)---------- const activeAccts = ['hr1', 'hr2'].filter(a => (byAcct[a]?.n || 0) > 0); +const renderAccts = ONLY_ACCT ? activeAccts.filter(a => a === ONLY_ACCT) : activeAccts; const md = []; -if (activeAccts.length > 0) { -md.push(`# 📊 智联简历筛选日报`); +if (renderAccts.length > 0) { +const titleAcct = ONLY_ACCT ? `(${ACCT_META[ONLY_ACCT].short})` : ''; +md.push(`📊 **简历筛选日报${titleAcct} ${day} 周${wk}**`); md.push(''); -md.push(`**${day} 周${wk}** · 二手车出口外贸业务员`); -md.push(''); - -// ===== 密集型总览:一张表装下所有账号的核心数字 ===== -md.push(`## ■ 总览`); -md.push(''); -if (activeAccts.length === 2) { - md.push(`| 账号 | 筛选 | 打招呼 | 推荐 | 待定 | 拦截 | 聊天剩余 | 状态 |`); - md.push(`|:--|--:|--:|--:|--:|--:|--:|:--|`); - for (const a of activeAccts) { - const v = byAcct[a]; - const p = (n) => v.n ? Math.round(n / v.n * 100) + '%' : '0%'; - md.push(`| ${ACCT_META[a].short} | ${v.n} | ${v.greeted} | ${v.rec} | ${v.wait} | ${v.irr}(${p(v.irr)}) | ${ACCT_META[a].chatLeft()} | ${ACCT_META[a].status()} |`); - } - md.push(`| **合计** | **${total.n}** | **${total.greeted}** | **${total.rec}** | **${total.wait}** | **${total.irr}** | | |`); -} else { - const a = activeAccts[0]; +for (const a of renderAccts) { const v = byAcct[a]; const p = (n) => v.n ? Math.round(n / v.n * 100) + '%' : '0%'; - md.push(`| 账号 | 筛选 | 打招呼 | 推荐 | 待定 | 拦截 | 聊天剩余 | 状态 |`); - md.push(`|:--|--:|--:|--:|--:|--:|--:|:--|`); - md.push(`| ${ACCT_META[a].short} | ${v.n} | ${v.greeted} | ${v.rec} | ${v.wait} | ${v.irr}(${p(v.irr)}) | ${ACCT_META[a].chatLeft()} | ${ACCT_META[a].status()} |`); + md.push(`**${ACCT_META[a].short}** 筛${v.n}·打${v.greeted}|✅${v.rec} ⏳${v.wait} ⛔${v.irr}(${p(v.irr)})|剩${ACCT_META[a].chatLeft()}次 ${ACCT_META[a].status()}`); +} +if (renderAccts.length > 1 && !ONLY_ACCT) { + const p = (n) => total.n ? Math.round(n / total.n * 100) + '%' : '0%'; + md.push(`**合计** 筛${total.n}·打${total.greeted}|✅${total.rec} ⏳${total.wait} ⛔${total.irr}(${p(total.irr)})|库累计${histTotal.toLocaleString()}`); +} else { + md.push(`> 库累计 ${histTotal.toLocaleString()} 人`); } -md.push(''); -md.push(`> 历史累计 ${histTotal.toLocaleString()} 人`); md.push(''); -// ===== 打招呼画像(密集:每账号一张表,最多5行,信息列合并) ===== -for (const acct of activeAccts) { +// 打招呼画像(3列极简:姓名 | 基本情况 | 分·级别) +for (const acct of renderAccts) { const gr = greetedRows.filter(g => g.account === acct); if (!gr.length) continue; - md.push(`## ■ ${ACCT_META[acct].short} 打招呼 ${gr.length} 人`); + md.push(`**打招呼 ${gr.length} 人**(${ACCT_META[acct].short})`); md.push(''); - md.push(`| 姓名 | 年龄·学历·经验 | 评分·级别 |命中|`); + md.push(`| 姓名 | 情况 | 分 | 命中 |`); md.push(`|:--|:--|:--|:--|`); - const top = gr.slice(0, 5); - for (const g of top) { + for (const g of gr.slice(0, 5)) { 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} | ${(g.notes || '').split(';').slice(0, 2).join(' ').slice(0, 24)} |`); + // 命中:notes里的 经验/专业/证书 提取关键词,紧凑展示(经验值/专业语种/证书名) + const hit = (g.notes || '') + .split(/[;;]/).map(s => s.trim()).filter(Boolean) + .map(s => s.replace(/^经验:外贸\/出口.*/, '外贸出口').replace(/^经验:/, '').replace(/^专业:国际经济与贸易$/, '国贸').replace(/^专业:商务英语$/, '商英').replace(/^专业:应用英语$/, '应英').replace(/^专业:/, '').replace(/^证书:/, '').replace(/大学英语四级|英语四级/g, 'CET4').replace(/大学英语六级|英语六级/g, 'CET6').replace(/英语专业四级|英语四级考试/g, '专四').replace(/英语专业八级/g, '专八').replace(/\/.*/, '')) + .join('·'); + md.push(`| ${g.name} | ${g.age || '?'}·${g.education || '?'}·${exp} | ${g.score}${g.score_level === '推荐' ? '✅' : ''} | ${hit.slice(0, 20)} |`); } - if (gr.length > 5) md.push(`| …共 ${gr.length} 人 | | | |`); + if (gr.length > 5) md.push(`| …${gr.length}人 | | |`); md.push(''); } -} // end if (activeAccts.length > 0) +} // end if console.log(md.join('\n')); db.close(); diff --git a/scripts/enforce-cutoff.js b/scripts/enforce-cutoff.js index 2de9375..d9f6b3f 100644 --- a/scripts/enforce-cutoff.js +++ b/scripts/enforce-cutoff.js @@ -99,18 +99,31 @@ const sentFlag = path.join(STATE_DIR, `report-sent-${today()}`); if (s.date === today()) env[skey] = map[s.status] || '✅ 在线'; } catch { } } - const md = execSync(`node ${SCRIPTS}/daily-report.js`, { cwd: SCRIPTS, encoding: 'utf8', timeout: 60000, env }); - // v1.2(2026-09-02 用户定稿):两个账号都没开工 → 日报输出为空 → 不发送 - if (!md.trim()) { + // v1.3(2026-09-15 用户定稿):按账号分开发送,每个开工账号一条消息(不再合并合集) + const { execSync: es } = require('child_process'); + // 先探一次全量渲染拿开工账号列表(输出空=都没开工) + const probe = es(`node ${SCRIPTS}/daily-report.js`, { cwd: SCRIPTS, encoding: 'utf8', timeout: 60000, env }); + if (!probe.trim()) { log('双账号今日均无数据,日报不发送(不发空消息)'); fs.writeFileSync(sentFlag, new Date().toISOString()); reportSent = 'skipped_empty'; } else { - fs.writeFileSync(`/tmp/daily-report-enforce-${today()}.md`, md); - execSync(`${HERMES} send -t "feishu:${GROUP}" -f /tmp/daily-report-enforce-${today()}.md`, { stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000 }); + // 判断开工账号:有数据的才发 + for (const [acct, envKeys] of [['hr1', { left: 'CHAT_LEFT_HR1', st: 'HR1_STATUS' }], ['hr2', { left: 'CHAT_LEFT_HR2', st: 'HR2_STATUS' }]]) { + const envA = { ...env }; + try { + const s = JSON.parse(fs.readFileSync(path.join(STATE_DIR, `state-${acct}.json`), 'utf8')); + if (s.chatLeft != null) envA[envKeys.left] = String(s.chatLeft); + if (s.date === today()) envA[envKeys.st] = s.status === 'time_cutoff' ? '✅' : (s.status === 'running' ? '✅' : '⚠️' + s.status); + } catch { } + const mdA = es(`node ${SCRIPTS}/daily-report.js --account ${acct}`, { cwd: SCRIPTS, encoding: 'utf8', timeout: 60000, env: envA }); + if (!mdA.trim()) continue; // 该账号无数据,跳过不发 + fs.writeFileSync(`/tmp/daily-report-${acct}-${today()}.md`, mdA); + es(`${HERMES} send -t "feishu:${GROUP}" -f /tmp/daily-report-${acct}-${today()}.md`, { stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000 }); + log(`${acct} 日报已单独发送到群 ✅`); + } fs.writeFileSync(sentFlag, new Date().toISOString()); reportSent = true; - log('日报已发送到群 ✅'); } } catch (e) { findings.push(`日报发送失败: ${String(e.message).slice(0, 150)}`); } }