(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(//gi, '\n') .replace(/<\/(p|div|li|tr)>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/ /gi, ' ') .replace(/&/gi, '&') .replace(/</gi, '<') .replace(/>/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)}©=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 = '1D◇2D1N◇3D2N◇4D3N◇5D4N◇6D5N◇7D6N◇8D7N◇9D8N◇10D9N'; 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(//gi, '\n') .replace(/<\/(p|div|li|tr)>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/ /gi, ' ') .replace(/&/gi, '&') .replace(/</gi, '<') .replace(/>/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 }; })();