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); });