Compare commits

...

1 Commits
v0.1.0 ... main

Author SHA1 Message Date
ywkj 20d3529068 fix: use Page.loadEventFired + networkIdle instead of fixed timeout
register-skill-release / register (push) Successful in 14s Details
Replace 15s polling loop with proper CDP event-based page load
detection: wait for Page.loadEventFired, then PerformanceObserver
network idle (no new resource requests for 1s). More reliable and
faster than fixed timeouts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 06:51:51 +08:00
1 changed files with 67 additions and 29 deletions

View File

@ -28,6 +28,7 @@ class CdpSession {
private ws!: WebSocket; private ws!: WebSocket;
private msgId = 0; private msgId = 0;
private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>(); private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
private eventListeners = new Map<string, Array<(params: any) => void>>();
static async connect(port: number): Promise<CdpSession> { static async connect(port: number): Promise<CdpSession> {
const resp = await fetch(`http://localhost:${port}/json`); const resp = await fetch(`http://localhost:${port}/json`);
@ -45,13 +46,20 @@ class CdpSession {
this.ws.onopen = () => resolve(); this.ws.onopen = () => resolve();
this.ws.onerror = (e: any) => reject(new Error(`WebSocket error: ${e.message || e}`)); this.ws.onerror = (e: any) => reject(new Error(`WebSocket error: ${e.message || e}`));
this.ws.onmessage = (ev: MessageEvent) => { this.ws.onmessage = (ev: MessageEvent) => {
const msg: CdpResult = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString()); const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : ev.data.toString());
// Handle command responses
if (msg.id != null && this.pending.has(msg.id)) { if (msg.id != null && this.pending.has(msg.id)) {
const p = this.pending.get(msg.id)!; const p = this.pending.get(msg.id)!;
this.pending.delete(msg.id); this.pending.delete(msg.id);
if (msg.error) p.reject(new Error(msg.error.message)); if (msg.error) p.reject(new Error(msg.error.message));
else p.resolve(msg.result); else p.resolve(msg.result);
} }
// Handle events
if (msg.method && this.eventListeners.has(msg.method)) {
for (const fn of this.eventListeners.get(msg.method)!) {
fn(msg.params);
}
}
}; };
}); });
} }
@ -64,6 +72,29 @@ class CdpSession {
}); });
} }
waitForEvent(event: string, timeoutMs: number = 30000): Promise<any> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for ${event}`));
}, timeoutMs);
const handler = (params: any) => {
cleanup();
resolve(params);
};
const cleanup = () => {
clearTimeout(timer);
const listeners = this.eventListeners.get(event);
if (listeners) {
const idx = listeners.indexOf(handler);
if (idx >= 0) listeners.splice(idx, 1);
}
};
if (!this.eventListeners.has(event)) this.eventListeners.set(event, []);
this.eventListeners.get(event)!.push(handler);
});
}
async evaluate(expression: string): Promise<any> { async evaluate(expression: string): Promise<any> {
const res = await this.send('Runtime.evaluate', { expression, returnByValue: true }); const res = await this.send('Runtime.evaluate', { expression, returnByValue: true });
return res?.result?.value; return res?.result?.value;
@ -202,13 +233,25 @@ export async function run(
mobile: false, mobile: false,
}); });
// Navigate and wait for page load event
const loadPromise = cdp.waitForEvent('Page.loadEventFired', 30000);
await cdp.send('Page.navigate', { url }); await cdp.send('Page.navigate', { url });
await loadPromise;
// Wait for window.context.result.data to be populated (poll up to 15s) // Wait for networkIdle — poll until no pending requests for 1s
await cdp.evaluate(`
new Promise(resolve => {
let timer;
const reset = () => { clearTimeout(timer); timer = setTimeout(resolve, 1000); };
const observer = new PerformanceObserver(() => reset());
observer.observe({ entryTypes: ['resource'] });
reset();
})
`);
// Extract window.context.result.data
let productPackInfo: unknown = null; let productPackInfo: unknown = null;
let windowContext: unknown = null; let windowContext: unknown = null;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 500));
const ctx = await cdp.evaluate(` const ctx = await cdp.evaluate(`
(function() { (function() {
try { try {
@ -229,12 +272,7 @@ export async function run(
const parsed = JSON.parse(ctx); const parsed = JSON.parse(ctx);
productPackInfo = parsed.productPackInfo; productPackInfo = parsed.productPackInfo;
windowContext = parsed; windowContext = parsed;
break;
} }
}
// Extra wait for remaining dynamic content (images etc)
await new Promise(r => setTimeout(r, 2000));
const outputDir = path.join('/tmp', '1688-logistics', offerId); const outputDir = path.join('/tmp', '1688-logistics', offerId);