Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
20d3529068 |
96
src/index.ts
96
src/index.ts
|
|
@ -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,40 +233,47 @@ 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++) {
|
const ctx = await cdp.evaluate(`
|
||||||
await new Promise(r => setTimeout(r, 500));
|
(function() {
|
||||||
const ctx = await cdp.evaluate(`
|
try {
|
||||||
(function() {
|
const d = window.context && window.context.result && window.context.result.data;
|
||||||
try {
|
if (d && d.productPackInfo) {
|
||||||
const d = window.context && window.context.result && window.context.result.data;
|
return JSON.stringify({
|
||||||
if (d && d.productPackInfo) {
|
productPackInfo: d.productPackInfo,
|
||||||
return JSON.stringify({
|
productTitle: d.productTitle || null,
|
||||||
productPackInfo: d.productPackInfo,
|
productAttributes: d.productAttributes || null,
|
||||||
productTitle: d.productTitle || null,
|
skuSelection: d.skuSelection || null,
|
||||||
productAttributes: d.productAttributes || null,
|
});
|
||||||
skuSelection: d.skuSelection || null,
|
}
|
||||||
});
|
} catch(e) {}
|
||||||
}
|
return null;
|
||||||
} catch(e) {}
|
})()
|
||||||
return null;
|
`);
|
||||||
})()
|
if (ctx) {
|
||||||
`);
|
const parsed = JSON.parse(ctx);
|
||||||
if (ctx) {
|
productPackInfo = parsed.productPackInfo;
|
||||||
const parsed = JSON.parse(ctx);
|
windowContext = parsed;
|
||||||
productPackInfo = parsed.productPackInfo;
|
|
||||||
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);
|
||||||
|
|
||||||
// Capture full-page screenshots (scrolling)
|
// Capture full-page screenshots (scrolling)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue