Files
LWLT-AIBOT/tools/run_readonly_reference_probe_via_extension.mjs
2026-08-25 10:33:29 +08:00

260 lines
14 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { readFile } from 'node:fs/promises';
const DEFAULT_CDP = '/Users/inmanx/.agents/skills/chrome-cdp/scripts/cdp.mjs';
const DEFAULT_PLATFORM_TARGET = 'DA600CC0';
const EXTENSION_ID = 'kafggjjlhebccdgkechmbgflkaifkccf';
function parseArgs(argv) {
const args = {
targetsFile: '',
expectedRunId: '',
platformTarget: DEFAULT_PLATFORM_TARGET,
cdp: process.env.CDP_CLI || DEFAULT_CDP,
timeoutMs: 30_000
};
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (value === '--targets-file') args.targetsFile = argv[++index] || '';
else if (value === '--expected-run-id') args.expectedRunId = argv[++index] || '';
else if (value === '--platform-target') args.platformTarget = argv[++index] || '';
else if (value === '--cdp') args.cdp = argv[++index] || '';
else if (value === '--timeout-ms') args.timeoutMs = Number(argv[++index]);
else throw new Error(`unknown argument: ${value}`);
}
if (!args.targetsFile) throw new Error('--targets-file is required');
if (!args.expectedRunId) throw new Error('--expected-run-id is required');
if (!Number.isFinite(args.timeoutMs) || args.timeoutMs < 5_000) throw new Error('--timeout-ms must be at least 5000');
return args;
}
function cdp(args, command, target, ...rest) {
return execFileSync(process.execPath, [args.cdp, command, target, ...rest], {
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024
}).trim();
}
function parseJson(value, label) {
try {
return JSON.parse(String(value || '').trim());
} catch {
throw new Error(`${label} returned non-JSON output: ${String(value || '').slice(0, 500)}`);
}
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function validateProbe(probe, expectedRunId) {
const errors = [];
if (probe?.run_id !== expectedRunId) errors.push('run_id_mismatch');
if (probe?.marker !== 'TEST-202609') errors.push('marker_mismatch');
if (probe?.account !== '测试ai员工账号') errors.push('account_mismatch');
if (probe?.expect_absent !== undefined && probe.expect_absent !== true) errors.push('expect_absent_invalid');
if (!Array.isArray(probe?.targets) || !probe.targets.length || probe.targets.length > 20) errors.push('target_count_invalid');
for (const target of probe?.targets || []) {
if (!['independent_order', 'shared_plan', 'shared_child_order'].includes(target?.kind)) errors.push('target_kind_invalid');
if (target?.kind === 'shared_child_order') {
if (!/^D\d+$/.test(String(target?.identifier || ''))) errors.push('child_identifier_invalid');
if (!String(target?.parent_group_no || '').includes('TEST-202609')) errors.push('target_marker_missing');
if (!/^\d+$/.test(String(target?.tid || '')) || !/^\d+$/.test(String(target?.ddid || ''))) errors.push('child_reference_invalid');
} else if (!String(target?.identifier || '').includes('TEST-202609')) errors.push('target_marker_missing');
if (!/^2026-09-\d{2}$/.test(String(target?.departure_date || ''))) errors.push('target_date_outside_2026_09');
}
return [...new Set(errors)];
}
function lookupExpression(probeKey, targets, expectAbsent = false) {
return `(async()=>{
const tabs=await chrome.tabs.query({url:"https://lwlt.hisy.cc/*"});
const tab=tabs.find((candidate)=>/\\/System\\/Mainlt\\.asp/i.test(candidate.url||""))||tabs[0];
if(!tab?.id)throw new Error("erp_tab_missing");
const injected=await chrome.scripting.executeScript({
target:{tabId:tab.id},
world:"MAIN",
func:async(targets,expectAbsent)=>{
const dateYyyyMD=(value)=>String(value||"").split("-").map((part,index)=>index?String(Number(part)):part).join("-");
const sha256=async(value)=>{
const digest=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(value));
return [...new Uint8Array(digest)].map((byte)=>byte.toString(16).padStart(2,"0")).join("");
};
const output=[];
for(const target of targets){
const independent=target.kind==="independent_order";
const endpoint=independent?"/System/DAT/orders.asp":"/System/DAT/plan.asp";
const action=independent?"JH_OrderList":"SP_OrderList";
const searchIdentifier=target.kind==="shared_child_order"?String(target.parent_group_no||""):target.identifier;
const url=new URL(endpoint,location.origin);
url.searchParams.set("Act",action);
const date=dateYyyyMD(target.departure_date);
const params=new URLSearchParams({
Tpage:"1",P_Size:"50",riqi:"chufari",S_chufariqi:date,S_chufarizhi:date,
S_fabudanwei:"老挝联泰",S_tuanxuhao:searchIdentifier,S_zhuangtai:String(target.parent_status||"")
});
const response=await fetch(url.href,{
method:"POST",credentials:"same-origin",
headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},
body:params.toString()
});
const text=await response.text();
const documentFragment=new DOMParser().parseFromString("<table><tbody>"+text+"</tbody></table>","text/html");
const rows=[...documentFragment.querySelectorAll("tr")].filter((row)=>{
const controls=[...row.querySelectorAll("input,a,button")]
.map((element)=>[element.value||"",element.getAttribute("href")||"",element.getAttribute("onclick")||""].join(" ")).join(" ");
const evidence=((row.innerText||row.textContent||"")+" "+controls).replace(/\\s+/g," ");
const native=/^tr[_-]\\d+$/i.test(row.id||"")||Boolean(row.querySelector("input[name=xuanzeid],input#xuanzeid"));
return native&&evidence.includes(target.identifier);
});
const row=rows.length===1?rows[0]:null;
const actions=row?[...row.querySelectorAll("[onclick],a[href]")]
.map((element)=>((element.getAttribute("onclick")||"")+" "+(element.getAttribute("href")||"")).trim()):[];
const joined=actions.join(" ");
const tid=(row?.id||"").match(/^tr[_-](\\d+)$/i)?.[1]
||joined.match(/[?&]tid=(\\d+)/i)?.[1]
||joined.match(/OPEN_finance_list\\(\\s*[^,]+,\\s*[\"']?(\\d+)/i)?.[1]||"";
const ddid=independent
? joined.match(/OPEN_update\\(\\s*[\"']?(\\d+)[\"']?\\s*,\\s*[\"']?0[\"']?\\s*,/i)?.[1]
||joined.match(/[?&]ddid=(\\d+)/i)?.[1]||""
: "";
const childRefs=target.kind==="shared_plan"&&row&&tid
? [...new Map([...row.querySelectorAll("[onclick]")].map((element)=>{
const match=String(element.getAttribute("onclick")||"").match(/OPEN_update\\(\\s*(\\d+)\\s*,\\s*(\\d+)/i);
return match&&match[1]===tid?[match[2],{tid:match[1],ddid:match[2]}]:null;
}).filter(Boolean)).values()]
: null;
const nativeActionSignatures=[...joined.matchAll(/([A-Za-z_]+)\\(([^)]*)\\)/g)].map((match)=>({
name:match[1],
numeric_args:(match[2].match(/\\d+/g)||[])
}));
const evidence=row?((row.innerText||row.textContent||"")+" "+joined).replace(/\\s+/g," "):"";
const markerMatched=evidence.includes("TEST-202609");
const accountMatched=evidence.includes("测试ai员工账号");
const loginTimeout=/登录|登陆|login|验证码/i.test(text);
const permissionError=/权限|permission|无权/i.test(text);
let childDetail=null;
if(target.kind==="shared_child_order"){
const childDdid=String(target.ddid||String(target.identifier||"").replace(/^D/i,""));
const detailUrl=new URL("/System/Business/plan_order.asp",location.origin);
detailUrl.searchParams.set("tdid",String(target.tid||tid));
detailUrl.searchParams.set("ddid",childDdid);
const detailResponse=await fetch(detailUrl.href,{credentials:"same-origin",cache:"no-store"});
const detailText=await detailResponse.text();
const detailDocument=new DOMParser().parseFromString(detailText,"text/html");
const detailForm=detailDocument.querySelector("#ListForm,form[name=ListForm]");
const detailValue=(name)=>String(detailForm?.elements?.namedItem?.(name)?.value||detailDocument.querySelector('[name="'+name+'"]')?.getAttribute("value")||"");
const detailStatus=detailValue("danzhuangtai");
const detailMarkerMatched=detailText.includes("TEST-202609");
const detailAccountMatched=detailText.includes("测试ai员工账号");
const detailTid=detailValue("oldtdid")||detailValue("tdid")||String(target.tid||tid);
const detailDdid=detailValue("ddid");
childDetail={
http_status:detailResponse.status,current_status:detailStatus,
form_present:Boolean(detailForm),marker_matched:detailMarkerMatched,
account_matched:detailAccountMatched,tid_matched:detailTid===String(target.tid||tid),
ddid_matched:detailDdid===childDdid,response_sha256:await sha256(detailText),
expected_present:!expectAbsent,absence_proven:!detailForm&&!detailMarkerMatched,
matched:detailResponse.ok&&(expectAbsent
? !detailForm&&!detailMarkerMatched
: Boolean(detailForm)&&detailMarkerMatched&&detailAccountMatched
&&detailTid===String(target.tid||tid)&&detailDdid===childDdid),
value_redacted:true
};
}
output.push({
kind:target.kind,identifier:target.identifier,departure_date:target.departure_date,
http_status:response.status,byte_length:new Blob([text]).size,response_sha256:await sha256(text),
candidate_count:rows.length,tid,ddid,native_action_signatures:independent&&!ddid?nativeActionSignatures:[],
child_count:Array.isArray(childRefs)?childRefs.length:null,child_refs:childRefs,
marker_matched:markerMatched,account_matched:accountMatched,
login_timeout:loginTimeout,permission_error:permissionError,child_detail:childDetail,
expected_present:!expectAbsent,present:rows.length===1,
matched:expectAbsent
? response.ok&&rows.length===0&&!loginTimeout&&!permissionError&&(!childDetail||childDetail.matched)
: response.ok&&rows.length===1&&Boolean(tid)&&(!independent||Boolean(ddid))
&&markerMatched&&accountMatched&&!loginTimeout&&!permissionError
&&(!childDetail||childDetail.matched)
});
}
return {url:location.href,results:output};
},
args:[${JSON.stringify(targets)},${JSON.stringify(expectAbsent)}]
});
const report=injected[0]?.result||{results:[]};
const saved=await chrome.storage.local.get("businessTaskResults");
const resultMap=saved.businessTaskResults||{};
resultMap[${JSON.stringify(probeKey)}]={
status:"completed",stage:"read_only_reference_lookup",message:"exact native list lookup completed",
write_attempted:false,no_erp_write:true,
running_tasks:typeof runningTasks==="undefined"?[]:[...runningTasks],report
};
await chrome.storage.local.set({businessTaskResults:resultMap});
return true;
})()`;
}
const args = parseArgs(process.argv.slice(2));
const probe = JSON.parse(await readFile(args.targetsFile, 'utf8'));
const safetyErrors = validateProbe(probe, args.expectedRunId);
if (safetyErrors.length) throw new Error(`probe safety check failed: ${safetyErrors.join(',')}`);
parseJson(cdp(args, 'eval', args.platformTarget, `(async()=>JSON.stringify(await sendToExtension('PING',{},3000)))()`), 'extension ping');
const targetsResult = parseJson(cdp(args, 'evalraw', args.platformTarget, 'Target.getTargets', '{}'), 'target list');
const serviceWorker = targetsResult.targetInfos?.find((target) => (
target.type === 'service_worker' && target.url === `chrome-extension://${EXTENSION_ID}/background.js`
));
if (!serviceWorker?.targetId) throw new Error('extension_service_worker_missing');
const attached = parseJson(cdp(args, 'evalraw', args.platformTarget, 'Target.attachToTarget', JSON.stringify({
targetId: serviceWorker.targetId,
flatten: false
})), 'service worker attach');
if (!attached.sessionId) throw new Error('extension_service_worker_attach_failed');
const probeKey = `READONLY-REFS-${args.expectedRunId}-${Date.now()}`;
const expression = lookupExpression(probeKey, probe.targets, probe.expect_absent === true);
const message = JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: { expression, awaitPromise: true, returnByValue: true }
});
cdp(args, 'evalraw', args.platformTarget, 'Target.sendMessageToTarget', JSON.stringify({
sessionId: attached.sessionId,
message
}));
const startedAt = Date.now();
let result = null;
while (Date.now() - startedAt < args.timeoutMs) {
try {
const response = parseJson(cdp(
args,
'eval',
args.platformTarget,
`(async()=>JSON.stringify(await sendToExtension('GET_TASK_RESULT',{task_id:${JSON.stringify(probeKey)}},3000)))()`
), 'probe result');
result = response?.result || null;
if (result?.status === 'completed') break;
} catch {
// The extension worker may still be completing the bounded read-only lookup.
}
await wait(500);
}
try {
cdp(args, 'evalraw', args.platformTarget, 'Target.detachFromTarget', JSON.stringify({ sessionId: attached.sessionId }));
} catch {
// Detachment is cleanup only; it does not affect the recorded read result.
}
if (!result) throw new Error(`read-only reference probe timed out after ${args.timeoutMs}ms`);
const rows = result.report?.results || [];
if (rows.length !== probe.targets.length || rows.some((row) => row.matched !== true)) {
console.log(JSON.stringify({ status: 'read_only_reference_probe_blocked', probe_key: probeKey, result }, null, 2));
process.exitCode = 2;
} else {
console.log(JSON.stringify({ status: 'read_only_reference_probe_completed', probe_key: probeKey, result }, null, 2));
}