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

242 lines
10 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');
const ROOT = path.resolve(__dirname, '..');
const PROFILE = path.join(ROOT, 'diagnostics', 'erp-playwright-profile');
const RUN_ID = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
const RUN_DIR = path.join(ROOT, 'diagnostics', 'single-team-execute', `run-${RUN_ID}`);
const LOG_FILE = path.join(RUN_DIR, 'run.log');
const ORDER = {
product: '遇见老挝',
departureDate: '2026-07-12',
pax: { adult: 2, childBed: 1, childNoBed: 1, infant: 0, leader: 0 },
rooms: { SGL: 0, TWN: 0, TRP: 0, DBL: 2, HNM: 0, TL: 0 },
prices: { adult: 500, childBed: 200, childNoBed: 100, infant: 0, leader: 0 },
op: '测试',
sales: '琳琳',
note: '测试团队单个下单,自动化测试后可删除。',
};
fs.mkdirSync(RUN_DIR, { recursive: true });
function log(message, data) {
const line = `[${new Date().toISOString()}] ${message}${data === undefined ? '' : ` ${JSON.stringify(data)}`}`;
fs.appendFileSync(LOG_FILE, `${line}\n`, 'utf8');
console.log(line);
}
async function shot(page, name) {
const file = path.join(RUN_DIR, `${name}.png`);
await page.screenshot({ path: file, fullPage: false });
log('screenshot', { file });
}
async function getOrderFrame(page) {
for (let i = 0; i < 60; i += 1) {
const frame = page.frames().find((f) => f.url().includes('/Business/orders_add.asp'));
if (frame) return frame;
await page.waitForTimeout(500);
}
throw new Error('orders_add.asp frame not found');
}
async function dumpFrame(frame, name) {
const data = await frame.evaluate(() => {
const valueOf = (id) => {
const el = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
return el ? el.value : null;
};
const values = {};
for (const id of [
'chufari', 'zutuanshe', 'zutuansheid', 'zhuanxianming', 'chanpinming',
'TianShu', 'tuanxuhao1', 'tuanxuhao2', 'gendanren', 'xiaoshouren',
'darenshu', 'xiaorenshu', 'ertrenshu', 'yingrenshu', 'quanrenshu',
'frenshu0', 'frenshu1', 'frenshu2', 'frenshu3', 'frenshu4', 'frenshu5',
'ys_danwei0', 'ys_xiangmu0', 'ys_bizhong0', 'ys_shuliang0', 'ys_danjia0', 'ys_jine0',
'ys_danwei1', 'ys_xiangmu1', 'ys_bizhong1', 'ys_shuliang1', 'ys_danjia1', 'ys_jine1',
'ys_danwei2', 'ys_xiangmu2', 'ys_bizhong2', 'ys_shuliang2', 'ys_danjia2', 'ys_jine2',
'ys_danwei3', 'ys_xiangmu3', 'ys_bizhong3', 'ys_shuliang3', 'ys_danjia3', 'ys_jine3',
]) values[id] = valueOf(id);
return {
url: location.href,
title: document.title,
values,
bodyText: document.body.innerText.replace(/\s+/g, ' ').slice(0, 2500),
};
});
const file = path.join(RUN_DIR, `${name}.json`);
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
log('dump', { file, values: data.values, body: data.bodyText.slice(0, 600) });
return data;
}
async function setField(frame, selector, value) {
const locator = frame.locator(selector).first();
await locator.waitFor({ state: 'attached', timeout: 15000 });
await locator.fill(String(value));
await locator.evaluate((el) => {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
});
}
async function clickAndWait(page, locator) {
await locator.click();
await page.waitForTimeout(1000);
}
function extractGroupNumber(text) {
const match = text.match(/[A-Z]{1,4}-\d{6,8}[A-Z]?(?:-[A-Z])?/);
return match ? match[0] : null;
}
async function readSuccessDialog(page) {
for (let i = 0; i < 60; i += 1) {
const result = await page.evaluate(() => {
const text = document.body.innerText.replace(/\s+/g, ' ').trim();
const okButtons = Array.from(document.querySelectorAll('button, input, a')).map((el) => ({
text: (el.innerText || el.value || '').replace(/\s+/g, ' ').trim(),
id: el.id || '',
cls: el.className || '',
}));
return { text, okButtons };
}).catch(() => ({ text: '', okButtons: [] }));
if (result.text.includes('下单成功') || result.text.includes('操作成功')) return result.text;
await page.waitForTimeout(500);
}
return '';
}
async function clickDialogOk(page) {
const candidates = [
'button:has-text("确定")',
'input[value="确定"]',
'a:has-text("确定")',
'.aui_state_highlight',
];
for (const selector of candidates) {
const locator = page.locator(selector).first();
if (await locator.count().catch(() => 0)) {
await locator.click().catch(() => {});
await page.waitForTimeout(500);
return true;
}
}
return false;
}
async function main() {
log('run-start', { RUN_DIR, ORDER });
const context = await chromium.launchPersistentContext(PROFILE, {
headless: false,
viewport: { width: 1920, height: 1000 },
acceptDownloads: true,
downloadsPath: path.join(RUN_DIR, 'downloads'),
});
const page = context.pages()[0] || await context.newPage();
page.setDefaultTimeout(15000);
const browserDialogs = [];
page.on('dialog', async (dialog) => {
const item = { type: dialog.type(), message: dialog.message() };
browserDialogs.push(item);
log('browser-dialog', item);
await dialog.accept().catch(() => {});
});
try {
await page.goto('https://ltjt.yunzhi.run/System/Business/orders.asp', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1000);
await shot(page, '01-orders');
await page.locator('input[onclick="OPEN_update(0,0,0)"]').click();
const frame = await getOrderFrame(page);
await frame.waitForSelector('#chanpinming, [name="chanpinming"]', { timeout: 30000 });
await page.waitForTimeout(1000);
await setField(frame, '#chanpinming, [name="chanpinming"]', ORDER.product);
await clickAndWait(page, frame.locator('#gengxinxingcheng, a[onclick*="Find_product"]').first());
await page.waitForTimeout(2200);
await shot(page, '02-after-product-update');
const afterProduct = await dumpFrame(frame, '02-after-product-update');
const requiredAfterProduct = {
zutuanshe: afterProduct.values.zutuanshe,
zhuanxianming: afterProduct.values.zhuanxianming,
chanpinming: afterProduct.values.chanpinming,
TianShu: afterProduct.values.TianShu,
tuanxuhao1: afterProduct.values.tuanxuhao1,
tuanxuhao2: afterProduct.values.tuanxuhao2,
gendanren: afterProduct.values.gendanren,
xiaoshouren: afterProduct.values.xiaoshouren,
};
const missing = Object.entries(requiredAfterProduct).filter(([, value]) => !String(value || '').trim()).map(([key]) => key);
if (missing.length) {
throw new Error(`required fields still empty after product update: ${missing.join(', ')}`);
}
await setField(frame, '#chufari, [name="chufari"]', ORDER.departureDate);
await setField(frame, '#gendanren, [name="gendanren"]', ORDER.op);
await setField(frame, '#xiaoshouren, [name="xiaoshouren"]', ORDER.sales);
await setField(frame, '#darenshu, [name="darenshu"]', ORDER.pax.adult);
await setField(frame, '#xiaorenshu, [name="xiaorenshu"]', ORDER.pax.childBed);
await setField(frame, '#ertrenshu, [name="ertrenshu"]', ORDER.pax.childNoBed);
await setField(frame, '#yingrenshu, [name="yingrenshu"]', ORDER.pax.infant);
await setField(frame, '#quanrenshu, [name="quanrenshu"]', ORDER.pax.leader);
await setField(frame, '#frenshu0, [name="frenshu0"]', ORDER.rooms.SGL);
await setField(frame, '#frenshu1, [name="frenshu1"]', ORDER.rooms.TWN);
await setField(frame, '#frenshu2, [name="frenshu2"]', ORDER.rooms.TRP);
await setField(frame, '#frenshu3, [name="frenshu3"]', ORDER.rooms.DBL);
await setField(frame, '#frenshu4, [name="frenshu4"]', ORDER.rooms.HNM);
await setField(frame, '#frenshu5, [name="frenshu5"]', ORDER.rooms.TL);
await setField(frame, '#xiadanbeizhu, [name="xiadanbeizhu"]', ORDER.note).catch(() => {});
const receivables = [
{ row: 0, qty: ORDER.pax.adult, price: ORDER.prices.adult },
{ row: 1, qty: ORDER.pax.childBed, price: ORDER.prices.childBed },
{ row: 2, qty: ORDER.pax.childNoBed, price: ORDER.prices.childNoBed },
{ row: 3, qty: ORDER.pax.infant, price: ORDER.prices.infant },
];
for (const item of receivables) {
await setField(frame, `#ys_shuliang${item.row}, [name="ys_shuliang${item.row}"]`, item.qty).catch(() => {});
await setField(frame, `#ys_danjia${item.row}, [name="ys_danjia${item.row}"]`, item.price).catch(() => {});
await setField(frame, `#ys_jine${item.row}, [name="ys_jine${item.row}"]`, item.qty * item.price).catch(() => {});
}
await page.waitForTimeout(1000);
await shot(page, '03-before-save');
const beforeSave = await dumpFrame(frame, '03-before-save');
const requiredBeforeSave = ['chufari', 'zutuanshe', 'zhuanxianming', 'chanpinming', 'TianShu', 'tuanxuhao1', 'tuanxuhao2', 'gendanren', 'xiaoshouren'];
const stillMissing = requiredBeforeSave.filter((key) => !String(beforeSave.values[key] || '').trim());
if (stillMissing.length) throw new Error(`required fields missing before save: ${stillMissing.join(', ')}`);
log('click-save');
await frame.locator('#SubmitButton, [name="SubmitButton"]').first().click();
await page.waitForTimeout(1500);
const successText = await readSuccessDialog(page);
await shot(page, '04-after-save');
log('success-dialog-text', successText);
const groupNumber = extractGroupNumber(successText);
if (!groupNumber) {
throw new Error(`could not extract group number from save dialog: ${successText.slice(0, 500)}`);
}
await clickDialogOk(page);
const result = { groupNumber, successText, browserDialogs, runDir: RUN_DIR };
fs.writeFileSync(path.join(RUN_DIR, 'result.json'), JSON.stringify(result, null, 2), 'utf8');
log('run-complete', result);
} catch (error) {
log('error', { message: error.message, stack: error.stack });
await shot(page, 'error').catch(() => {});
throw error;
} finally {
await context.close();
}
}
main().catch((error) => {
console.error(error);
process.exit(1);
});