#!/usr/bin/env node const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const DEFAULT_MAX_AGE_MINUTES = 10; function defaultLogPath() { const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); return path.join(localAppData, 'hermes', 'logs', 'agent.log'); } function parseArgs(argv) { const args = { order: '', log: defaultLogPath(), maxAgeMinutes: DEFAULT_MAX_AGE_MINUTES, json: false, help: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--order') { args.order = argv[index + 1]; index += 1; } else if (arg.startsWith('--order=')) { args.order = arg.slice('--order='.length); } else if (arg === '--log') { args.log = argv[index + 1]; index += 1; } else if (arg.startsWith('--log=')) { args.log = arg.slice('--log='.length); } else if (arg === '--max-age-minutes') { args.maxAgeMinutes = Number(argv[index + 1]); index += 1; } else if (arg.startsWith('--max-age-minutes=')) { args.maxAgeMinutes = Number(arg.slice('--max-age-minutes='.length)); } else if (arg === '--json') { args.json = true; } else if (arg === '--help' || arg === '-h') { args.help = true; } } return args; } function normalizeText(value) { return String(value || '') .replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\s+/g, ' ') .trim(); } function compactText(value) { return normalizeText(value).replace(/\s+/g, '').toLowerCase(); } function parseTimestampMs(line) { const match = String(line || '').match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}),(\d{3})/); if (!match) return 0; const parsed = Date.parse(`${match[1]}T${match[2]}.${match[3]}+08:00`); return Number.isFinite(parsed) ? parsed : 0; } function parseInboundWeixinLine(line) { const match = String(line || '').match(/gateway\.run: inbound message: platform=weixin user=(\S+) chat=(\S+) msg='([\s\S]*?)' reply_to_id=/); if (!match) return null; return { timestampMs: parseTimestampMs(line), senderId: match[1], chatId: match[2], messageSnippet: normalizeText(match[3]), }; } function isMatchingOrder(logSnippet, orderText) { const snippet = compactText(logSnippet); const order = compactText(orderText); if (!snippet || !order) return false; const probeLength = Math.min(80, Math.max(24, Math.min(snippet.length, order.length))); const snippetProbe = snippet.slice(0, probeLength); const orderProbe = order.slice(0, probeLength); return order.startsWith(snippetProbe) || snippet.startsWith(orderProbe); } function findLatestSource({ orderText, logPath, maxAgeMinutes = DEFAULT_MAX_AGE_MINUTES, nowMs = Date.now() }) { const log = fs.readFileSync(logPath, 'utf8').replace(/^\uFEFF/, ''); const lines = log.split(/\r?\n/); const maxAgeMs = Number(maxAgeMinutes) > 0 ? Number(maxAgeMinutes) * 60 * 1000 : Infinity; for (let index = lines.length - 1; index >= 0; index -= 1) { const entry = parseInboundWeixinLine(lines[index]); if (!entry) continue; if (entry.timestampMs && nowMs - entry.timestampMs > maxAgeMs) continue; if (!isMatchingOrder(entry.messageSnippet, orderText)) continue; return { ok: true, platform: 'weixin', senderId: entry.senderId, chatId: entry.chatId, matchedAt: entry.timestampMs ? new Date(entry.timestampMs).toISOString() : '', matchedSnippet: entry.messageSnippet, logPath, }; } return { ok: false, reason: 'weixin_source_not_found', logPath, }; } function helpText() { return [ 'Usage: node tools/hermes_weixin_source.js --order order.txt [--json]', '', 'Finds the recent Hermes Weixin sender/chat id for a just-received order.', 'Use the returned senderId with erp_task_dispatcher.js --sender-id.', ].join('\n'); } function main() { const args = parseArgs(process.argv.slice(2)); if (args.help || !args.order) { console.log(helpText()); process.exitCode = args.help ? 0 : 1; return; } const orderPath = path.isAbsolute(args.order) ? args.order : path.resolve(args.order); const orderText = fs.readFileSync(orderPath, 'utf8'); const result = findLatestSource({ orderText, logPath: args.log, maxAgeMinutes: args.maxAgeMinutes, }); if (args.json) { console.log(JSON.stringify(result, null, 2)); } else if (result.ok) { console.log(result.senderId); } else { console.error(result.reason); } if (!result.ok) process.exitCode = 2; } if (require.main === module) { main(); } module.exports = { parseArgs, parseInboundWeixinLine, isMatchingOrder, findLatestSource, };