260 lines
14 KiB
JavaScript
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));
|
|
}
|