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

306 lines
12 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
const teamSingleOps = require('./erp_team_single_browser_operations');
const { connectLiveContext, isLiveSessionEnabled } = require('./erp_live_session_manager');
const {
ensureErpSessionReady,
installErpDialogHandler,
} = require('./erp_session_guard');
const { readTravelerTableState } = require('./erp_traveler_import');
function parseArgs(argv = process.argv.slice(2)) {
const args = {
config: 'config/erp-deployment.local.json',
groupNo: '',
departureDate: '',
target: 'modify',
json: false,
debug: false,
};
for (let index = 0; index < argv.length; index += 1) {
const item = argv[index];
if (item === '--config') args.config = argv[++index] || args.config;
else if (item === '--group') args.groupNo = argv[++index] || '';
else if (item === '--departure-date') args.departureDate = argv[++index] || '';
else if (item === '--target') args.target = argv[++index] || args.target;
else if (item === '--json') args.json = true;
else if (item === '--debug') args.debug = true;
}
if (!args.groupNo) throw new Error('--group is required');
if (!args.departureDate) throw new Error('--departure-date is required');
if (!['modify', 'detail'].includes(args.target)) throw new Error('--target must be modify or detail');
return args;
}
async function readTravelerStructure(frame) {
return frame.evaluate(() => {
const skipTypes = new Set(['hidden', 'button', 'submit', 'reset', 'image', 'file']);
const writable = (root) => Array.from(root.querySelectorAll('input, textarea, select'))
.filter((el) => !el.disabled && !skipTypes.has(String(el.type || '').toLowerCase()));
const rows = Array.from(document.querySelectorAll('tr'));
const bodyText = String(document.body && document.body.innerText || '').replace(/\s+/g, ' ').trim();
const headerIndex = rows.findIndex((row) => {
const text = String(row.innerText || row.textContent || '');
return text.includes('游客信息')
|| text.includes('游客名单')
|| text.includes('名单格式')
|| text.includes('娓稿');
});
const rowSummaries = rows.slice(Math.max(0, headerIndex - 3), Math.min(rows.length, headerIndex + 25))
.map((row, index) => {
const rowIndex = Math.max(0, headerIndex - 3) + index;
const rowText = String(row.innerText || row.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 180);
const controls = writable(row).map((el) => ({
tag: el.tagName,
id: el.id || '',
name: el.getAttribute('name') || '',
type: el.getAttribute('type') || '',
hasValue: Boolean(String(el.value || '').trim()),
}));
return {
index: rowIndex,
text: headerIndex >= 0 && rowIndex > headerIndex && /^\d+\s/.test(rowText)
? '[traveler row redacted]'
: rowText,
controlCount: controls.length,
controls,
};
});
return {
frameUrl: location.href,
headerIndex,
bodySnippet: headerIndex >= 0
? `traveler section detected at row ${headerIndex}`
: bodyText.slice(0, 1200),
rowSummaries,
};
});
}
function loadConfig(file) {
const fullPath = path.resolve(file);
return JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, ''));
}
async function launchContext(runtime) {
if (!isLiveSessionEnabled(runtime)) {
throw new Error('This diagnostic uses the configured live ERP Chrome session.');
}
return connectLiveContext(runtime);
}
async function setSearchFields(page, args) {
await page.evaluate((lookup) => {
const setValue = (selector, value) => {
const el = document.querySelector(selector);
if (!el) return false;
el.value = String(value || '');
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
return true;
};
const dateRadio = Array.from(document.querySelectorAll('input[name="riqi"]'))
.find((el) => ['chufari', '1'].includes(String(el.value || '')));
if (dateRadio) {
dateRadio.checked = true;
dateRadio.dispatchEvent(new Event('change', { bubbles: true }));
}
setValue('#S_chufariqi', lookup.departureDate);
setValue('#S_chufarizhi', lookup.departureDate);
setValue('#S_tuanxuhao', lookup.groupNo);
setValue('#S_kehuming', '');
setValue('#S_chanpinming', '');
}, args);
}
async function waitForGroupModifyLink(page, args) {
const deadline = Date.now() + 12000;
let lastRows = [];
while (Date.now() < deadline) {
const result = await page.evaluate((lookup) => {
const rows = Array.from(document.querySelectorAll('tr')).map((tr) => {
const text = String(tr.innerText || tr.textContent || '').replace(/\s+/g, ' ').trim();
const links = Array.from(tr.querySelectorAll('a')).map((a) => ({
text: String(a.innerText || a.textContent || '').replace(/\s+/g, ' ').trim(),
onclick: a.getAttribute('onclick') || '',
href: a.getAttribute('href') || '',
}));
return { text, links };
});
const groupRows = rows.filter((row) => row.text.includes(lookup.groupNo));
const modifyLink = groupRows
.flatMap((row) => row.links.map((link) => ({ ...link, rowText: row.text })))
.find((link) => link.onclick.includes('OPEN_update') || link.text.includes('修改'));
return { groupRows, modifyLink };
}, args);
lastRows = result.groupRows || [];
if (result.modifyLink) return result.modifyLink;
await page.waitForTimeout(500);
}
const error = new Error(`Team order modify link not found: ${args.groupNo}`);
error.rows = lastRows.slice(0, 5);
throw error;
}
async function waitForGroupDetailLink(page, args) {
const deadline = Date.now() + 12000;
let lastRows = [];
while (Date.now() < deadline) {
const result = await page.evaluate((lookup) => {
const rows = Array.from(document.querySelectorAll('tr')).map((tr) => {
const text = String(tr.innerText || tr.textContent || '').replace(/\s+/g, ' ').trim();
const links = Array.from(tr.querySelectorAll('a')).map((a) => ({
text: String(a.innerText || a.textContent || '').replace(/\s+/g, ' ').trim(),
onclick: a.getAttribute('onclick') || '',
href: a.getAttribute('href') || '',
title: a.getAttribute('title') || '',
}));
return { text, links };
});
const groupRows = rows.filter((row) => row.text.includes(lookup.groupNo));
const detailLink = groupRows
.flatMap((row) => row.links.map((link) => ({ ...link, rowText: row.text })))
.find((link) => link.text.includes(lookup.groupNo) || link.onclick.includes('OPEN_List'));
return { groupRows, detailLink };
}, args);
lastRows = result.groupRows || [];
if (result.detailLink) return result.detailLink;
await page.waitForTimeout(500);
}
const error = new Error(`Team order detail link not found: ${args.groupNo}`);
error.rows = lastRows.slice(0, 5);
throw error;
}
async function readGroupRowLinks(page, args) {
return page.evaluate((lookup) => {
const rows = Array.from(document.querySelectorAll('tr')).map((tr) => {
const text = String(tr.innerText || tr.textContent || '').replace(/\s+/g, ' ').trim();
const links = Array.from(tr.querySelectorAll('a')).map((a) => ({
text: String(a.innerText || a.textContent || '').replace(/\s+/g, ' ').trim(),
onclick: a.getAttribute('onclick') || '',
href: a.getAttribute('href') || '',
title: a.getAttribute('title') || '',
}));
return { text, links };
});
return rows.filter((row) => row.text.includes(lookup.groupNo)).slice(0, 3);
}, args);
}
function parseDdid(onclick = '') {
const match = String(onclick).match(/OPEN_update\('?(\d+)/i);
return match ? match[1] : '';
}
function parseDetailId(onclick = '') {
const match = String(onclick).match(/OPEN_List\([^,]+,\s*['"]?(\d+)/i);
return match ? match[1] : '';
}
async function waitForDetailFrame(page, detailId = '') {
for (let index = 0; index < 80; index += 1) {
const frame = page.frames().find((candidate) => {
const url = candidate.url();
if (!url.includes('orders_list.asp') && !url.includes('orders_view.asp')) return false;
return !detailId || url.includes(`id=${detailId}`) || url.includes(`did=${detailId}`) || url.includes(`tid=${detailId}`);
});
if (frame) return frame;
await page.waitForTimeout(500);
}
throw new Error(`Timed out waiting for team detail frame: ${detailId}`);
}
async function openDetailFrame(page, args, detailLink) {
const detailId = parseDetailId(detailLink.onclick);
await page.evaluate((lookup) => {
const rows = Array.from(document.querySelectorAll('tr'));
const row = rows.find((candidate) => String(candidate.innerText || '').includes(lookup.groupNo));
if (!row) throw new Error(`group row not found: ${lookup.groupNo}`);
const link = Array.from(row.querySelectorAll('a')).find((candidate) => {
const onclick = candidate.getAttribute('onclick') || '';
const text = String(candidate.innerText || candidate.textContent || '');
return text.includes(lookup.groupNo) || onclick.includes('OPEN_List');
});
if (!link) throw new Error(`detail link not found for ${lookup.groupNo}`);
link.click();
}, args);
return waitForDetailFrame(page, detailId);
}
async function main() {
const args = parseArgs();
const config = loadConfig(args.config);
const runtime = teamSingleOps.runtimeFromConfig(config);
const context = await launchContext(runtime);
const page = context.pages()[0] || await context.newPage();
page.setDefaultTimeout(30000);
const dialogState = installErpDialogHandler(page);
try {
await page.goto(runtime.ordersUrl, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {});
await page.waitForTimeout(1200);
await ensureErpSessionReady(page, dialogState, {
source: `team.travelerAudit:${args.groupNo}`,
targetUrl: runtime.ordersUrl,
timeoutMs: runtime.interactiveLoginWaitMs,
});
await setSearchFields(page, args);
await page.locator('#SearchButton').click();
let openedLink = null;
let ddid = '';
let detailId = '';
let frame = null;
const groupRows = args.debug ? await readGroupRowLinks(page, args) : [];
if (args.target === 'detail') {
openedLink = await waitForGroupDetailLink(page, args);
detailId = parseDetailId(openedLink.onclick);
frame = await openDetailFrame(page, args, openedLink);
} else {
openedLink = await waitForGroupModifyLink(page, args);
ddid = parseDdid(openedLink.onclick);
await page.evaluate((lookup) => {
const rows = Array.from(document.querySelectorAll('tr'));
const row = rows.find((candidate) => String(candidate.innerText || '').includes(lookup.groupNo));
if (!row) throw new Error(`group row not found: ${lookup.groupNo}`);
const link = Array.from(row.querySelectorAll('a')).find((candidate) => {
const onclick = candidate.getAttribute('onclick') || '';
const text = String(candidate.innerText || candidate.textContent || '');
return onclick.includes('OPEN_update') || text.includes('修改');
});
if (!link) throw new Error(`modify link not found: ${lookup.groupNo}`);
link.click();
}, args);
frame = await teamSingleOps.waitForSingleFrame(page, ddid);
}
const travelerState = await readTravelerTableState(frame);
const result = {
status: 'ok',
groupNo: args.groupNo,
departureDate: args.departureDate,
target: args.target,
ddid,
detailId,
openedLink: args.debug ? openedLink : undefined,
travelerState,
};
if (args.debug) result.debug = await readTravelerStructure(frame);
if (args.debug) result.groupRows = groupRows;
console.log(args.json ? JSON.stringify(result, null, 2) : result);
} finally {
await context.close().catch(() => {});
}
}
main().catch((error) => {
const payload = {
status: 'error',
message: error.message,
code: error.code || '',
rows: error.rows || undefined,
};
console.error(JSON.stringify(payload, null, 2));
process.exit(1);
});