306 lines
12 KiB
JavaScript
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);
|
|
});
|