Files
LWLT-AI/chrome-extension/ltjt-order-assistant/inpage.js
2026-07-13 19:57:46 +08:00

1951 lines
92 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function installLTJTOrderAssistant() {
'use strict';
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function toNumber(value, fallback = 0) {
const num = Number(value);
return Number.isFinite(num) ? num : fallback;
}
function integerString(value) {
return String(Math.max(0, Math.trunc(toNumber(value))));
}
function compactNumber(value) {
const num = toNumber(value);
if (Number.isInteger(num)) return String(num);
return String(Math.round((num + Number.EPSILON) * 100) / 100);
}
function dateYyyyMD(value) {
if (typeof value !== 'string') return value;
const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (!match) return value;
return `${match[1]}-${Number(match[2])}-${Number(match[3])}`;
}
function passengerTotal(counts = {}) {
return ['adult', 'child_bed', 'child_no_bed', 'infant', 'leader']
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(counts[key]))), 0);
}
function roomTotal(roomCounts = {}) {
return ['SGL', 'TWN', 'TRP', 'DBL', 'HNM', 'TL']
.reduce((sum, key) => sum + Math.max(0, Math.trunc(toNumber(roomCounts[key]))), 0);
}
function splitRows(text) {
const str = String(text || '');
if (!str) return [];
if (str.includes('◇')) return str.split('◇').map((row) => row.trim()).filter(Boolean);
if (/\r?\n/.test(str)) return str.split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
return [str.trim()].filter(Boolean);
}
function splitColumns(row) {
return String(row || '').split('◆').map((part) => part.trim());
}
function rows(text) {
return splitRows(text).map((row, rowIndex) => ({ rowIndex, columns: splitColumns(row) }));
}
function summarizeText(text) {
const parsedRows = splitRows(text);
const histogram = {};
parsedRows.slice(0, 200).forEach((row) => {
const count = splitColumns(row).length;
histogram[count] = (histogram[count] || 0) + 1;
});
return {
byte_length: new Blob([String(text || '')]).size,
row_count: parsedRows.length,
column_count_histogram_first_200_rows: histogram,
value_redacted: true
};
}
function exactMatches(text, expected, column) {
const expectedText = String(expected || '').trim();
if (!expectedText) return [];
return rows(text).filter((row) => String(row.columns[column] || '').trim() === expectedText);
}
function containsMatches(text, expected, column) {
const expectedText = String(expected || '').trim();
if (!expectedText) return [];
return rows(text).filter((row) => String(row.columns[column] || '').trim().includes(expectedText));
}
function selectUniqueExistingOption(text, expected, column, name) {
const exact = exactMatches(text, expected, column);
const contains = containsMatches(text, expected, column);
if (exact.length === 1) {
return {
match: exact[0],
rule: 'exact',
exact_count: exact.length,
contains_count: contains.length,
blocker: ''
};
}
if (exact.length === 0 && contains.length === 1) {
return {
match: contains[0],
rule: 'unique_contains',
exact_count: 0,
contains_count: contains.length,
blocker: ''
};
}
return {
match: null,
rule: exact.length > 1 ? 'ambiguous_exact' : 'blocked',
exact_count: exact.length,
contains_count: contains.length,
blocker: exact.length > 1
? `${name}: expected exactly one existing LTJT option, found multiple exact matches`
: `${name}: no exact match and unique contains fallback did not produce exactly one option`
};
}
async function sha256(text) {
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
}
function sha256Sync(ascii) {
function rightRotate(value, amount) {
return (value >>> amount) | (value << (32 - amount));
}
const mathPow = Math.pow;
const maxWord = mathPow(2, 32);
const lengthProperty = 'length';
let i;
let j;
const result = [];
const words = [];
const asciiBitLength = ascii[lengthProperty] * 8;
let hash = sha256Sync.h = sha256Sync.h || [];
let k = sha256Sync.k = sha256Sync.k || [];
let primeCounter = k[lengthProperty];
const isComposite = {};
for (let candidate = 2; primeCounter < 64; candidate += 1) {
if (!isComposite[candidate]) {
for (i = 0; i < 313; i += candidate) isComposite[i] = candidate;
hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0;
k[primeCounter] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
primeCounter += 1;
}
}
ascii += String.fromCharCode(0x80);
while (ascii[lengthProperty] % 64 - 56) ascii += String.fromCharCode(0);
for (i = 0; i < ascii[lengthProperty]; i += 1) {
j = ascii.charCodeAt(i);
if (j >> 8) throw new Error('sha256Sync only supports 8-bit input');
words[i >> 2] |= j << ((3 - i) % 4) * 8;
}
words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0);
words[words[lengthProperty]] = asciiBitLength;
for (j = 0; j < words[lengthProperty];) {
const w = words.slice(j, j += 16);
const oldHash = hash;
hash = hash.slice(0, 8);
for (i = 0; i < 64; i += 1) {
const w15 = w[i - 15];
const w2 = w[i - 2];
const a = hash[0];
const e = hash[4];
const temp1 = hash[7]
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25))
+ ((e & hash[5]) ^ ((~e) & hash[6]))
+ k[i]
+ (w[i] = (i < 16) ? w[i] : (
w[i - 16]
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3))
+ w[i - 7]
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
) | 0);
const temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22))
+ ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
hash = [(temp1 + temp2) | 0].concat(hash);
hash[4] = (hash[4] + temp1) | 0;
}
for (i = 0; i < 8; i += 1) hash[i] = (hash[i] + oldHash[i]) | 0;
}
for (i = 0; i < 8; i += 1) {
for (j = 3; j + 1; j -= 1) {
const b = (hash[i] >> (j * 8)) & 255;
result.push((b < 16 ? '0' : '') + b.toString(16));
}
}
return result.join('');
}
function isLoginText(text) {
return /登陆|登录|login/i.test(String(text || ''));
}
function visibleElement(el) {
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden' && el.getClientRects().length > 0;
}
function loadingRoot(el) {
let current = el;
let root = el;
for (let depth = 0; depth < 5 && current?.parentElement; depth += 1) {
const parent = current.parentElement;
const className = String(parent.className || '');
const position = window.getComputedStyle(parent).position;
const parentText = String(parent.textContent || '').trim();
if (
/dialog|window|messager|loading|mask|aui|layui|panel/i.test(className)
|| (['absolute', 'fixed'].includes(position) && parentText.length <= 80)
) {
root = parent;
current = parent;
} else {
break;
}
}
return root;
}
function closeLingeringLoading(jq) {
const cleanup = {
attempted: [],
closed_by_api: 0,
removed_elements: 0,
errors: []
};
try {
if (jq?.messager?.progress) {
jq.messager.progress('close');
cleanup.attempted.push('jquery_easyui_messager_progress_close');
cleanup.closed_by_api += 1;
}
} catch (error) {
cleanup.errors.push(`messager.progress close failed: ${String(error.message || error).slice(0, 120)}`);
}
try {
if (jq?.dialog?.list && typeof jq.dialog.list === 'object') {
Object.values(jq.dialog.list).forEach((dialog) => {
const text = String(dialog?.DOM?.content?.[0]?.textContent || dialog?.content || '').trim();
if (/loading|加载|处理中|请稍候/i.test(text) && typeof dialog.close === 'function') {
dialog.close();
cleanup.closed_by_api += 1;
}
});
cleanup.attempted.push('jquery_dialog_list_close_loading');
}
} catch (error) {
cleanup.errors.push(`dialog list close failed: ${String(error.message || error).slice(0, 120)}`);
}
const selector = [
'.datagrid-mask',
'.datagrid-mask-msg',
'.window-mask',
'.window-shadow',
'.messager-window',
'.layui-layer-loading',
'.layui-layer-shade',
'.aui_state_lock',
'.aui_outer',
'.aui_dialog'
].join(',');
const roots = new Set();
document.querySelectorAll(selector).forEach((el) => {
if (visibleElement(el)) roots.add(el);
});
document.querySelectorAll('div, span, td').forEach((el) => {
const text = String(el.textContent || '').trim();
if (/^loading\.{0,3}$/i.test(text) || /^(加载中|处理中|请稍候)\.{0,3}$/i.test(text)) {
if (visibleElement(el)) roots.add(loadingRoot(el));
}
});
roots.forEach((el) => {
try {
el.remove();
cleanup.removed_elements += 1;
} catch (error) {
cleanup.errors.push(`remove loading element failed: ${String(error.message || error).slice(0, 120)}`);
}
});
if (roots.size) cleanup.attempted.push('remove_visible_loading_nodes');
return cleanup;
}
function returnToOrderList({ marker = '', dateFrom = '', dateTo = '' } = {}) {
closeLingeringLoading(window.jQuery || window.$);
const url = new URL('/System/Business/orders.asp', location.origin);
const normalizedFrom = dateYyyyMD(dateFrom || '');
const normalizedTo = dateYyyyMD(dateTo || dateFrom || '');
if (normalizedFrom) {
url.searchParams.set('riqi', 'chufari');
url.searchParams.set('S_chufariqi', normalizedFrom);
url.searchParams.set('S_chufarizhi', normalizedTo || normalizedFrom);
}
if (marker) url.searchParams.set('S_tuanxuhao', marker);
url.searchParams.set('wode', '0');
location.href = url.href;
return {
status: 'return_to_order_list_requested',
marker,
date_range: {
from: normalizedFrom,
to: normalizedTo || normalizedFrom
},
target_url_redacted: '/System/Business/orders.asp'
};
}
function operationIdentifier(operation = {}) {
const data = operation.data || {};
const refs = data.existing_refs || {};
return String(
operation.identifier
|| refs.identifier
|| refs.order_no
|| refs.group_no
|| refs.child_order_no
|| refs.parent_group_no
|| ''
).trim();
}
function extractGroupNumber(text) {
return String(text || '').match(/[A-Z]{1,4}-\d{6,8}[A-Z0-9]*(?:-[A-Z0-9][A-Z0-9_-]*)*/i)?.[0] || '';
}
function extractDdid(links = []) {
for (const link of links) {
const source = `${link.onclick || ''} ${link.href || ''}`;
const match = source.match(/OPEN_update\(['"]?([^,'")]+)|[?&]ddid=([^&#]+)/i);
const value = match?.[1] || match?.[2] || '';
if (value) return value;
}
return '';
}
function extractTid(text, links = []) {
const source = `${text || ''} ${links.map((link) => `${link.href || ''} ${link.onclick || ''}`).join(' ')}`;
return source.match(/[?&]tid=(\d+)/i)?.[1]
|| source.match(/OPEN_finance_list\(\s*[^,]+,\s*["']?(\d+)/i)?.[1]
|| source.match(/OPEN_List\(\s*["']?[^,"')]+["']?\s*,\s*["']?(\d+)/i)?.[1]
|| '';
}
function readVisibleOrderRows() {
return 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((link) => ({
text: String(link.innerText || link.textContent || '').replace(/\s+/g, ' ').trim(),
href: link.getAttribute('href') || '',
onclick: link.getAttribute('onclick') || '',
}));
const groupNo = extractGroupNumber(text);
return {
group_no: groupNo,
ddid: extractDdid(links),
tid: extractTid(text, links),
row_text_length: text.length,
has_modify_link: links.some((link) => /修改|OPEN_update/i.test(`${link.text} ${link.onclick}`)),
has_detail_link: links.some((link) => /orders_view|查看|详情/i.test(`${link.text} ${link.href}`)),
};
}).filter((row) => row.row_text_length > 0 && (row.group_no || row.has_modify_link || row.has_detail_link));
}
function inspectOrderList(operation = {}, adapter = 'inspect_existing_order_update') {
const identifier = operationIdentifier(operation);
const path = location.pathname.toLowerCase();
if (!/\/system\/business\/orders\.asp$/i.test(path)) {
return {
status: 'erp_page_mismatch',
adapter,
expected_path: '/System/Business/orders.asp',
current_path: location.pathname,
identifier,
no_erp_write: true,
blockers: ['请在独立团计划表页面执行订单定位预检。']
};
}
const rows = readVisibleOrderRows();
const matches = identifier
? rows.filter((row) => row.group_no === identifier || row.group_no.includes(identifier))
: [];
return {
status: matches.length === 1 ? 'erp_order_candidate_unique' : (matches.length > 1 ? 'erp_order_candidate_ambiguous' : 'erp_order_candidate_not_found'),
adapter,
identifier,
candidate_count: matches.length,
visible_row_count: rows.length,
candidates: matches,
page: { path: location.pathname, title: document.title },
no_erp_write: true,
write_attempted: false,
};
}
function inspectSplitParentPage(operation = {}) {
const path = location.pathname.toLowerCase();
const supportedPage = /\/system\/business\/plan(?:_add)?\.asp$/i.test(path);
const controls = Array.from(document.querySelectorAll('input,select,textarea,button,a'));
return {
status: supportedPage ? 'split_parent_page_detected' : 'erp_page_mismatch',
adapter: 'inspect_split_parent_plan',
current_path: location.pathname,
expected_paths: ['/System/Business/plan.asp', '/System/Business/plan_add.asp'],
control_summary: {
input_count: controls.filter((el) => el.tagName.toLowerCase() === 'input').length,
select_count: controls.filter((el) => el.tagName.toLowerCase() === 'select').length,
textarea_count: controls.filter((el) => el.tagName.toLowerCase() === 'textarea').length,
add_plan_control_count: controls.filter((el) => /新增计划|新建计划/i.test(String(el.innerText || el.value || ''))).length,
submit_control_count: controls.filter((el) => /保存|提交|确定/i.test(String(el.innerText || el.value || ''))).length,
},
requested_dates: operation.data?.departure_dates || [],
no_erp_write: true,
write_attempted: false,
};
}
function inspectSplitChildPage(operation = {}) {
const path = location.pathname.toLowerCase();
const supportedPage = /\/system\/business\/(?:plan|plan_order|orders_add)\.asp$/i.test(path);
const identifier = operationIdentifier(operation);
const rows = readVisibleOrderRows();
const parent = identifier ? rows.filter((row) => row.group_no === identifier || row.group_no.includes(identifier)) : [];
return {
status: supportedPage ? 'split_child_page_detected' : 'erp_page_mismatch',
adapter: 'inspect_split_child_order',
current_path: location.pathname,
identifier,
parent_candidate_count: parent.length,
parent_candidates: parent,
no_erp_write: true,
write_attempted: false,
};
}
function inspectOperationContext(operation = {}) {
if (/login/i.test(location.href) || /登录|登陆/i.test(document.title || '')) {
return { status: 'login_required', no_erp_write: true, blockers: ['login_required'] };
}
switch (String(operation.action || '')) {
case 'order_update':
return inspectOrderList(operation, 'inspect_existing_order_update');
case 'passenger_list_import':
return {
...inspectOrderList(operation, 'inspect_existing_order_traveler'),
attachment_count: Array.isArray(operation.data?.attachments) ? operation.data.attachments.length : 0,
traveler_action_count: Array.isArray(operation.data?.updatePlan?.actions)
? operation.data.updatePlan.actions.filter((item) => /^travelerList/i.test(String(item?.target || ''))).length
: 0,
import_write_attempted: false,
};
case 'confirmation_export':
return inspectOrderList(operation, 'inspect_confirmation_sources');
case 'shared_plan_create':
return inspectSplitParentPage(operation);
case 'shared_child_order_create':
return inspectSplitChildPage(operation);
default:
return {
status: 'operation_context_not_required',
current_path: location.pathname,
action: operation.action || '',
no_erp_write: true,
write_attempted: false,
};
}
}
function sameOriginDocuments(rootDocument = document, depth = 0, output = []) {
if (!rootDocument || depth > 6) return output;
output.push(rootDocument);
Array.from(rootDocument.querySelectorAll?.('iframe,frame') || []).forEach((frame) => {
try {
if (frame.contentDocument) sameOriginDocuments(frame.contentDocument, depth + 1, output);
} catch (error) {
// Cross-origin or unloaded frames are intentionally ignored.
}
});
return output;
}
function documentPath(rootDocument = document, pattern) {
const documents = sameOriginDocuments(rootDocument);
return documents.find((candidate) => {
try {
return pattern.test(candidate.location.pathname);
} catch (error) {
return false;
}
}) || null;
}
function mainIframe() {
return document.getElementById('Iframe_Home')
|| document.querySelector('iframe[name="MainIframe"]');
}
function mainIframeDocument() {
try {
return mainIframe()?.contentDocument || null;
} catch (error) {
return null;
}
}
async function waitForDocumentPath(pattern, timeoutMs = 20000) {
const deadline = Date.now() + timeoutMs;
let last = null;
while (Date.now() < deadline) {
last = documentPath(document, pattern);
if (last?.readyState === 'complete' || last?.querySelector('body')) return last;
await sleep(250);
}
return last;
}
async function waitForCondition(condition, timeoutMs = 20000, intervalMs = 250) {
const deadline = Date.now() + timeoutMs;
let last = null;
while (Date.now() < deadline) {
last = await condition();
if (last) return last;
await sleep(intervalMs);
}
return null;
}
function setDomField(scopeDocument, idOrName, value, options = {}) {
const selector = idOrName.startsWith('#') || idOrName.startsWith('[')
? idOrName
: `#${idOrName}, [name="${idOrName}"]`;
const element = scopeDocument.querySelector(selector);
if (!element) {
if (options.optional) return false;
throw new Error(`missing ERP field: ${idOrName}`);
}
const next = String(value ?? '');
element.value = next;
const ownerWindow = scopeDocument.defaultView || window;
['input', 'change', 'blur'].forEach((eventName) => {
element.dispatchEvent(new ownerWindow.Event(eventName, { bubbles: true }));
});
return true;
}
function textOf(element) {
return String(element?.innerText || element?.textContent || '').replace(/\s+/g, ' ').trim();
}
function parseErpResponseText(value) {
return String(value || '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr)>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.trim();
}
function extractOrderNumbers(value) {
return [...new Set(String(value || '').match(/D\d{4,}/gi) || [])];
}
function isSuccessResponse(value) {
return /操作成功|下单成功|修改成功|成功|success|สำเร็จ/i.test(parseErpResponseText(value));
}
function captureAjaxSubmit(scopeDocument, endpointPattern, submitFunctionName, options = {}) {
const ownerWindow = scopeDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
const capture = {
requests: [],
alerts: [],
native_alerts: [],
ajax_events: [],
invoked: false,
completed: false,
invoke_error: ''
};
if (!jq || typeof jq.ajax !== 'function') {
capture.invoke_error = 'jquery_ajax_not_found';
return Promise.resolve(capture);
}
const originalAjax = jq.ajax;
const originalDialogAlert = jq.dialog?.alert;
const originalNativeAlert = ownerWindow.alert;
const safeFieldNames = options.safeFieldNames || [];
const restore = () => {
jq.ajax = originalAjax;
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
ownerWindow.alert = originalNativeAlert;
};
if (jq.dialog) {
jq.dialog.alert = (...args) => capture.alerts.push(parseErpResponseText(args[0] || ''));
}
ownerWindow.alert = (...args) => capture.native_alerts.push(parseErpResponseText(args[0] || ''));
jq.ajax = function ajaxCapture(optionsArg, ...rest) {
const config = typeof optionsArg === 'string'
? { ...(rest[0] || {}), url: optionsArg }
: { ...(optionsArg || {}) };
const url = String(config.url || '');
const requestData = String(config.data || '');
const isTarget = endpointPattern.test(url) && (!options.requestPrefix || requestData.startsWith(options.requestPrefix));
if (!isTarget) return originalAjax.call(this, optionsArg, ...rest);
const params = new URLSearchParams(requestData.replace(/^Act=[^&]+&/, ''));
const eventBase = () => ({
status: 0,
text_status: '',
response_text: '',
response_bytes: 0,
error: ''
});
capture.requests.push({
url: url.replace(/^https?:\/\/[^/]+/i, ''),
method: config.type || config.method || 'GET',
data_length: new Blob([requestData]).size,
field_count: [...params.keys()].length,
fields: Object.fromEntries(safeFieldNames.map((name) => [name, name === 'Act'
? [requestData.match(/^Act=([^&]+)/)?.[1] || '']
: params.getAll(name).slice(0, 6)]))
});
const originalSuccess = config.success;
const originalError = config.error;
const originalComplete = config.complete;
config.success = function successCapture(data, textStatus, jqXHR) {
const event = eventBase();
event.event = 'success';
event.status = Number(jqXHR?.status || 0);
event.text_status = String(textStatus || '');
event.response_text = String(jqXHR?.responseText || data || '').slice(0, 4000);
event.response_bytes = new Blob([event.response_text]).size;
capture.ajax_events.push(event);
capture.completed = true;
if (typeof originalSuccess === 'function') return originalSuccess.apply(this, arguments);
return undefined;
};
config.error = function errorCapture(jqXHR, textStatus, errorThrown) {
const event = eventBase();
event.event = 'error';
event.status = Number(jqXHR?.status || 0);
event.text_status = String(textStatus || '');
event.response_text = String(jqXHR?.responseText || '').slice(0, 4000);
event.response_bytes = new Blob([event.response_text]).size;
event.error = String(errorThrown?.message || errorThrown || '');
capture.ajax_events.push(event);
capture.completed = true;
if (typeof originalError === 'function') return originalError.apply(this, arguments);
return undefined;
};
config.complete = function completeCapture(jqXHR, textStatus) {
if (!capture.ajax_events.length || capture.ajax_events[capture.ajax_events.length - 1].event !== 'success') {
const event = eventBase();
event.event = 'complete';
event.status = Number(jqXHR?.status || 0);
event.text_status = String(textStatus || '');
event.response_text = String(jqXHR?.responseText || '').slice(0, 4000);
event.response_bytes = new Blob([event.response_text]).size;
capture.ajax_events.push(event);
}
capture.completed = true;
if (typeof originalComplete === 'function') return originalComplete.apply(this, arguments);
return undefined;
};
return originalAjax.call(this, config, ...rest);
};
try {
if (typeof ownerWindow[submitFunctionName] !== 'function') throw new Error(`${submitFunctionName}_not_found`);
capture.invoked = true;
ownerWindow[submitFunctionName]();
} catch (error) {
capture.invoke_error = String(error.message || error);
capture.completed = true;
}
const waitMs = Number(options.waitMs || 20000);
return (async () => {
const deadline = Date.now() + waitMs;
while (!capture.completed && Date.now() < deadline) await sleep(250);
if (!capture.completed) capture.timed_out = true;
restore();
const responseTexts = [
...capture.alerts,
...capture.native_alerts,
...capture.ajax_events.map((event) => event.response_text)
].filter(Boolean);
const joined = responseTexts.join('\n');
capture.success = isSuccessResponse(joined);
capture.login_timeout = isLoginText(joined);
capture.permission_error = /权限|permission/i.test(joined);
capture.order_numbers = extractOrderNumbers(joined);
capture.response_preview = parseErpResponseText(joined).slice(0, 1200);
return capture;
})();
}
function selectLookupFromInput(input, expected, valueColumn = 0) {
const expectedText = String(expected || '').trim();
const candidates = rows(input?.getAttribute('data') || '')
.filter((row) => String(row.columns[valueColumn] || '').trim() === expectedText);
if (candidates.length !== 1) {
return { ok: false, exact_count: candidates.length, expected: expectedText };
}
return { ok: true, row: candidates[0], exact_count: candidates.length, expected: expectedText };
}
function selectProductRadio(scopeDocument, data) {
const productName = String(data.product?.name || '').trim();
const cpid = String(data.product?.cpid || data.product?.id || '').trim();
let candidates = Array.from(scopeDocument.querySelectorAll('input[name="cp_id"]')).map((input) => {
const row = input.closest('tr') || input.parentElement?.parentElement || input.parentElement;
return { input, text: textOf(row), cpid: String(input.value || '').trim() };
});
if (cpid) candidates = candidates.filter((candidate) => candidate.cpid === cpid);
if (productName) candidates = candidates.filter((candidate) => candidate.text.includes(productName));
if (candidates.length !== 1) {
return { ok: false, blocker: `product lookup expected exactly one radio, found ${candidates.length}`, candidates: candidates.map((candidate) => candidate.cpid) };
}
candidates[0].input.click();
return { ok: true, cpid: candidates[0].cpid, label_length: candidates[0].text.length };
}
function fillCustomerLookup(scopeDocument, customer, fieldName = 'zutuanshe') {
const expected = String(customer?.name || '').trim();
const input = scopeDocument.querySelector(`#${fieldName}, [name="${fieldName}"]`);
if (!input) return { ok: false, blocker: `${fieldName}_field_missing` };
const selected = selectLookupFromInput(input, expected, 2);
if (!selected.ok) return { ok: false, blocker: `customer lookup expected exactly one option, found ${selected.exact_count}` };
const columns = selected.row.columns;
setDomField(scopeDocument, fieldName, columns[2] || expected);
setDomField(scopeDocument, `${fieldName}id`, columns[1] || customer.id || '', { optional: true });
setDomField(scopeDocument, `${fieldName}gzr`, columns[4] || '', { optional: true });
setDomField(scopeDocument, 'lianxiren', columns[3] || customer.contact || '', { optional: true });
setDomField(scopeDocument, 'bizhong', columns[0] || 'CNY', { optional: true });
return { ok: true, column_count: columns.length };
}
function fillStaffLookup(scopeDocument, fieldName, expected) {
const input = scopeDocument.querySelector(`#${fieldName}, [name="${fieldName}"]`);
if (!input) return { ok: false, blocker: `${fieldName}_field_missing` };
const selected = selectLookupFromInput(input, expected, 0);
if (!selected.ok) return { ok: false, blocker: `${fieldName} lookup expected exactly one option, found ${selected.exact_count}` };
setDomField(scopeDocument, fieldName, selected.row.columns[0] || expected);
return { ok: true, column_count: selected.row.columns.length };
}
async function ensureMainPage(path, pattern, query = {}) {
const frame = mainIframe();
if (!frame) throw new Error('main_iframe_not_found');
let current = mainIframeDocument();
if (!current || !pattern.test(current.location.pathname)) {
const url = new URL(path, location.origin);
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null && String(value) !== '') url.searchParams.set(key, String(value));
});
url.searchParams.set('_', String(Date.now()));
frame.src = url.href;
current = await waitForCondition(async () => {
const candidate = mainIframeDocument();
return candidate && pattern.test(candidate.location.pathname) ? candidate : null;
}, 30000, 300);
}
if (!current) throw new Error(`main_page_not_ready:${path}`);
return current;
}
async function searchMainPage(scopeDocument, values = {}) {
const setIfPresent = (name, value) => {
const element = scopeDocument.querySelector(`#${name}, [name="${name}"]`);
if (!element) return false;
setDomField(scopeDocument, name, value);
return true;
};
Object.entries(values).forEach(([name, value]) => setIfPresent(name, value));
const button = scopeDocument.querySelector('#SearchButton, [name="SearchButton"]');
if (button) button.click();
await sleep(1000);
return scopeDocument;
}
function rowForIdentifier(scopeDocument, identifier) {
const expected = String(identifier || '').trim();
return Array.from(scopeDocument.querySelectorAll('tr')).find((row) => {
const text = textOf(row);
return expected && (text.includes(expected) || extractGroupNumber(text) === expected);
}) || null;
}
function closeDialogDocument(scopeDocument) {
try {
if (scopeDocument?.defaultView?.frameElement?.api?.close) {
scopeDocument.defaultView.frameElement.api.close();
return true;
}
} catch (error) {
return false;
}
return false;
}
async function createSplitParentLive(operation = {}) {
const data = operation.data || {};
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
const blockers = [];
if (dates.length !== 1) blockers.push('split parent live adapter currently requires exactly one departure date');
if (Number(data.groups_per_date || data.plans_per_date || 1) !== 1) blockers.push('split parent live adapter currently requires one plan per date');
if (!data.product?.name) blockers.push('split parent product is missing');
if (!data.customer?.name) blockers.push('split parent customer is missing');
if (blockers.length) return { status: 'split_parent_live_blocked', blockers, no_erp_write: true };
let listDocument = await ensureMainPage('/System/Business/plan.asp', /\/system\/business\/plan\.asp$/i, {
S_chufariqi: dateYyyyMD(dates[0]),
S_chufarizhi: dateYyyyMD(dates[0])
});
const addButton = listDocument.querySelector('#NewBuildB, [name="NewBuildB"]');
if (!addButton) return { status: 'split_parent_live_blocked', blockers: ['split parent add button not found'], no_erp_write: true };
addButton.click();
const formDocument = await waitForDocumentPath(/\/system\/business\/plan_add\.asp$/i, 30000);
if (!formDocument) return { status: 'split_parent_live_blocked', blockers: ['split parent form did not load'], no_erp_write: true };
await waitForCondition(() => {
const productInputs = formDocument.querySelectorAll('input[name="cp_id"]');
const customerInput = formDocument.querySelector('#zutuanshe, [name="zutuanshe"]');
return productInputs.length > 0 && String(customerInput?.getAttribute('data') || '').length > 0 ? formDocument : null;
}, 15000, 300);
const form = formDocument.querySelector('#InfoForm1, form');
if (!form) return { status: 'split_parent_live_blocked', blockers: ['split parent form missing'], no_erp_write: true };
const lookupChecks = [];
setDomField(formDocument, 'Riqi1', dateYyyyMD(dates[0]));
setDomField(formDocument, 'Riqi2', dateYyyyMD(dates[0]));
setDomField(formDocument, 'jihuashu', data.planned_capacity || 0);
const customer = fillCustomerLookup(formDocument, data.customer);
lookupChecks.push({ name: 'customer', ok: customer.ok, exact_match_count: customer.ok ? 1 : 0 });
const op = fillStaffLookup(formDocument, 'jiedairen', data.op_user?.name);
const sales = fillStaffLookup(formDocument, 'xiaoshouren', data.sales_user?.name);
lookupChecks.push({ name: 'op_user', ok: op.ok, exact_match_count: op.ok ? 1 : 0 });
lookupChecks.push({ name: 'sales_user', ok: sales.ok, exact_match_count: sales.ok ? 1 : 0 });
const product = selectProductRadio(formDocument, data);
if (!customer.ok || !op.ok || !sales.ok || !product.ok) {
closeDialogDocument(formDocument);
return { status: 'split_parent_live_blocked', blockers: [customer.blocker, op.blocker, sales.blocker, product.blocker].filter(Boolean), lookup_checks: lookupChecks, no_erp_write: true };
}
if (typeof formDocument.defaultView.ShowFoucsBox === 'function') formDocument.defaultView.ShowFoucsBox();
await sleep(100);
const dateBoxes = Array.from(formDocument.querySelectorAll('input[name="zhidingzhouqi"]'));
dateBoxes.forEach((input, index) => { input.checked = index === 0; });
setDomField(formDocument, 'tuanxuhao1', formDocument.querySelector('#tuanxuhao1')?.value || 'LW', { optional: true });
setDomField(formDocument, 'tuanxuhao2', data.test_marker || 'CDP-SPLIT-PARENT-TEST');
const capture = await captureAjaxSubmit(formDocument, /\/System\/DAT\/plan\.asp/i, 'SubmitInfoForm', {
requestPrefix: 'Act=DoInfoSPs&',
safeFieldNames: ['Act', 'Riqi1', 'Riqi2', 'jihuashu', 'tuanxuhao1', 'tuanxuhao2', 'zutuanshe', 'zutuansheid', 'jiedairen', 'xiaoshouren', 'cp_id', 'zhidingzhouqi'],
waitMs: 15000
});
const groupNumber = capture.order_numbers.find((value) => /[A-Z]{1,4}-\d{6,}/i.test(value)) || capture.response_preview.match(/[A-Z]{1,4}-\d{6,}[A-Z0-9_-]*/i)?.[0] || '';
closeDialogDocument(formDocument);
listDocument = mainIframeDocument() || listDocument;
if (groupNumber) {
await searchMainPage(listDocument, { S_chufariqi: dateYyyyMD(dates[0]), S_chufarizhi: dateYyyyMD(dates[0]), S_tuanxuhao: data.test_marker || groupNumber });
}
const row = groupNumber ? rowForIdentifier(listDocument, groupNumber) : null;
const verified = Boolean(groupNumber && row && textOf(row).includes(groupNumber));
return {
status: verified && capture.success ? 'split_parent_completed' : (capture.timed_out ? 'split_parent_live_uncertain' : 'split_parent_live_failed'),
no_erp_write: false,
write_attempted: true,
lookup_checks: lookupChecks,
submit: capture,
erp_receipt: groupNumber ? { group_number: groupNumber, departure_date: dateYyyyMD(dates[0]), planned_capacity: Number(data.planned_capacity || 0) } : undefined,
verification: { status: verified ? 'parent_group_found' : 'parent_group_not_found', row_text_length: row ? textOf(row).length : 0 },
blockers: verified && capture.success ? [] : ['split parent save was not confirmed by success response and list requery']
};
}
async function createSplitChildLive(operation = {}) {
const data = operation.data || {};
const parentGroup = String(data.parent_group_no || data.existing_refs?.parent_group_no || '').trim();
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
const blockers = [];
if (!parentGroup) blockers.push('split child parent_group_no is missing');
if (dates.length !== 1) blockers.push('split child live adapter currently requires exactly one departure date');
if (!data.customer?.name) blockers.push('split child customer is missing');
if (passengerTotal(data.passenger_counts || {}) <= 0) blockers.push('split child passenger total must be greater than zero');
if (blockers.length) return { status: 'split_child_live_blocked', blockers, no_erp_write: true };
let listDocument = await ensureMainPage('/System/Business/plan.asp', /\/system\/business\/plan\.asp$/i, {
S_chufariqi: dateYyyyMD(dates[0]),
S_chufarizhi: dateYyyyMD(dates[0]),
S_tuanxuhao: parentGroup
});
await searchMainPage(listDocument, { S_chufariqi: dateYyyyMD(dates[0]), S_chufarizhi: dateYyyyMD(dates[0]), S_tuanxuhao: parentGroup });
const parentRow = rowForIdentifier(listDocument, parentGroup);
if (!parentRow) return { status: 'split_child_live_blocked', blockers: ['parent group not found on split plan list'], no_erp_write: true };
const childButton = Array.from(parentRow.querySelectorAll('a')).find((link) => /OPEN_update\([^,]+,\s*['"]?0/i.test(link.getAttribute('onclick') || ''));
if (!childButton) return { status: 'split_child_live_blocked', blockers: ['split child entry link not found on parent row'], no_erp_write: true };
const parentTid = extractTid(textOf(parentRow), Array.from(parentRow.querySelectorAll('a')).map((link) => ({ text: textOf(link), href: link.getAttribute('href') || '', onclick: link.getAttribute('onclick') || '' })));
childButton.click();
const formDocument = await waitForDocumentPath(/\/system\/business\/plan_order\.asp$/i, 30000);
if (!formDocument) return { status: 'split_child_live_blocked', blockers: ['split child form did not load'], no_erp_write: true };
await waitForCondition(() => {
const lookupInputs = ['zutuanshe', 'gendanren', 'xiaoshouren']
.map((name) => formDocument.querySelector(`#${name}, [name="${name}"]`));
return lookupInputs.every((input) => String(input?.getAttribute('data') || '').length > 0) ? formDocument : null;
}, 15000, 300);
const lookupChecks = [];
const customer = fillCustomerLookup(formDocument, data.customer);
const op = fillStaffLookup(formDocument, 'gendanren', data.op_user?.name);
const sales = fillStaffLookup(formDocument, 'xiaoshouren', data.sales_user?.name);
lookupChecks.push({ name: 'customer', ok: customer.ok, exact_match_count: customer.ok ? 1 : 0 });
lookupChecks.push({ name: 'op_user', ok: op.ok, exact_match_count: op.ok ? 1 : 0 });
lookupChecks.push({ name: 'sales_user', ok: sales.ok, exact_match_count: sales.ok ? 1 : 0 });
if (!customer.ok || !op.ok || !sales.ok) {
closeDialogDocument(formDocument);
return { status: 'split_child_live_blocked', blockers: [customer.blocker, op.blocker, sales.blocker].filter(Boolean), lookup_checks: lookupChecks, no_erp_write: true };
}
const counts = data.passenger_counts || {};
[['darenshu', counts.adult], ['xiaorenshu', counts.child_bed], ['ertrenshu', counts.child_no_bed], ['yingrenshu', counts.infant], ['quanrenshu', counts.leader]].forEach(([name, value]) => setDomField(formDocument, name, integerString(value)));
if (typeof formDocument.defaultView.GetVisitorsHtml === 'function') formDocument.defaultView.GetVisitorsHtml();
const prices = data.prices || {};
const priceRows = [
['adult', 0, counts.adult],
['child_bed', 1, counts.child_bed],
['child_no_bed', 2, counts.child_no_bed],
['infant', 3, counts.infant],
['leader', 5, counts.leader]
];
priceRows.forEach(([key, index, quantity]) => {
setDomField(formDocument, `ys_danwei${index}`, data.customer.name, { optional: true });
setDomField(formDocument, `ys_danweiid${index}`, data.customer.id || '', { optional: true });
setDomField(formDocument, `ys_shuliang${index}`, integerString(quantity), { optional: true });
setDomField(formDocument, `ys_danjia${index}`, compactNumber(prices[key] || 0), { optional: true });
setDomField(formDocument, `ys_jine${index}`, compactNumber(toNumber(quantity) * toNumber(prices[key] || 0)), { optional: true });
});
if (typeof formDocument.defaultView.sum_jiesuan === 'function') formDocument.defaultView.sum_jiesuan('ys');
setDomField(formDocument, 'xiadanbeizhu', data.test_marker || data.special_requests || 'CDP-SPLIT-CHILD-TEST', { optional: true });
const capture = await captureAjaxSubmit(formDocument, /\/System\/DAT\/plan\.asp/i, 'SubmitInfoForm', {
requestPrefix: 'Act=DoInfo_order&',
safeFieldNames: ['Act', 'tdid', 'ddid', 'zutuanshe', 'zutuansheid', 'gendanren', 'xiaoshouren', 'darenshu', 'xiaorenshu', 'ertrenshu', 'yingrenshu', 'quanrenshu', 'xiadanbeizhu'],
waitMs: 20000
});
const childOrder = capture.order_numbers.find((value) => /^D\d{4,}$/i.test(value)) || '';
closeDialogDocument(formDocument);
listDocument = mainIframeDocument() || listDocument;
if (parentTid && typeof listDocument.defaultView?.AjaxLoadDataOne === 'function') listDocument.defaultView.AjaxLoadDataOne(parentTid);
await sleep(1000);
const refreshedParentRow = rowForIdentifier(listDocument, parentGroup);
const verified = Boolean(childOrder && refreshedParentRow && textOf(refreshedParentRow).includes(childOrder));
return {
status: verified && capture.success ? 'split_child_completed' : (capture.timed_out ? 'split_child_live_uncertain' : 'split_child_live_failed'),
no_erp_write: false,
write_attempted: true,
lookup_checks: lookupChecks,
submit: capture,
erp_receipt: childOrder ? { order_number: childOrder, parent_group_no: parentGroup } : undefined,
verification: { status: verified ? 'child_order_found_on_parent' : 'child_order_not_found_on_parent', row_text_length: refreshedParentRow ? textOf(refreshedParentRow).length : 0 },
blockers: verified && capture.success ? [] : ['split child save was not confirmed by success response and parent requery']
};
}
function normalizeExportType(value) {
const key = String(value || '').trim().toLowerCase();
return ({
confirmation: 'xingyou-confirm',
confirm: 'xingyou-confirm',
xingyou: 'xingyou-confirm',
'xingyou-confirm': 'xingyou-confirm',
liantai: 'liantai-confirm',
'liantai-confirm': 'liantai-confirm',
job: 'job-order',
beian: 'job-order',
'job-order': 'job-order'
})[key] || key;
}
async function exportConfirmationSources(operation = {}) {
const data = operation.data || {};
const identifier = operationIdentifier(operation);
const dates = Array.isArray(data.departure_dates) ? data.departure_dates : [];
const listDocument = await ensureMainPage('/System/Business/orders.asp', /\/system\/business\/orders\.asp$/i, {
S_chufariqi: dateYyyyMD(dates[0] || ''),
S_chufarizhi: dateYyyyMD(dates[0] || ''),
S_tuanxuhao: identifier
});
await searchMainPage(listDocument, {
S_chufariqi: dateYyyyMD(dates[0] || ''),
S_chufarizhi: dateYyyyMD(dates[0] || ''),
S_tuanxuhao: identifier
});
const row = rowForIdentifier(listDocument, identifier);
if (!row) return { status: 'export_source_blocked', blockers: ['order row not found for export'], no_erp_write: true };
const links = Array.from(row.querySelectorAll('a')).map((link) => ({ text: textOf(link), href: link.getAttribute('href') || '', onclick: link.getAttribute('onclick') || '' }));
const ddid = extractDdid(links) || data.existing_refs?.ddid || data.existing_refs?.ltjt_ddid || '';
const tid = extractTid(textOf(row), links) || data.existing_refs?.tid || data.existing_refs?.ltjt_tdid || '';
if (!ddid) return { status: 'export_source_blocked', blockers: ['order row did not expose ddid'], no_erp_write: true };
const requested = (Array.isArray(operation.exportTypes) && operation.exportTypes.length
? operation.exportTypes
: (Array.isArray(data.exportTypes) && data.exportTypes.length ? data.exportTypes : ['xingyou-confirm']))
.map(normalizeExportType);
const specs = requested.map((type) => {
if (type === 'xingyou-confirm') return { type, path: `/System/Business/orders_confirm_news.asp?did=${encodeURIComponent(ddid)}&tid=${encodeURIComponent(tid)}` };
if (type === 'liantai-confirm') return { type, path: `/System/Business/orders_confirm_new.asp?did=${encodeURIComponent(ddid)}&tid=${encodeURIComponent(tid)}` };
if (type === 'job-order') return { type, path: `/System/Business/teams_beian1.asp?did=${encodeURIComponent(ddid)}` };
return { type, path: '' };
});
const artifacts = [];
for (const spec of specs) {
if (!spec.path) {
artifacts.push({ type: spec.type, status: 'unsupported_export_type' });
continue;
}
const response = await fetch(new URL(spec.path, location.origin).href, { credentials: 'same-origin' });
const buffer = await response.arrayBuffer();
const preview = new TextDecoder('utf-8', { fatal: false }).decode(buffer.slice(0, 1000));
artifacts.push({
type: spec.type,
path: spec.path,
http_status: response.status,
ok: response.ok,
content_type: response.headers.get('content-type') || '',
content_disposition: response.headers.get('content-disposition') || '',
bytes: buffer.byteLength,
login_timeout: isLoginText(preview),
permission_error: /权限|permission/i.test(preview),
word_like: /application\/vnd\.ms-word|Word\.Document|schemas-microsoft-com/i.test(`${response.headers.get('content-type') || ''} ${preview}`)
});
}
const failed = artifacts.filter((artifact) => !artifact.ok || artifact.login_timeout || artifact.permission_error || artifact.status === 'unsupported_export_type');
return {
status: failed.length ? 'export_source_failed' : 'export_source_completed',
no_erp_write: true,
write_attempted: false,
identifier,
source_record: { ddid, tid, group_no: extractGroupNumber(textOf(row)) },
artifacts,
export_only: true,
never_resave: true,
blockers: failed.map((artifact) => `${artifact.type}: export source response was not valid`)
};
}
function getOrderForm() {
const form = document.querySelector('#ListForm');
if (!form || !String(location.href).includes('/orders_add.asp')) return null;
return form;
}
function getFrameVisibility() {
try {
if (!window.frameElement) return true;
return Boolean(window.frameElement.offsetParent || window.frameElement.getClientRects().length);
} catch (error) {
return false;
}
}
function getValue(form, name) {
const el = form.elements[name];
if (!el) return '';
if (el.length && !el.tagName) return Array.from(el).find((item) => item.checked)?.value || el[0]?.value || '';
return el.value ?? '';
}
function setValue(form, blockers, setResults, name, value, source) {
const elements = Array.from(form.elements).filter((el) => el.name === name);
if (!elements.length) {
blockers.push(`Cannot set missing form field ${name} from ${source}`);
return;
}
const textValue = String(value ?? '');
const type = String(elements[0].type || '').toLowerCase();
if (type === 'radio') {
const matched = elements.find((el) => String(el.value) === textValue) || elements[0];
elements.forEach((el) => { el.checked = el === matched; });
} else if (type === 'checkbox') {
elements[0].checked = Boolean(value);
if (textValue) elements[0].value = textValue;
} else {
elements[0].value = textValue;
}
setResults.push({ name, source, type, value_length: textValue.length, value_redacted: true });
}
function summarizeValues(values) {
const normalized = values.map((value) => String(value ?? ''));
return {
value_count: normalized.length,
nonempty_count: normalized.filter((value) => value !== '').length,
total_value_length: normalized.reduce((sum, value) => sum + value.length, 0),
value_redacted: true
};
}
function snapshotForm(form) {
const snapshot = {};
Array.from(form.elements).forEach((el, index) => {
const name = el.name || el.id || `__element_${index}`;
const type = String(el.type || '').toLowerCase();
const value = (type === 'checkbox' || type === 'radio') ? (el.checked ? el.value : '') : (el.value ?? '');
if (!snapshot[name]) snapshot[name] = [];
snapshot[name].push(String(value));
});
return snapshot;
}
function compareSnapshots(before, after) {
const names = Array.from(new Set([...Object.keys(before), ...Object.keys(after)])).sort();
return names
.filter((name) => JSON.stringify(before[name] || []) !== JSON.stringify(after[name] || []))
.map((name) => ({ name, before: summarizeValues(before[name] || []), after: summarizeValues(after[name] || []) }));
}
function groupChangedFields(changes) {
const groups = {};
for (const change of changes) {
let group = 'other';
if (/^(Text|shuoming|oldxingcheng|xingcheng)/.test(change.name)) group = 'itinerary_text';
else if (/^(zao|zhong|wan|zhusu|jingdian)/.test(change.name)) group = 'resources';
else if (/^(ys_|youyingshou|baojia|yingfu)/.test(change.name)) group = 'pricing_receivable';
else if (/^(TianShu|chanpinming|tuanxuhao|zhuanxianming|zutuanshe|bizhong|lianxiren)/.test(change.name)) group = 'core';
groups[group] = (groups[group] || 0) + 1;
}
return groups;
}
function serializeForm(form) {
if (window.jQuery) return window.jQuery(form).serialize();
return Array.from(new FormData(form).entries())
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
async function fetchText(path) {
const url = new URL(path, location.href).href;
const res = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
});
const text = await res.text();
return { ok: res.ok, status: res.status, text };
}
function localValidate(operation) {
const blockers = [];
const data = operation?.data || {};
const counts = data.passenger_counts || {};
if (operation?.action !== 'team_order_create') blockers.push('Unsupported action; expected team_order_create.');
if (operation?.submit_mode !== 'dry_run') blockers.push('submit_mode must be dry_run before approved live submit.');
if (operation?.order_nature !== 'test') blockers.push('Extension v0.1 only allows test orders for live submit.');
if (!data.test_marker) blockers.push('data.test_marker is required.');
if (!data.product?.name) blockers.push('data.product.name is required.');
if (!Array.isArray(data.departure_dates) || data.departure_dates.length !== 1 || !data.departure_dates[0]) blockers.push('Exactly one departure date is required.');
const total = passengerTotal(counts);
if (total <= 0) blockers.push('Passenger total must be greater than zero.');
if (counts.expected_total != null && Math.trunc(toNumber(counts.expected_total)) !== total) blockers.push('expected_total does not equal computed passenger total.');
if (!data.op_user?.name) blockers.push('data.op_user.name is required.');
if (!data.sales_user?.name) blockers.push('data.sales_user.name is required.');
return blockers;
}
async function openOrderForm({ fabudanwei = '老挝联泰' } = {}) {
if (/login/i.test(location.href) || /云智办公i-5.1/.test(document.title || '')) {
return { status: 'login_required', blockers: ['login_required'], top: { url: location.href, title: document.title } };
}
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
if (!frame) return { status: 'main_iframe_not_found', blockers: ['main_iframe_not_found'] };
const entryUrl = `https://ltjt.yunzhi.run/System/Business/orders_add.asp?fabudanwei=${encodeURIComponent(fabudanwei)}&copy=0&ddid=0&_=${Date.now()}`;
const entryStamp = new URL(entryUrl).searchParams.get('_');
frame.src = entryUrl;
return {
status: 'order_form_open_requested',
entry_stamp: entryStamp,
frame_id_hint: frame.id || frame.name || '',
url_redacted: true
};
}
async function pingOrderFrame() {
const form = getOrderForm();
if (!form) return { status: 'not_order_frame', url: location.href, title: document.title };
return {
status: form.elements.length >= 800 ? 'order_frame_ready' : 'order_frame_loading',
url: location.href,
title: document.title,
entry_stamp: new URL(location.href).searchParams.get('_') || '',
form_element_count: form.elements.length,
visible: getFrameVisibility()
};
}
function readOrderEditSnapshot() {
const form = getOrderForm();
if (!form) return { status: 'not_order_frame', no_erp_write: true };
const readNumber = (names) => {
for (const name of names) {
const element = form.elements[name];
const value = element?.value;
if (value !== undefined && value !== '') return toNumber(value);
}
return 0;
};
const readText = (names) => {
for (const name of names) {
const element = form.elements[name];
const value = element?.value;
if (value !== undefined && value !== '') return String(value);
}
return '';
};
const params = new URL(location.href).searchParams;
const remark = readText(['dingdanbeizhu', 'xiadanbeizhu', 'ddbeizhu', 'beizhu', 'beizhu1', 'remark']);
return {
status: 'order_edit_snapshot_ready',
mode: params.get('ddid') && params.get('ddid') !== '0' ? 'edit' : 'create',
identifier: params.get('ddid') || '',
fields: {
pax: {
adult: readNumber(['darenshu']),
child_bed: readNumber(['xiaorenshu']),
child_no_bed: readNumber(['ertrenshu']),
infant: readNumber(['yingrenshu']),
leader: readNumber(['quanrenshu']),
},
rooms: {
SGL: readNumber(['frenshu0']),
TWN: readNumber(['frenshu1']),
TRP: readNumber(['frenshu2']),
DBL: readNumber(['frenshu3']),
HNM: readNumber(['frenshu4']),
TL: readNumber(['frenshu5']),
},
prices: {
adult: readNumber(['ys_danjia0']),
child_bed: readNumber(['ys_danjia1']),
child_no_bed: readNumber(['ys_danjia2']),
infant: readNumber(['ys_danjia3']),
leader: readNumber(['ys_danjia4']),
},
remark: {
present: Boolean(remark),
length: remark.length,
},
},
form_element_count: form.elements.length,
no_erp_write: true,
write_attempted: false,
};
}
async function preflightRawInstruction(operation, options = {}) {
const form = getOrderForm();
if (!form) return { status: 'not_order_frame' };
if (form.elements.length < 800) {
return {
status: 'raw_instruction_test_blocked',
blockers: ['orders_add_form_not_ready'],
page: { url: location.href, title: document.title, form_element_count: form.elements.length }
};
}
const blockers = localValidate(operation);
const warnings = [];
const setResults = [];
const ajaxRecords = [];
const data = operation.data || {};
const marker = String(data.test_marker || '').trim();
const markerSuffix = `${marker}-${Date.now()}`;
const fabudanwei = form.elements.fabudanwei?.value || data.system_defaults?.fabudanwei || '老挝联泰';
const endpoints = {
product: `../dat/AjaxPublicFun.asp?Act=ProGetProductname&fls=1&fabudanwei=${encodeURIComponent(fabudanwei)}`,
route: `../dat/AjaxPublicFun.asp?Act=GetInformation&fl=3&fls=1&fabudanwei=${encodeURIComponent(fabudanwei)}`,
customer: `../dat/AjaxPublicFun.asp?Act=ProTravel&fabudanwei=${encodeURIComponent(fabudanwei)}`,
staff: `../dat/AjaxPublicFun.asp?Act=ProDanwei_Yuangong&fabudanwei=${encodeURIComponent(fabudanwei)}`
};
const fetched = {};
for (const [key, path] of Object.entries(endpoints)) {
fetched[key] = await fetchText(path);
if (!fetched[key].ok) blockers.push(`${key} lookup endpoint returned HTTP ${fetched[key].status}`);
if (isLoginText(fetched[key].text)) blockers.push(`${key} lookup response looked like login timeout`);
}
const productSelection = selectUniqueExistingOption(fetched.product?.text || '', data.product?.name, 1, 'product');
if (!productSelection.match) blockers.push(productSelection.blocker);
const productInitialCheck = {
name: 'product_initial_selection',
endpoint_key: 'product',
match_rule: productSelection.rule,
exact_match_count: productSelection.exact_count,
contains_match_count: productSelection.contains_count,
match_shape: productSelection.match ? { row_index: productSelection.match.rowIndex, column_count: productSelection.match.columns.length, set_value_targets: ['chanpinming'] } : null,
summary: summarizeText(fetched.product?.text || '')
};
const jq = window.jQuery || window.$;
const originalAjax = jq?.ajax;
if (!jq || typeof originalAjax !== 'function') blockers.push('jQuery.ajax is not available in order form');
if (typeof window.Find_product !== 'function') blockers.push('Find_product() is not available');
if (!blockers.length) {
jq.ajax = function ajaxWrapper(optionsArg, ...rest) {
const config = typeof optionsArg === 'string' ? { url: optionsArg } : { ...(optionsArg || {}) };
const isProductEffect = String(config.url || '').includes('Act=GetProduct');
if (!isProductEffect) return originalAjax.call(this, optionsArg, ...rest);
const record = {
url_path_redacted: '../dat/AjaxPublicFun.asp?Act=GetProduct&cpm=[redacted]',
method: config.type || config.method || 'GET',
dataType: config.dataType || '',
completed: false,
ok: false,
text_status: '',
response_byte_length: 0,
contains_login_timeout_text: false
};
ajaxRecords.push(record);
const originalSuccess = config.success;
const originalError = config.error;
const originalComplete = config.complete;
config.success = function successWrapper(responseData, textStatus) {
const text = String(responseData || '');
record.ok = true;
record.text_status = textStatus || '';
record.response_byte_length = new Blob([text]).size;
record.contains_login_timeout_text = isLoginText(text);
return originalSuccess ? originalSuccess.apply(this, arguments) : undefined;
};
config.error = function errorWrapper(jqXHR, textStatus, errorThrown) {
record.ok = false;
record.text_status = textStatus || '';
record.error_text_redacted = errorThrown ? String(errorThrown).slice(0, 80) : '';
return originalError ? originalError.apply(this, arguments) : undefined;
};
config.complete = function completeWrapper() {
record.completed = true;
return originalComplete ? originalComplete.apply(this, arguments) : undefined;
};
return originalAjax.call(this, config, ...rest);
};
}
const beforeProduct = snapshotForm(form);
if (!blockers.length) {
setValue(form, blockers, setResults, 'chanpinming', productSelection.match.columns[1], 'product_lookup_selection');
try {
window.Find_product();
} catch (error) {
blockers.push(`Find_product() threw: ${String(error.message || error).slice(0, 160)}`);
}
}
const effectStarted = Date.now();
while (Date.now() - effectStarted < 15000) {
if (ajaxRecords.some((record) => record.completed)) break;
await sleep(250);
}
await sleep(800);
if (jq && originalAjax) jq.ajax = originalAjax;
const afterProduct = snapshotForm(form);
const productChanges = compareSnapshots(beforeProduct, afterProduct);
const productAjax = ajaxRecords.find((record) => record.url_path_redacted.includes('GetProduct')) || null;
if (!productAjax) blockers.push('GetProduct ajax request was not observed');
else if (!productAjax.ok) blockers.push('GetProduct ajax request did not complete successfully');
else if (productAjax.contains_login_timeout_text) blockers.push('GetProduct response looked like login-timeout script');
if (!productChanges.length) blockers.push('Find_product/GetProduct completed without changing serialized form fields');
const core = {
product: getValue(form, 'chanpinming'),
trip: getValue(form, 'TianShu'),
route: getValue(form, 'zhuanxianming'),
routePrefix: getValue(form, 'tuanxuhao1'),
customer: getValue(form, 'zutuanshe'),
customerId: getValue(form, 'zutuansheid'),
currency: getValue(form, 'bizhong')
};
const staticTrip = '1D2D1N3D2N4D3N5D4N6D5N7D6N8D7N9D8N10D9N';
const productAfterMatches = exactMatches(fetched.product?.text || '', core.product, 1);
const tripMatches = exactMatches(staticTrip, core.trip, 0);
const routeMatches = exactMatches(fetched.route?.text || '', core.route, 1);
const customerMatches = exactMatches(fetched.customer?.text || '', core.customer, 2);
const opSelection = selectUniqueExistingOption(fetched.staff?.text || '', data.op_user?.name, 0, 'op_user');
const salesSelection = selectUniqueExistingOption(fetched.staff?.text || '', data.sales_user?.name, 0, 'sales_user');
if (!opSelection.match) blockers.push(opSelection.blocker);
if (!salesSelection.match) blockers.push(salesSelection.blocker);
const lookupChecks = [
productInitialCheck,
{ name: 'product_after_effect', endpoint_key: 'product', exact_match_count: productAfterMatches.length, match_shape: productAfterMatches.length === 1 ? { row_index: productAfterMatches[0].rowIndex, column_count: productAfterMatches[0].columns.length } : null, summary: summarizeText(fetched.product?.text || '') },
{ name: 'trip_after_effect', endpoint_key: '', exact_match_count: tripMatches.length, match_shape: tripMatches.length === 1 ? { row_index: tripMatches[0].rowIndex, column_count: tripMatches[0].columns.length } : null, summary: summarizeText(staticTrip) },
{ name: 'route_after_effect', endpoint_key: 'route', exact_match_count: routeMatches.length, match_shape: routeMatches.length === 1 ? { row_index: routeMatches[0].rowIndex, column_count: routeMatches[0].columns.length } : null, summary: summarizeText(fetched.route?.text || '') },
{ name: 'customer_after_effect', endpoint_key: 'customer', exact_match_count: customerMatches.length, match_shape: customerMatches.length === 1 ? { row_index: customerMatches[0].rowIndex, column_count: customerMatches[0].columns.length } : null, summary: summarizeText(fetched.customer?.text || '') },
{ name: 'op_user', endpoint_key: 'staff', exact_match_count: opSelection.exact_count, contains_match_count: opSelection.contains_count, match_rule: opSelection.rule, match_shape: opSelection.match ? { row_index: opSelection.match.rowIndex, column_count: opSelection.match.columns.length, set_value_targets: ['gendanren'] } : null, summary: summarizeText(fetched.staff?.text || '') },
{ name: 'sales_user', endpoint_key: 'staff', exact_match_count: salesSelection.exact_count, contains_match_count: salesSelection.contains_count, match_rule: salesSelection.rule, match_shape: salesSelection.match ? { row_index: salesSelection.match.rowIndex, column_count: salesSelection.match.columns.length, set_value_targets: ['xiaoshouren'] } : null, summary: summarizeText(fetched.staff?.text || '') }
];
for (const check of lookupChecks.slice(1, 5)) {
if (check.exact_match_count !== 1) blockers.push(`${check.name}: expected exactly one existing LTJT option, found ${check.exact_match_count}`);
}
const consistencyChecks = [
['product_template_product_nonempty', 'chanpinming', core.product],
['product_template_trip_nonempty', 'TianShu', core.trip],
['product_template_route_nonempty', 'zhuanxianming', core.route],
['product_template_route_prefix_nonempty', 'tuanxuhao1', core.routePrefix],
['product_template_customer_nonempty', 'zutuanshe', core.customer],
['product_template_customer_id_nonempty', 'zutuansheid', core.customerId]
].map(([name, field, value]) => {
const passed = Boolean(String(value || '').trim());
if (!passed) blockers.push(`${name}: product template did not populate ${field}`);
return { name, field, passed, required: true, actual_redacted: true, actual_length: String(value || '').length };
});
if (customerMatches.length === 1 && String(customerMatches[0].columns[1] || '').trim() !== String(core.customerId || '').trim()) {
blockers.push('customer_after_effect: product template customer id does not match existing customer option id');
}
if (routeMatches.length === 1 && String(routeMatches[0].columns[3] || '').trim() !== String(core.routePrefix || '').trim()) {
blockers.push('route_after_effect: product template route prefix does not match existing route option prefix');
}
const counts = data.passenger_counts || {};
const rooms = data.room_counts || {};
const prices = data.prices || {};
const passengerSum = passengerTotal(counts);
const expectedPassengerTotal = counts.expected_total == null ? passengerSum : Math.trunc(toNumber(counts.expected_total));
const computedRoomTotal = roomTotal(rooms);
const logicalChecks = [
{ name: 'passenger_total', passed: passengerSum > 0 && passengerSum === expectedPassengerTotal, expected: expectedPassengerTotal, actual: passengerSum },
{ name: 'room_total', passed: computedRoomTotal >= 0, actual: computedRoomTotal },
{ name: 'departure_date_count', passed: Array.isArray(data.departure_dates) && data.departure_dates.length === 1, actual: Array.isArray(data.departure_dates) ? data.departure_dates.length : 0 }
];
logicalChecks.forEach((check) => {
if (!check.passed) blockers.push(`${check.name}: raw instruction logical validation failed`);
});
const receivableRows = [];
[
['adult', '成人团费'],
['child_bed', '小童占床'],
['child_no_bed', '小童不占床'],
['infant', '婴儿'],
['leader', '领队']
].forEach(([key, label]) => {
const quantity = toNumber(counts[key]);
const unitPrice = toNumber(prices[key]);
if (quantity > 0 && unitPrice > 0) receivableRows.push({ key, label, quantity, unit_price: unitPrice, amount: quantity * unitPrice });
else if (quantity > 0 && unitPrice <= 0) blockers.push(`price for ${key} must be greater than zero when quantity is greater than zero`);
});
if (!receivableRows.length) blockers.push('No receivable rows were generated from passenger counts and prices');
if (!blockers.length) {
const note = `${marker}|测试订单|${String(data.special_requests || '')}`;
const currency = core.currency || prices.currency || data.system_defaults?.currency || 'USD';
setValue(form, blockers, setResults, 'chufa_ri', dateYyyyMD(data.departure_dates[0]), 'raw_instruction_departure_date');
setValue(form, blockers, setResults, 'tuanxuhao2', markerSuffix, 'raw_instruction_obvious_test_suffix');
setValue(form, blockers, setResults, 'gendanren', opSelection.match.columns[0], 'raw_instruction_existing_op_user');
setValue(form, blockers, setResults, 'xiaoshouren', salesSelection.match.columns[0], 'raw_instruction_existing_sales_user');
setValue(form, blockers, setResults, 'darenshu', integerString(counts.adult), 'raw_instruction_passenger_counts');
setValue(form, blockers, setResults, 'xiaorenshu', integerString(counts.child_bed), 'raw_instruction_passenger_counts');
setValue(form, blockers, setResults, 'ertrenshu', integerString(counts.child_no_bed), 'raw_instruction_passenger_counts');
setValue(form, blockers, setResults, 'yingrenshu', integerString(counts.infant), 'raw_instruction_passenger_counts');
setValue(form, blockers, setResults, 'quanrenshu', integerString(counts.leader), 'raw_instruction_passenger_counts');
setValue(form, blockers, setResults, 'frenshu0', integerString(rooms.SGL), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'frenshu1', integerString(rooms.TWN), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'frenshu2', integerString(rooms.TRP), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'frenshu3', integerString(rooms.DBL), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'frenshu4', integerString(rooms.HNM), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'frenshu5', integerString(rooms.TL), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'frenshu6', integerString(computedRoomTotal), 'raw_instruction_room_counts');
setValue(form, blockers, setResults, 'danzhuangtai', data.system_defaults?.business_status || '预订', 'raw_instruction_status');
setValue(form, blockers, setResults, 'yaobeian', data.system_defaults?.filing_required === false ? '' : '要备案', 'raw_instruction_checkbox');
setValue(form, blockers, setResults, 'xiadanbeizhu', note, 'raw_instruction_obvious_test_note');
const receivableTargets = ['ys_danwei','ys_danweiid','ys_shuoming','ys_zhanwei','ys_xiangmu','ys_fangshi','ys_bizhong','ys_shuliang','ys_danjia','ys_jine','ys_yishoufu','ys_beizhu','ys_id','ys_shoufulei','ys_caozuoren','ys_shenheren'];
for (let index = 0; index < 10; index += 1) {
receivableTargets.forEach((prefix) => setValue(form, blockers, setResults, `${prefix}${index}`, '', 'raw_instruction_clear_receivables'));
}
receivableRows.forEach((row, index) => {
setValue(form, blockers, setResults, `ys_danwei${index}`, core.customer, 'raw_instruction_receivable_customer');
setValue(form, blockers, setResults, `ys_danweiid${index}`, core.customerId, 'raw_instruction_receivable_customer_id');
setValue(form, blockers, setResults, `ys_xiangmu${index}`, `${marker} ${row.label}`, 'raw_instruction_receivable_item');
setValue(form, blockers, setResults, `ys_shuoming${index}`, '', 'raw_instruction_receivable_unit');
setValue(form, blockers, setResults, `ys_bizhong${index}`, currency, 'raw_instruction_receivable_currency');
setValue(form, blockers, setResults, `ys_shuliang${index}`, compactNumber(row.quantity), 'raw_instruction_receivable_quantity');
setValue(form, blockers, setResults, `ys_danjia${index}`, compactNumber(row.unit_price), 'raw_instruction_receivable_unit_price');
setValue(form, blockers, setResults, `ys_jine${index}`, compactNumber(row.amount), 'raw_instruction_receivable_amount');
setValue(form, blockers, setResults, `ys_yishoufu${index}`, '0', 'raw_instruction_receivable_paid');
setValue(form, blockers, setResults, `ys_beizhu${index}`, note, 'raw_instruction_receivable_remark');
setValue(form, blockers, setResults, `ys_shoufulei${index}`, '0', 'raw_instruction_receivable_payment_type');
setValue(form, blockers, setResults, `ys_caozuoren${index}`, opSelection.match.columns[0], 'raw_instruction_receivable_operator');
});
}
const requiredTargets = ['chufa_ri','zhuanxianming','TianShu','zutuanshe','chanpinming','gendanren','tuanxuhao1','tuanxuhao2','darenshu','xiaorenshu','ertrenshu','yingrenshu','quanrenshu','xiaoshouren'];
const requiredMissing = requiredTargets.filter((name) => String(getValue(form, name) || '').trim() === '');
if (requiredMissing.length) blockers.push(`Required fields still blank: ${requiredMissing.join(', ')}`);
const serializedForm = serializeForm(form);
const serializedParams = new URLSearchParams(serializedForm);
let submitIntercept = null;
if (options.interceptSubmit && !blockers.length) {
submitIntercept = await interceptSubmit(form, jq);
if (submitIntercept.status !== 'submit_intercept_captured') blockers.push('submit intercept did not capture exactly one clean DoInfoJH branch');
}
const receivableAmountTotal = receivableRows.reduce((sum, row) => sum + row.amount, 0);
const output = {
ok: true,
status: blockers.length ? 'raw_instruction_test_blocked' : 'raw_instruction_test_passed',
page: { url: location.href, title: document.title, form_element_count: form.elements.length },
parsed_instruction: {
action: operation.action,
order_nature: operation.order_nature,
order_mode: data.order_mode || '',
departure_date: dateYyyyMD(data.departure_dates?.[0] || ''),
passenger_total: passengerSum,
room_total: computedRoomTotal,
receivable_row_count: receivableRows.length,
receivable_amount_total: receivableAmountTotal,
values_are_test_only: true
},
lookup_checks: lookupChecks,
product_side_effect: {
ajax_records: ajaxRecords,
changed_field_count: productChanges.length,
changed_field_groups: groupChangedFields(productChanges),
changed_field_names: productChanges.map((change) => change.name)
},
consistency_checks: consistencyChecks,
core_after_product: Object.fromEntries(Object.entries(core).map(([key, value]) => [key, { nonempty: Boolean(String(value || '').trim()), value_length: String(value || '').length, value_redacted: true }])),
logical_checks: logicalChecks,
test_markers: {
suffix_marker: marker,
note_marker: marker,
full_suffix_marker: markerSuffix,
values_are_test_only: true
},
set_summary: {
set_field_count: setResults.length,
set_field_names: Array.from(new Set(setResults.map((item) => item.name))).sort(),
set_results: setResults
},
final_form: {
serialized_field_count: Array.from(serializedParams.keys()).length,
serialized_form_sha256: await sha256(`Act=DoInfoJH&${serializedForm}`),
serialized_form_redacted: true,
required_missing: requiredMissing
},
submit_intercept: submitIntercept,
blockers,
warnings,
submit_safety: {
live_submit_attempted: false,
DoInfoJH_network_prevented: Boolean(submitIntercept?.DoInfoJH_network_prevented),
note: 'Chrome extension preflight intercepted DoInfoJH before network.'
}
};
if (submitIntercept?.intercepted_submits?.[0]?.payload_sha256) {
output.final_form.submit_payload_sha256 = submitIntercept.intercepted_submits[0].payload_sha256;
output.final_form.submit_function_mutates_payload = output.final_form.submit_payload_sha256 !== output.final_form.serialized_form_sha256;
}
return output;
}
async function interceptSubmit(form, jq) {
const originalAjaxForSubmit = jq.ajax;
const originalAlert = window.alert;
const originalDialogAlert = jq.dialog?.alert;
const intercepted = [];
const alerts = [];
window.alert = function interceptedAlert(message) {
alerts.push({ type: 'alert', message_length: String(message || '').length, message_redacted: true });
return undefined;
};
if (jq.dialog && typeof jq.dialog.alert === 'function') {
jq.dialog.alert = function interceptedDialogAlert(message) {
alerts.push({ type: 'dialog.alert', message_length: String(message || '').length, message_redacted: true });
return undefined;
};
}
jq.ajax = function ajaxIntercept(optionsArg, ...rest) {
const config = typeof optionsArg === 'string' ? { url: optionsArg } : { ...(optionsArg || {}) };
const url = String(config.url || '');
const requestData = String(config.data || '');
const isSubmit = /\/System\/DAT\/orders\.asp/i.test(url) && requestData.startsWith('Act=DoInfoJH&');
if (!isSubmit) return originalAjaxForSubmit.call(this, optionsArg, ...rest);
const params = new URLSearchParams(requestData.slice('Act=DoInfoJH&'.length));
intercepted.push({
url_redacted: '/System/DAT/orders.asp',
method: config.type || config.method || 'GET',
dataType: config.dataType || '',
prevented_from_network: true,
payload: requestData,
payload_summary: {
starts_with_DoInfoJH: true,
byte_length: new Blob([requestData]).size,
field_count: Array.from(params.keys()).length,
field_names: Array.from(new Set(Array.from(params.keys()))).sort(),
value_redacted: true
}
});
return { readyState: 4, status: 0, statusText: 'intercepted_by_ltjt_order_assistant', abort() {} };
};
let thrown = '';
try {
window.SubmitInfoForm();
} catch (error) {
thrown = error.message || String(error);
}
await sleep(1000);
jq.ajax = originalAjaxForSubmit;
window.alert = originalAlert;
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
const loadingCleanup = closeLingeringLoading(jq);
for (const item of intercepted) {
item.payload_sha256 = await sha256(item.payload || '');
item.payload_redacted = true;
delete item.payload;
}
return {
status: intercepted.length === 1 && !alerts.length && !thrown ? 'submit_intercept_captured' : 'submit_intercept_blocked',
intercepted_submit_count: intercepted.length,
intercepted_submits: intercepted,
alerts,
ui_cleanup: {
loading_cleanup: loadingCleanup
},
thrown_redacted: Boolean(thrown),
thrown_length: String(thrown || '').length,
DoInfoJH_network_prevented: true
};
}
async function liveSubmitApproved({ expectedPayloadSha256, expectedFieldCount } = {}) {
const form = getOrderForm();
if (!form) return { status: 'not_order_frame' };
if (form.elements.length < 800) {
return {
status: 'live_submit_blocked',
blockers: ['orders_add_form_not_ready'],
form_element_count: form.elements.length,
submit_safety: { live_submit_attempted: false }
};
}
const jq = window.jQuery || window.$;
if (!jq || typeof jq.ajax !== 'function') return { status: 'live_submit_blocked', blockers: ['jquery_ajax_not_found'] };
if (typeof window.SubmitInfoForm !== 'function') return { status: 'live_submit_blocked', blockers: ['SubmitInfoForm_not_found'] };
if (!expectedPayloadSha256) return { status: 'live_submit_blocked', blockers: ['expectedPayloadSha256_missing'] };
const serializedForm = serializeForm(form);
const requestBody = `Act=DoInfoJH&${serializedForm}`;
const currentHash = await sha256(requestBody);
const params = new URLSearchParams(serializedForm);
const blockers = [];
if (expectedFieldCount && Array.from(params.keys()).length !== expectedFieldCount) blockers.push('current browser field count does not match approved report field count');
if (blockers.length) {
return {
status: 'live_submit_blocked',
current_payload: { payload_sha256: currentHash, field_count: Array.from(params.keys()).length, value_redacted: true },
blockers,
submit_safety: { live_submit_attempted: false }
};
}
const originalAjax = jq.ajax;
const ajaxRecords = [];
const alerts = [];
const deferredDialogCallbacks = [];
const originalAlert = window.alert;
const originalDialogAlert = jq.dialog?.alert;
const originalMessagerAlert = jq.messager?.alert;
function parseErpSubmitReceipt(message) {
const raw = typeof message === 'object' && message
? String(message.content || message.message || message.msg || message.text || '')
: String(message || '');
const text = raw
.replace(/\r/g, '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr)>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.trim();
if (!text) return null;
const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
const valueAfterLabel = (label) => {
const pattern = new RegExp(`${label}\\s*[:]\\s*([^\\n]+)`, 'i');
const line = lines.find((item) => pattern.test(item));
const source = line || text;
const match = source.match(pattern);
const value = match ? match[1].trim() : '';
return label === '团号' ? value.replace(/[\s。;,].*$/, '').trim() : value;
};
const groupNumber = valueAfterLabel('团号');
const product = valueAfterLabel('产品');
const passengerText = valueAfterLabel('人数');
const success = /下单成功|成功|success|ສໍາເລັດ/i.test(text);
if (!success && !groupNumber && !product && !passengerText) return null;
return {
status: success ? 'submit_success_receipt' : 'submit_receipt',
success,
title: lines[0] || '',
group_number: groupNumber,
product,
passenger_text: passengerText,
line_count: lines.length,
received_at: new Date().toISOString()
};
}
function alertRecord(type, message) {
const text = typeof message === 'object' && message
? String(message.content || message.message || message.msg || message.text || '')
: String(message || '');
const receipt = parseErpSubmitReceipt(text);
return {
type,
message_length: text.length,
message_redacted: !receipt,
message: receipt ? text.slice(0, 500) : undefined,
erp_receipt: receipt || undefined
};
}
function captureVisibleDialogReceipts() {
const selector = [
'.messager-window',
'.messager-body',
'.window.messager-window',
'.aui_dialog',
'.ui-dialog',
'.dialog',
'[role="dialog"]'
].join(',');
Array.from(document.querySelectorAll(selector)).forEach((node) => {
const text = String(node.textContent || '').trim();
if (!text) return;
const receipt = parseErpSubmitReceipt(text);
if (receipt && !alerts.some((alert) => alert.erp_receipt?.group_number === receipt.group_number)) {
alerts.push({
type: 'visible_dialog',
message_length: text.length,
message_redacted: false,
message: text.slice(0, 500),
erp_receipt: receipt
});
}
});
}
window.alert = function liveSubmitAlert(message) {
alerts.push(alertRecord('alert', message));
return undefined;
};
if (jq.dialog && typeof jq.dialog.alert === 'function') {
jq.dialog.alert = function liveSubmitDialogAlert(message) {
alerts.push(alertRecord('dialog.alert', message));
Array.from(arguments).forEach((arg) => {
if (typeof arg === 'function') deferredDialogCallbacks.push(arg);
});
return undefined;
};
}
if (jq.messager && typeof jq.messager.alert === 'function') {
jq.messager.alert = function liveSubmitMessagerAlert(title, message) {
alerts.push(alertRecord('messager.alert', [title, message].filter(Boolean).join('\n')));
Array.from(arguments).forEach((arg) => {
if (typeof arg === 'function') deferredDialogCallbacks.push(arg);
});
return undefined;
};
}
jq.ajax = function liveSubmitAjaxWrapper(optionsArg, ...rest) {
const config = typeof optionsArg === 'string' ? { url: optionsArg } : { ...(optionsArg || {}) };
const url = String(config.url || '');
const requestData = String(config.data || '');
const isSubmit = /\/System\/DAT\/orders\.asp/i.test(url) && requestData.startsWith('Act=DoInfoJH&');
if (!isSubmit) return originalAjax.call(this, optionsArg, ...rest);
const record = {
url_redacted: '/System/DAT/orders.asp',
method: config.type || config.method || 'GET',
dataType: config.dataType || '',
request_payload_sha256: sha256Sync(requestData),
request_field_count: expectedFieldCount,
live_submit_attempted: true,
prevented_from_network: false,
completed: false,
ok: false,
text_status: '',
response_byte_length: 0,
response_contains_login_timeout: false,
response_contains_permission_text: false,
response_contains_success_hint: false,
response_contains_error_hint: false,
response_redacted: true
};
ajaxRecords.push(record);
if (record.request_payload_sha256 !== expectedPayloadSha256) {
record.prevented_from_network = true;
record.completed = true;
record.ok = false;
record.text_status = 'blocked_hash_mismatch_before_network';
return { readyState: 4, status: 0, statusText: 'blocked_hash_mismatch_before_network', abort() {} };
}
const originalSuccess = config.success;
const originalError = config.error;
const originalComplete = config.complete;
config.success = function successWrapper(responseText, textStatus) {
const text = String(responseText || '');
record.ok = true;
record.text_status = textStatus || '';
record.response_byte_length = new Blob([text]).size;
record.response_contains_login_timeout = isLoginText(text);
record.response_contains_permission_text = /权限|permission/i.test(text);
record.response_contains_success_hint = /成功|保存|添加|完成|ok|success/i.test(text);
record.response_contains_error_hint = /失败|错误|异常|error|alert\(/i.test(text) && !record.response_contains_success_hint;
return originalSuccess ? originalSuccess.apply(this, arguments) : undefined;
};
config.error = function errorWrapper(jqXHR, textStatus, errorThrown) {
record.ok = false;
record.text_status = textStatus || '';
record.error_text_redacted = errorThrown ? String(errorThrown).slice(0, 80) : '';
return originalError ? originalError.apply(this, arguments) : undefined;
};
config.complete = function completeWrapper() {
record.completed = true;
return originalComplete ? originalComplete.apply(this, arguments) : undefined;
};
return originalAjax.call(this, config, ...rest);
};
let thrown = '';
try {
window.SubmitInfoForm();
} catch (error) {
thrown = error.message || String(error);
}
const started = Date.now();
while (Date.now() - started < 30000) {
if (ajaxRecords.some((record) => record.completed)) break;
await sleep(250);
}
jq.ajax = originalAjax;
captureVisibleDialogReceipts();
window.alert = originalAlert;
if (jq.dialog && typeof originalDialogAlert === 'function') jq.dialog.alert = originalDialogAlert;
if (jq.messager && typeof originalMessagerAlert === 'function') jq.messager.alert = originalMessagerAlert;
const immediateLoadingCleanup = closeLingeringLoading(jq);
if (deferredDialogCallbacks.length) {
// Native dialog callbacks may dereference a popup/opener that no longer
// exists after the extension intercepts the dialog. Verification and
// return-to-list are handled explicitly by the executor, so replaying
// those callbacks is both unnecessary and able to disrupt reconciliation.
window.setTimeout(() => {
closeLingeringLoading(window.jQuery || window.$);
}, 100);
} else {
window.setTimeout(() => closeLingeringLoading(window.jQuery || window.$), 800);
}
const submitBlockers = [];
if (thrown) submitBlockers.push('SubmitInfoForm threw before ajax');
if (!ajaxRecords.length) submitBlockers.push('SubmitInfoForm did not attempt DoInfoJH ajax submit');
if (ajaxRecords.length !== 1) submitBlockers.push('Expected exactly one DoInfoJH ajax submit attempt');
if (ajaxRecords[0]?.prevented_from_network) submitBlockers.push('DoInfoJH ajax was blocked before network because payload hash mismatched');
if (ajaxRecords[0] && !ajaxRecords[0].completed) submitBlockers.push('DoInfoJH ajax did not complete before timeout');
if (ajaxRecords[0]?.response_contains_login_timeout) submitBlockers.push('DoInfoJH response looks like login timeout');
if (ajaxRecords[0]?.response_contains_permission_text) submitBlockers.push('DoInfoJH response contains permission text');
const erpReceipt = alerts.map((alert) => alert.erp_receipt).find(Boolean) || null;
return {
status: submitBlockers.length ? 'live_submit_uncertain_or_failed' : 'live_submit_completed',
page: { url: location.href, title: document.title, form_element_count: form.elements.length },
current_payload: { payload_sha256: currentHash, field_count: Array.from(params.keys()).length, value_redacted: true },
ajax_records: ajaxRecords,
alerts,
erp_receipt: erpReceipt,
blockers: submitBlockers,
ui_cleanup: {
immediate_loading_cleanup: immediateLoadingCleanup,
deferred_dialog_callback_count: deferredDialogCallbacks.length,
deferred_dialog_callbacks_suppressed: deferredDialogCallbacks.length > 0,
followup_loading_cleanup_scheduled: true
},
submit_safety: {
live_submit_attempted: ajaxRecords.some((record) => record.live_submit_attempted),
approved_payload_sha256_matched: currentHash === expectedPayloadSha256 || ajaxRecords[0]?.request_payload_sha256 === expectedPayloadSha256
}
};
}
async function verifyOrderMarker({ marker, dateFrom, dateTo } = {}) {
const body = new URLSearchParams({
Act: 'JH_OrderList',
Tpage: '1',
P_Size: '50',
riqi: 'chufari',
S_chufariqi: dateYyyyMD(dateFrom || ''),
S_chufarizhi: dateYyyyMD(dateTo || dateFrom || ''),
S_fabudanwei: '老挝联泰',
S_tuanxuhao: marker || '',
S_kehuming: '',
S_chanpinming: '',
S_gendanren: '',
S_youkexinxi: '',
S_daoyou: '',
S_lingdui: '',
S_querenshu: '',
S_zhuanxianming: '',
S_zhuangtai: ''
});
const res = await fetch('/System/dat/orders.asp', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: body.toString()
});
const text = await res.text();
const markerOccurrences = marker ? Math.max(0, text.split(marker).length - 1) : 0;
return {
status: markerOccurrences ? 'order_marker_found' : 'order_marker_not_found',
request: {
endpoint: '/System/dat/orders.asp',
action: 'JH_OrderList',
date_range: { from: dateYyyyMD(dateFrom || ''), to: dateYyyyMD(dateTo || dateFrom || '') },
marker_redacted: false,
marker
},
response: {
http_status: res.status,
byte_length: new Blob([text]).size,
marker_occurrences: markerOccurrences,
contains_login_timeout_text: isLoginText(text),
contains_permission_text: /权限|permission/i.test(text),
looks_empty_or_no_match: /无数据|没有|暂无|empty/i.test(text) || markerOccurrences === 0,
value_redacted: true
},
submit_safety: {
live_submit_attempted: false,
verification_only: true
}
};
}
window.LTJTOrderAssistant = {
version: '0.2.14',
openOrderForm,
pingOrderFrame,
preflightRawInstruction,
liveSubmitApproved,
verifyOrderMarker,
returnToOrderList,
inspectOperationContext,
readOrderEditSnapshot,
createSplitParentLive,
createSplitChildLive,
exportConfirmationSources
};
})();