164 lines
4.7 KiB
JavaScript
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,
|
|
};
|