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

131 lines
7.3 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 = { fixture: '', expectedRunId: '', platformTarget: DEFAULT_PLATFORM_TARGET, cdp: process.env.CDP_CLI || DEFAULT_CDP, timeoutMs: 45_000 };
for (let index = 0; index < argv.length; index += 1) {
const value = argv[index];
if (value === '--fixture') args.fixture = 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.fixture) throw new Error('--fixture is required');
if (!args.expectedRunId) throw new Error('--expected-run-id is required');
if (!Number.isFinite(args.timeoutMs) || args.timeoutMs < 10_000) throw new Error('--timeout-ms must be at least 10000');
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 validateOperation(operation, expectedRunId) {
const context = operation?.source?.test_context || {};
const refs = operation?.data?.existing_refs || {};
const errors = [];
if (!String(operation?.action || '').match(/^(?:passenger_list_import|arrangement_(?:guide|vehicle|hotel|transport|other)|order_update_(?:independent|shared_plan|shared_child)|order_(?:cancel|restore))$/)) errors.push('action_not_requery_allowlisted');
if (context.run_id !== expectedRunId) errors.push('run_id_mismatch');
if (context.marker !== 'TEST-202609' || refs.marker !== 'TEST-202609') errors.push('marker_mismatch');
if (context.account !== '测试ai员工账号' || refs.owner_account !== '测试ai员工账号') errors.push('owner_mismatch');
if (context.allow_live_write !== false) errors.push('read_only_context_required');
if (!/^2026-09-\d{2}$/.test(String(refs.departure_date || ''))) errors.push('date_outside_2026_09');
if (!String(refs.tid || '').match(/^\d+$/)) errors.push('tid_missing');
return errors;
}
const args = parseArgs(process.argv.slice(2));
const operation = JSON.parse(await readFile(args.fixture, 'utf8'));
const validationErrors = validateOperation(operation, args.expectedRunId);
if (validationErrors.length) throw new Error(`read-only lifecycle requery safety check failed: ${validationErrors.join(',')}`);
parseJson(cdp(args, 'eval', args.platformTarget, `(async()=>JSON.stringify(await sendToExtension('PING',{},3000)))()`), 'extension ping');
const targetList = parseJson(cdp(args, 'evalraw', args.platformTarget, 'Target.getTargets', '{}'), 'target list');
const worker = targetList.targetInfos?.find((target) => target.type === 'service_worker' && target.url === `chrome-extension://${EXTENSION_ID}/background.js`);
if (!worker?.targetId) throw new Error('extension_service_worker_missing');
const attached = parseJson(cdp(args, 'evalraw', args.platformTarget, 'Target.attachToTarget', JSON.stringify({ targetId: worker.targetId, flatten: false })), 'worker attach');
if (!attached.sessionId) throw new Error('extension_service_worker_attach_failed');
const probeKey = `READONLY-LIFECYCLE-${args.expectedRunId}-${Date.now()}`;
const expression = `(async()=>{
const operation=${JSON.stringify(operation)};
const probeKey=${JSON.stringify(probeKey)};
let report;
try{
const tab=await findOrOpenErpTab(probeKey);
const routeResults=await runPageAction(tab.id,'prepareLifecycleOperation',[operation],{allFrames:false},probeKey);
const routePreparation=normalizeLifecycleReport(routeResults[0]?.result||{
status:'lifecycle_route_blocked',blockers:['lifecycle_route_preparation_missing_result'],no_erp_write:true,write_attempted:false
});
if(!['lifecycle_route_ready','lifecycle_route_not_required'].includes(routePreparation.status)){
report={status:'blocked',stage:'route_preparation',route_preparation:routePreparation,write_attempted:false,no_erp_write:true};
}else{
const discovery=await discoverExactLifecycleFrame(tab.id,operation,probeKey);
if(discovery.status!=='ready'&&discovery.status!=='not_required'){
report={status:'blocked',stage:'frame_discovery',route_preparation:routePreparation,frame_discovery:discovery,write_attempted:false,no_erp_write:true};
}else{
const target=discovery.frame_id?{frameIds:[discovery.frame_id]}:{allFrames:true};
const verificationResults=await runPageAction(tab.id,'verifyLifecycleOperation',[operation],target,probeKey);
const verification=selectLifecycleRequeryResult(verificationResults);
report={
status:verification.status==='lifecycle_requery_matched'&&verification.requery?.matched===true?'completed':'not_matched',
stage:'read_only_lifecycle_requery',route_preparation:routePreparation,frame_discovery:discovery,
verification,write_attempted:false,no_erp_write:true
};
}
}
}catch(error){
report={status:'blocked',stage:'read_only_lifecycle_requery_error',error_name:String(error?.name||'Error'),error_message:String(error?.message||error).slice(0,500),write_attempted:false,no_erp_write:true};
}
const saved=await chrome.storage.local.get('businessTaskResults');
const resultMap=saved.businessTaskResults||{};
resultMap[probeKey]={...report,message:'read-only lifecycle requery completed'};
await chrome.storage.local.set({businessTaskResults:resultMap});
return 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 && ['completed', 'not_matched', 'blocked'].includes(result.status)) break;
} catch {
// The bounded read-only route and requery are still running.
}
await wait(500);
}
try {
cdp(args, 'evalraw', args.platformTarget, 'Target.detachFromTarget', JSON.stringify({ sessionId: attached.sessionId }));
} catch {
// Detachment is cleanup only.
}
if (!result) throw new Error(`read-only lifecycle requery timed out after ${args.timeoutMs}ms`);
console.log(JSON.stringify({ probe_key: probeKey, result }, null, 2));
if (result.status !== 'completed') process.exitCode = 2;