Files
LWLT-AI/archive/handoff/2026-07-12/legacy-erp-handoff/tools/hermes_weixin_source.js
2026-07-13 19:57:46 +08:00

164 lines
4.7 KiB
JavaScript

#!/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,
};