109 lines
4.9 KiB
JavaScript
109 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 18:00 强制收队 + 异常检测 v1.0(2026-08-26)
|
||
* 由 cron 在北京时间 18:05 触发(UTC 10:05)。
|
||
*
|
||
* 职责:
|
||
* 1. 强杀所有仍在运行的 daily-runner / zhaopin-scrape 进程(正常应已自行结束)
|
||
* 2. 异常检测:
|
||
* a. 18点仍在跑的进程 → 判定"未按时收队"(含失联/卡死)
|
||
* b. 卡死检测:进程 cputime 为 0 或远小于 etime(pitfalls #81)
|
||
* c. 状态文件检查:state-hrX.json 缺失/非当日 → 该账号今日未正常运行
|
||
* 3. 若日报今日未发(runner 异常没发成)→ 补发生成日报到群
|
||
* 4. 输出检测结果(cron agent 转发给用户私聊)
|
||
*/
|
||
const { execSync } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const SCRIPTS = '/opt/data/skills/recruitment-resume-screener/scripts';
|
||
const STATE_DIR = '/opt/data/logs/recruitment/state';
|
||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||
const GROUP = 'oc_d09c77b62ca5f90f6699830fbc695c44'; // 2026-08-26 修正:只发私聊
|
||
const HERMES = '/opt/hermes/.venv/bin/hermes';
|
||
const bjNow = () => new Date(Date.now() + 8 * 3600 * 1000);
|
||
const today = () => bjNow().toISOString().slice(0, 10);
|
||
const log = (m) => console.log(`[enforce] ${m}`);
|
||
|
||
const findings = []; // 异常发现
|
||
let reportSent = false;
|
||
|
||
// ---------- 1. 进程检查与强杀 ----------
|
||
let psOut = '';
|
||
try { psOut = execSync("ps aux | grep -E 'daily-runner|zhaopin-scrape' | grep -v grep || true", { encoding: 'utf8' }); } catch { }
|
||
|
||
const lines = psOut.split('\n').filter(l => l.trim());
|
||
if (lines.length === 0) {
|
||
log('无残留进程 ✅');
|
||
} else {
|
||
for (const line of lines) {
|
||
const pid = line.trim().split(/\s+/)[1];
|
||
// 卡死检测:cputime vs etime
|
||
let hung = false, detail = '';
|
||
try {
|
||
const pi = execSync(`ps -p ${pid} -o pid,stat,etime,cputime,rss,cmd --no-headers`, { encoding: 'utf8' }).trim();
|
||
detail = pi;
|
||
const parts = pi.split(/\s+/);
|
||
const etime = parts[2] || '', cputime = parts[3] || '';
|
||
const toSec = (t) => { // etime: [[dd-]hh:]mm:ss
|
||
let s = 0;
|
||
const m = t.match(/(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)/);
|
||
if (m) s = (+m[1] || 0) * 86400 + (+m[2] || 0) * 3600 + (+m[3]) * 60 + (+m[4]);
|
||
return s;
|
||
};
|
||
const cSec = toSec(cputime), eSec = toSec(etime);
|
||
if (cSec === 0 || (eSec > 600 && cSec / eSec < 0.01)) hung = true;
|
||
} catch { }
|
||
findings.push(`18点后仍在运行${hung ? '(疑似卡死)' : ''}: ${line.trim().slice(0, 140)}`);
|
||
try { process.kill(parseInt(pid, 10), 'SIGKILL'); log(`已强杀 PID ${pid}`); }
|
||
catch (e) { log(`杀 ${pid} 失败: ${e.message}`); findings.push(`强杀失败 PID ${pid}: ${e.message}`); }
|
||
}
|
||
// 复查
|
||
try {
|
||
const left = execSync("ps aux | grep -E 'daily-runner|zhaopin-scrape' | grep -v grep || true", { encoding: 'utf8' }).trim();
|
||
if (left) findings.push(`强杀后仍有残留:\n${left.slice(0, 300)}`);
|
||
} catch { }
|
||
}
|
||
|
||
// ---------- 2. 状态文件检查 ----------
|
||
for (const acct of ['hr1', 'hr2']) {
|
||
const f = path.join(STATE_DIR, `state-${acct}.json`);
|
||
try {
|
||
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
|
||
if (s.date !== today()) findings.push(`${acct}: 今日无状态记录(runner 未启动或启动即失败)`);
|
||
else log(`${acct}: 今日状态=${s.status} endedAt=${s.endedAt}`);
|
||
} catch {
|
||
findings.push(`${acct}: 状态文件缺失(runner 未启动)`);
|
||
}
|
||
}
|
||
|
||
// ---------- 3. 日报补发 ----------
|
||
const sentFlag = path.join(STATE_DIR, `report-sent-${today()}`);
|
||
if (fs.existsSync(sentFlag)) {
|
||
reportSent = true;
|
||
log('日报今日已由 runner 发送 ✅');
|
||
} else {
|
||
log('日报未发送,补发中...');
|
||
try {
|
||
const env = { ...process.env };
|
||
// 从状态文件取聊天剩余
|
||
for (const [acct, key] of [['hr1', 'CHAT_LEFT_HR1'], ['hr2', 'CHAT_LEFT_HR2']]) {
|
||
try { const s = JSON.parse(fs.readFileSync(path.join(STATE_DIR, `state-${acct}.json`), 'utf8')); if (s.chatLeft != null) env[key] = String(s.chatLeft); } catch { }
|
||
}
|
||
const md = execSync(`node ${SCRIPTS}/daily-report.js`, { cwd: SCRIPTS, encoding: 'utf8', timeout: 60000, env });
|
||
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 });
|
||
fs.writeFileSync(sentFlag, new Date().toISOString());
|
||
reportSent = true;
|
||
log('日报已补发到群 ✅');
|
||
} catch (e) { findings.push(`日报补发失败: ${String(e.message).slice(0, 150)}`); }
|
||
}
|
||
|
||
// ---------- 4. 汇总输出 ----------
|
||
console.log('\n========== 18点收队结果 ==========');
|
||
console.log(`日报: ${reportSent ? '✅ 已发送' : '❌ 未发送'}`);
|
||
console.log(`异常: ${findings.length === 0 ? '无 ✅' : findings.length + ' 项 ⚠️'}`);
|
||
for (const f of findings) console.log(` ⚠️ ${f}`);
|
||
console.log('==================================');
|
||
process.exit(0);
|