131 lines
7.3 KiB
JavaScript
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;
|