@jackwener/opencli 1.5.7 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +17 -1
- package/README.zh-CN.md +17 -1
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +1 -7
- package/dist/browser/daemon-client.js +2 -9
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +1 -4
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +6 -35
- package/dist/browser/page.js +10 -189
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/commanderAdapter.js +6 -3
- package/dist/commanderAdapter.test.js +33 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/daemon.test.js +1 -1
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/extension-manifest-regression.test.js +1 -0
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +3 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/extension/dist/background.js +614 -794
- package/extension/manifest.json +2 -1
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +7 -156
- package/extension/src/cdp.test.ts +75 -0
- package/extension/src/cdp.ts +77 -3
- package/extension/src/protocol.ts +1 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +847 -0
- package/skills/opencli-oneshot/SKILL.md +216 -0
- package/skills/opencli-usage/SKILL.md +71 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.ts +3 -14
- package/src/browser/discover.ts +1 -4
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +13 -212
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +7 -3
- package/src/commands/daemon.test.ts +1 -1
- package/src/commands/daemon.ts +1 -1
- package/src/doctor.ts +7 -8
- package/src/explore.ts +1 -1
- package/src/extension-manifest-regression.test.ts +1 -0
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +3 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/SKILL.md +0 -879
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js → bridge.js} +0 -0
- /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
package/src/browser/page.ts
CHANGED
|
@@ -10,25 +10,13 @@
|
|
|
10
10
|
* chrome-extension:// tab that can't be debugged.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
13
|
+
import type { BrowserCookie, ScreenshotOptions } from '../types.js';
|
|
15
14
|
import { sendCommand } from './daemon-client.js';
|
|
16
15
|
import { wrapForEval } from './utils.js';
|
|
17
16
|
import { saveBase64ToFile } from '../utils.js';
|
|
18
|
-
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
19
17
|
import { generateStealthJs } from './stealth.js';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
typeTextJs,
|
|
23
|
-
pressKeyJs,
|
|
24
|
-
waitForTextJs,
|
|
25
|
-
waitForCaptureJs,
|
|
26
|
-
waitForSelectorJs,
|
|
27
|
-
scrollJs,
|
|
28
|
-
autoScrollJs,
|
|
29
|
-
networkRequestsJs,
|
|
30
|
-
waitForDomStableJs,
|
|
31
|
-
} from './dom-helpers.js';
|
|
18
|
+
import { waitForDomStableJs } from './dom-helpers.js';
|
|
19
|
+
import { BasePage } from './base-page.js';
|
|
32
20
|
|
|
33
21
|
export function isRetryableSettleError(err: unknown): boolean {
|
|
34
22
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -39,13 +27,13 @@ export function isRetryableSettleError(err: unknown): boolean {
|
|
|
39
27
|
/**
|
|
40
28
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
41
29
|
*/
|
|
42
|
-
export class Page
|
|
43
|
-
constructor(private readonly workspace: string = 'default') {
|
|
30
|
+
export class Page extends BasePage {
|
|
31
|
+
constructor(private readonly workspace: string = 'default') {
|
|
32
|
+
super();
|
|
33
|
+
}
|
|
44
34
|
|
|
45
35
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
46
36
|
private _tabId: number | undefined;
|
|
47
|
-
/** Last navigated URL, tracked in-memory to avoid extra round-trips */
|
|
48
|
-
private _lastUrl: string | null = null;
|
|
49
37
|
|
|
50
38
|
/** Helper: spread workspace into command params */
|
|
51
39
|
private _wsOpt(): { workspace: string } {
|
|
@@ -107,27 +95,8 @@ export class Page implements IPage {
|
|
|
107
95
|
}
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const current = await this.evaluate('window.location.href');
|
|
114
|
-
if (typeof current === 'string' && current) {
|
|
115
|
-
this._lastUrl = current;
|
|
116
|
-
return current;
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
// Best-effort: some commands may run before a debuggable tab is ready.
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Close the automation window in the extension */
|
|
125
|
-
async closeWindow(): Promise<void> {
|
|
126
|
-
try {
|
|
127
|
-
await sendCommand('close-window', { ...this._wsOpt() });
|
|
128
|
-
} catch {
|
|
129
|
-
// Window may already be closed or daemon may be down
|
|
130
|
-
}
|
|
98
|
+
getActiveTabId(): number | undefined {
|
|
99
|
+
return this._tabId;
|
|
131
100
|
}
|
|
132
101
|
|
|
133
102
|
async evaluate(js: string): Promise<unknown> {
|
|
@@ -146,124 +115,12 @@ export class Page implements IPage {
|
|
|
146
115
|
return Array.isArray(result) ? result : [];
|
|
147
116
|
}
|
|
148
117
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const snapshotJs = generateSnapshotJs({
|
|
152
|
-
viewportExpand: opts.viewportExpand ?? 800,
|
|
153
|
-
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
|
|
154
|
-
interactiveOnly: opts.interactive ?? false,
|
|
155
|
-
maxTextLength: opts.maxTextLength ?? 120,
|
|
156
|
-
includeScrollInfo: true,
|
|
157
|
-
bboxDedup: true,
|
|
158
|
-
});
|
|
159
|
-
|
|
118
|
+
/** Close the automation window in the extension */
|
|
119
|
+
async closeWindow(): Promise<void> {
|
|
160
120
|
try {
|
|
161
|
-
|
|
162
|
-
// The advanced engine already produces a clean, pruned, LLM-friendly output.
|
|
163
|
-
// Do NOT pass through formatSnapshot — its format is incompatible.
|
|
164
|
-
return result;
|
|
121
|
+
await sendCommand('close-window', { ...this._wsOpt() });
|
|
165
122
|
} catch {
|
|
166
|
-
//
|
|
167
|
-
return this._basicSnapshot(opts);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Fallback basic snapshot — original buildTree approach */
|
|
172
|
-
private async _basicSnapshot(opts: Pick<SnapshotOptions, 'interactive' | 'compact' | 'maxDepth' | 'raw'> = {}): Promise<unknown> {
|
|
173
|
-
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
|
|
174
|
-
const code = `
|
|
175
|
-
(async () => {
|
|
176
|
-
function buildTree(node, depth) {
|
|
177
|
-
if (depth > ${maxDepth}) return '';
|
|
178
|
-
const role = node.getAttribute?.('role') || node.tagName?.toLowerCase() || 'generic';
|
|
179
|
-
const name = node.getAttribute?.('aria-label') || node.getAttribute?.('alt') || node.textContent?.trim().slice(0, 80) || '';
|
|
180
|
-
const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(node.tagName?.toLowerCase()) || node.getAttribute?.('tabindex') != null;
|
|
181
|
-
|
|
182
|
-
${opts.interactive ? 'if (!isInteractive && !node.children?.length) return "";' : ''}
|
|
183
|
-
|
|
184
|
-
let indent = ' '.repeat(depth);
|
|
185
|
-
let line = indent + role;
|
|
186
|
-
if (name) line += ' "' + name.replace(/"/g, '\\\\\\"') + '"';
|
|
187
|
-
if (node.tagName?.toLowerCase() === 'a' && node.href) line += ' [' + node.href + ']';
|
|
188
|
-
if (node.tagName?.toLowerCase() === 'input') line += ' [' + (node.type || 'text') + ']';
|
|
189
|
-
|
|
190
|
-
let result = line + '\\n';
|
|
191
|
-
if (node.children) {
|
|
192
|
-
for (const child of node.children) {
|
|
193
|
-
result += buildTree(child, depth + 1);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
|
-
return buildTree(document.body, 0);
|
|
199
|
-
})()
|
|
200
|
-
`;
|
|
201
|
-
const raw = await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
202
|
-
if (opts.raw) return raw;
|
|
203
|
-
if (typeof raw === 'string') return formatSnapshot(raw, opts);
|
|
204
|
-
return raw;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async click(ref: string): Promise<void> {
|
|
208
|
-
const code = clickJs(ref);
|
|
209
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async typeText(ref: string, text: string): Promise<void> {
|
|
213
|
-
const code = typeTextJs(ref, text);
|
|
214
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async pressKey(key: string): Promise<void> {
|
|
218
|
-
const code = pressKeyJs(key);
|
|
219
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async scrollTo(ref: string): Promise<unknown> {
|
|
223
|
-
const code = scrollToRefJs(ref);
|
|
224
|
-
return sendCommand('exec', { code, ...this._cmdOpts() });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async getFormState(): Promise<Record<string, unknown>> {
|
|
228
|
-
const code = getFormStateJs();
|
|
229
|
-
return (await sendCommand('exec', { code, ...this._cmdOpts() })) as Record<string, unknown>;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async wait(options: number | WaitOptions): Promise<void> {
|
|
233
|
-
if (typeof options === 'number') {
|
|
234
|
-
if (options >= 1) {
|
|
235
|
-
// For waits >= 1s, use DOM-stable check: return early when the page
|
|
236
|
-
// stops mutating, with the original wait time as the hard cap.
|
|
237
|
-
// This turns e.g. `page.wait(5)` from a fixed 5s sleep into
|
|
238
|
-
// "wait until DOM is stable, max 5s" — often completing in <1s.
|
|
239
|
-
try {
|
|
240
|
-
const maxMs = options * 1000;
|
|
241
|
-
await sendCommand('exec', {
|
|
242
|
-
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
|
|
243
|
-
...this._cmdOpts(),
|
|
244
|
-
});
|
|
245
|
-
return;
|
|
246
|
-
} catch {
|
|
247
|
-
// Fallback: fixed sleep (e.g. if page has no DOM yet)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (typeof options.time === 'number') {
|
|
254
|
-
await new Promise(resolve => setTimeout(resolve, options.time! * 1000));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (options.selector) {
|
|
258
|
-
const timeout = (options.timeout ?? 10) * 1000;
|
|
259
|
-
const code = waitForSelectorJs(options.selector, timeout);
|
|
260
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (options.text) {
|
|
264
|
-
const timeout = (options.timeout ?? 30) * 1000;
|
|
265
|
-
const code = waitForTextJs(options.text, timeout);
|
|
266
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
123
|
+
// Window may already be closed or daemon may be down
|
|
267
124
|
}
|
|
268
125
|
}
|
|
269
126
|
|
|
@@ -289,27 +146,8 @@ export class Page implements IPage {
|
|
|
289
146
|
if (result?.selected) this._tabId = result.selected;
|
|
290
147
|
}
|
|
291
148
|
|
|
292
|
-
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
|
|
293
|
-
const code = networkRequestsJs(includeStatic);
|
|
294
|
-
const result = await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
295
|
-
return Array.isArray(result) ? result : [];
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Console messages are not available in lightweight daemon mode.
|
|
300
|
-
* Would require CDP Runtime.consoleAPICalled event listener.
|
|
301
|
-
* @returns Always returns empty array.
|
|
302
|
-
*/
|
|
303
|
-
async consoleMessages(_level: string = 'info'): Promise<unknown[]> {
|
|
304
|
-
return [];
|
|
305
|
-
}
|
|
306
|
-
|
|
307
149
|
/**
|
|
308
150
|
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
309
|
-
* @param options.format - 'png' (default) or 'jpeg'
|
|
310
|
-
* @param options.quality - JPEG quality 0-100
|
|
311
|
-
* @param options.fullPage - capture full scrollable page
|
|
312
|
-
* @param options.path - save to file path (returns base64 if omitted)
|
|
313
151
|
*/
|
|
314
152
|
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
|
|
315
153
|
const base64 = await sendCommand('screenshot', {
|
|
@@ -326,35 +164,6 @@ export class Page implements IPage {
|
|
|
326
164
|
return base64;
|
|
327
165
|
}
|
|
328
166
|
|
|
329
|
-
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
330
|
-
const code = scrollJs(direction, amount);
|
|
331
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
|
|
335
|
-
const times = options.times ?? 3;
|
|
336
|
-
const delayMs = options.delayMs ?? 2000;
|
|
337
|
-
const code = autoScrollJs(times, delayMs);
|
|
338
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async installInterceptor(pattern: string): Promise<void> {
|
|
342
|
-
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
343
|
-
// Must use evaluate() so wrapForEval() converts the arrow function into an IIFE;
|
|
344
|
-
// sendCommand('exec') sends the code as-is, and CDP never executes a bare arrow.
|
|
345
|
-
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
346
|
-
arrayName: '__opencli_xhr',
|
|
347
|
-
patchGuard: '__opencli_interceptor_patched',
|
|
348
|
-
}));
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async getInterceptedRequests(): Promise<unknown[]> {
|
|
352
|
-
const { generateReadInterceptedJs } = await import('../interceptor.js');
|
|
353
|
-
// Same as installInterceptor: must go through evaluate() for IIFE wrapping
|
|
354
|
-
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
355
|
-
return Array.isArray(result) ? result : [];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
167
|
/**
|
|
359
168
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
360
169
|
* Chrome reads the files directly from the local filesystem, avoiding the
|
|
@@ -370,14 +179,6 @@ export class Page implements IPage {
|
|
|
370
179
|
throw new Error('setFileInput returned no count — command may not be supported by the extension');
|
|
371
180
|
}
|
|
372
181
|
}
|
|
373
|
-
|
|
374
|
-
async waitForCapture(timeout: number = 10): Promise<void> {
|
|
375
|
-
const maxMs = timeout * 1000;
|
|
376
|
-
await sendCommand('exec', {
|
|
377
|
-
code: waitForCaptureJs(maxMs),
|
|
378
|
-
...this._cmdOpts(),
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
182
|
}
|
|
382
183
|
|
|
383
184
|
// (End of file)
|
package/src/browser/tabs.ts
CHANGED
|
@@ -21,12 +21,12 @@ export function extractTabEntries(raw: unknown): Array<{ index: number; identity
|
|
|
21
21
|
.map(line => line.trim())
|
|
22
22
|
.filter(Boolean)
|
|
23
23
|
.map(line => {
|
|
24
|
-
// Match
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
24
|
+
// Match tab list format: "- 0: (current) [title](url)" or "- 1: [title](url)"
|
|
25
|
+
const tabMatch = line.match(/^-\s+(\d+):\s*(.*)$/);
|
|
26
|
+
if (tabMatch) {
|
|
27
27
|
return {
|
|
28
|
-
index: parseInt(
|
|
29
|
-
identity:
|
|
28
|
+
index: parseInt(tabMatch[1], 10),
|
|
29
|
+
identity: tabMatch[2].trim() || `tab-${tabMatch[1]}`,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
// Legacy format: "Tab 0 ..."
|
package/src/browser.test.ts
CHANGED
|
@@ -104,43 +104,43 @@ describe('browser helpers', () => {
|
|
|
104
104
|
|
|
105
105
|
describe('BrowserBridge state', () => {
|
|
106
106
|
it('transitions to closed after close()', async () => {
|
|
107
|
-
const
|
|
107
|
+
const bridge = new BrowserBridge();
|
|
108
108
|
|
|
109
|
-
expect(
|
|
109
|
+
expect(bridge.state).toBe('idle');
|
|
110
110
|
|
|
111
|
-
await
|
|
111
|
+
await bridge.close();
|
|
112
112
|
|
|
113
|
-
expect(
|
|
113
|
+
expect(bridge.state).toBe('closed');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
it('rejects connect() after the session has been closed', async () => {
|
|
117
|
-
const
|
|
118
|
-
await
|
|
117
|
+
const bridge = new BrowserBridge();
|
|
118
|
+
await bridge.close();
|
|
119
119
|
|
|
120
|
-
await expect(
|
|
120
|
+
await expect(bridge.connect()).rejects.toThrow('Session is closed');
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('rejects connect() while already connecting', async () => {
|
|
124
|
-
const
|
|
125
|
-
(
|
|
124
|
+
const bridge = new BrowserBridge();
|
|
125
|
+
(bridge as any)._state = 'connecting';
|
|
126
126
|
|
|
127
|
-
await expect(
|
|
127
|
+
await expect(bridge.connect()).rejects.toThrow('Already connecting');
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
it('rejects connect() while closing', async () => {
|
|
131
|
-
const
|
|
132
|
-
(
|
|
131
|
+
const bridge = new BrowserBridge();
|
|
132
|
+
(bridge as any)._state = 'closing';
|
|
133
133
|
|
|
134
|
-
await expect(
|
|
134
|
+
await expect(bridge.connect()).rejects.toThrow('Session is closing');
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
138
138
|
vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
|
|
139
139
|
vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
|
|
140
140
|
|
|
141
|
-
const
|
|
141
|
+
const bridge = new BrowserBridge();
|
|
142
142
|
|
|
143
|
-
await expect(
|
|
143
|
+
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Browser Extension is not connected');
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './bestsellers.js';
|
|
3
|
+
|
|
4
|
+
describe('amazon bestsellers normalization', () => {
|
|
5
|
+
it('normalizes bestseller cards and infers review counts from card text', () => {
|
|
6
|
+
const result = __test__.normalizeBestsellerCandidate({
|
|
7
|
+
asin: 'B0DR31GC3D',
|
|
8
|
+
title: '',
|
|
9
|
+
href: 'https://www.amazon.com/NUTIKAS-Shelves-Desktop-Orgnizer-Shlef/dp/B0DR31GC3D/ref=zg_bs',
|
|
10
|
+
price_text: '$25.92',
|
|
11
|
+
rating_text: '4.3 out of 5 stars',
|
|
12
|
+
review_count_text: '',
|
|
13
|
+
card_text: 'Desk Shelves Desktop Organizer Shlef\n4.3 out of 5 stars\n435\n$25.92',
|
|
14
|
+
}, 2, 'Amazon Best Sellers: Best Desktop & Off-Surface Shelves', 'https://www.amazon.com/example');
|
|
15
|
+
|
|
16
|
+
expect(result.rank).toBe(2);
|
|
17
|
+
expect(result.asin).toBe('B0DR31GC3D');
|
|
18
|
+
expect(result.title).toBe('Desk Shelves Desktop Organizer Shlef');
|
|
19
|
+
expect(result.review_count).toBe(435);
|
|
20
|
+
expect(result.list_title).toBe('Amazon Best Sellers: Best Desktop & Off-Surface Shelves');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { CommandExecutionError } from '../../errors.js';
|
|
2
|
+
import { cli, Strategy } from '../../registry.js';
|
|
3
|
+
import type { IPage } from '../../types.js';
|
|
4
|
+
import {
|
|
5
|
+
buildProvenance,
|
|
6
|
+
cleanText,
|
|
7
|
+
extractAsin,
|
|
8
|
+
extractReviewCountFromCardText,
|
|
9
|
+
firstMeaningfulLine,
|
|
10
|
+
normalizeProductUrl,
|
|
11
|
+
parsePriceText,
|
|
12
|
+
parseRatingValue,
|
|
13
|
+
parseReviewCount,
|
|
14
|
+
resolveBestsellersUrl,
|
|
15
|
+
uniqueNonEmpty,
|
|
16
|
+
assertUsableState,
|
|
17
|
+
gotoAndReadState,
|
|
18
|
+
} from './shared.js';
|
|
19
|
+
|
|
20
|
+
interface BestsellersPagePayload {
|
|
21
|
+
href?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
list_title?: string;
|
|
24
|
+
cards?: Array<{
|
|
25
|
+
rank_text?: string | null;
|
|
26
|
+
asin?: string | null;
|
|
27
|
+
title?: string | null;
|
|
28
|
+
href?: string | null;
|
|
29
|
+
price_text?: string | null;
|
|
30
|
+
rating_text?: string | null;
|
|
31
|
+
review_count_text?: string | null;
|
|
32
|
+
card_text?: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
page_links?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeBestsellerCandidate(
|
|
38
|
+
candidate: NonNullable<BestsellersPagePayload['cards']>[number],
|
|
39
|
+
rank: number,
|
|
40
|
+
listTitle: string | null,
|
|
41
|
+
sourceUrl: string,
|
|
42
|
+
): Record<string, unknown> {
|
|
43
|
+
const productUrl = normalizeProductUrl(candidate.href);
|
|
44
|
+
const asin = extractAsin(candidate.asin ?? '') ?? extractAsin(productUrl ?? '') ?? null;
|
|
45
|
+
const title = cleanText(candidate.title) || firstMeaningfulLine(candidate.card_text);
|
|
46
|
+
const price = parsePriceText(cleanText(candidate.price_text) || candidate.card_text);
|
|
47
|
+
const ratingText = cleanText(candidate.rating_text) || null;
|
|
48
|
+
const reviewCountText = cleanText(candidate.review_count_text)
|
|
49
|
+
|| extractReviewCountFromCardText(candidate.card_text)
|
|
50
|
+
|| null;
|
|
51
|
+
const provenance = buildProvenance(sourceUrl);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
rank,
|
|
55
|
+
asin,
|
|
56
|
+
title: title || null,
|
|
57
|
+
product_url: productUrl,
|
|
58
|
+
list_title: listTitle,
|
|
59
|
+
...provenance,
|
|
60
|
+
price_text: price.price_text,
|
|
61
|
+
price_value: price.price_value,
|
|
62
|
+
currency: price.currency,
|
|
63
|
+
rating_text: ratingText,
|
|
64
|
+
rating_value: parseRatingValue(ratingText),
|
|
65
|
+
review_count_text: reviewCountText,
|
|
66
|
+
review_count: parseReviewCount(reviewCountText),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readBestsellersPage(page: IPage, url: string): Promise<BestsellersPagePayload> {
|
|
71
|
+
const state = await gotoAndReadState(page, url, 2500, 'bestsellers');
|
|
72
|
+
assertUsableState(state, 'bestsellers');
|
|
73
|
+
|
|
74
|
+
return await page.evaluate(`
|
|
75
|
+
(() => ({
|
|
76
|
+
href: window.location.href,
|
|
77
|
+
title: document.title || '',
|
|
78
|
+
list_title:
|
|
79
|
+
document.querySelector('#zg_banner_text')?.textContent
|
|
80
|
+
|| document.querySelector('h1')?.textContent
|
|
81
|
+
|| '',
|
|
82
|
+
cards: Array.from(document.querySelectorAll('.p13n-sc-uncoverable-faceout'))
|
|
83
|
+
.map((card) => ({
|
|
84
|
+
rank_text:
|
|
85
|
+
card.querySelector('.zg-bdg-text')?.textContent
|
|
86
|
+
|| card.querySelector('[class*="rank"]')?.textContent
|
|
87
|
+
|| '',
|
|
88
|
+
asin: card.id || '',
|
|
89
|
+
title:
|
|
90
|
+
card.querySelector('[class*="line-clamp"]')?.textContent
|
|
91
|
+
|| card.querySelector('img')?.getAttribute('alt')
|
|
92
|
+
|| '',
|
|
93
|
+
href: card.querySelector('a[href*="/dp/"]')?.href || '',
|
|
94
|
+
price_text: card.querySelector('.a-price .a-offscreen')?.textContent || '',
|
|
95
|
+
rating_text: card.querySelector('[aria-label*="out of 5 stars"]')?.getAttribute('aria-label') || '',
|
|
96
|
+
review_count_text:
|
|
97
|
+
card.querySelector('a[href*="#customerReviews"]')?.textContent
|
|
98
|
+
|| card.querySelector('.a-size-small')?.textContent
|
|
99
|
+
|| '',
|
|
100
|
+
card_text: card.innerText || '',
|
|
101
|
+
})),
|
|
102
|
+
page_links: Array.from(document.querySelectorAll('li.a-normal a, li.a-selected a'))
|
|
103
|
+
.map((anchor) => anchor.href || '')
|
|
104
|
+
.filter((href) => /\\/zgbs\\//.test(href) && /(?:[?&]pg=|ref=zg_bs_pg_)/.test(href)),
|
|
105
|
+
}))()
|
|
106
|
+
`) as BestsellersPagePayload;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cli({
|
|
110
|
+
site: 'amazon',
|
|
111
|
+
name: 'bestsellers',
|
|
112
|
+
description: 'Amazon Best Sellers pages for category candidate discovery',
|
|
113
|
+
domain: 'amazon.com',
|
|
114
|
+
strategy: Strategy.COOKIE,
|
|
115
|
+
navigateBefore: false,
|
|
116
|
+
args: [
|
|
117
|
+
{
|
|
118
|
+
name: 'input',
|
|
119
|
+
positional: true,
|
|
120
|
+
help: 'Best sellers URL or /zgbs path. Omit to use the root Best Sellers page.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'limit',
|
|
124
|
+
type: 'int',
|
|
125
|
+
default: 100,
|
|
126
|
+
help: 'Maximum number of ranked items to return (default 100)',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
columns: ['rank', 'asin', 'title', 'price_text', 'rating_value', 'review_count'],
|
|
130
|
+
func: async (page, kwargs) => {
|
|
131
|
+
const limit = Math.max(1, Number(kwargs.limit) || 100);
|
|
132
|
+
const initialUrl = resolveBestsellersUrl(typeof kwargs.input === 'string' ? kwargs.input : undefined);
|
|
133
|
+
|
|
134
|
+
const queue = [initialUrl];
|
|
135
|
+
const visited = new Set<string>();
|
|
136
|
+
const seenAsins = new Set<string>();
|
|
137
|
+
const results: Record<string, unknown>[] = [];
|
|
138
|
+
let listTitle: string | null = null;
|
|
139
|
+
|
|
140
|
+
while (queue.length > 0 && results.length < limit) {
|
|
141
|
+
const nextUrl = queue.shift()!;
|
|
142
|
+
if (visited.has(nextUrl)) continue;
|
|
143
|
+
visited.add(nextUrl);
|
|
144
|
+
|
|
145
|
+
const payload = await readBestsellersPage(page, nextUrl);
|
|
146
|
+
const sourceUrl = cleanText(payload.href) || nextUrl;
|
|
147
|
+
listTitle = cleanText(payload.list_title) || cleanText(payload.title) || listTitle;
|
|
148
|
+
const cards = payload.cards ?? [];
|
|
149
|
+
|
|
150
|
+
for (const card of cards) {
|
|
151
|
+
const normalized = normalizeBestsellerCandidate(card, results.length + 1, listTitle, sourceUrl);
|
|
152
|
+
const asin = cleanText(String(normalized.asin ?? ''));
|
|
153
|
+
if (!asin || seenAsins.has(asin)) continue;
|
|
154
|
+
seenAsins.add(asin);
|
|
155
|
+
results.push(normalized);
|
|
156
|
+
if (results.length >= limit) break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const pageLinks = uniqueNonEmpty(payload.page_links ?? []);
|
|
160
|
+
for (const href of pageLinks) {
|
|
161
|
+
if (!visited.has(href) && !queue.includes(href)) {
|
|
162
|
+
queue.push(href);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (results.length === 0) {
|
|
168
|
+
throw new CommandExecutionError(
|
|
169
|
+
'amazon bestsellers did not expose any ranked items',
|
|
170
|
+
'Open the same best sellers page in Chrome, verify it is a real Amazon ranking page, and retry.',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return results.slice(0, limit);
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const __test__ = {
|
|
179
|
+
normalizeBestsellerCandidate,
|
|
180
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __test__ } from './discussion.js';
|
|
3
|
+
|
|
4
|
+
describe('amazon discussion normalization', () => {
|
|
5
|
+
it('normalizes review summary and sample reviews', () => {
|
|
6
|
+
const result = __test__.normalizeDiscussionPayload({
|
|
7
|
+
href: 'https://www.amazon.com/product-reviews/B0FJS72893',
|
|
8
|
+
average_rating_text: '3.9 out of 5',
|
|
9
|
+
total_review_count_text: '27 global ratings',
|
|
10
|
+
qa_links: [],
|
|
11
|
+
review_samples: [
|
|
12
|
+
{
|
|
13
|
+
title: '5.0 out of 5 stars Great value and quality',
|
|
14
|
+
rating_text: '5.0 out of 5 stars',
|
|
15
|
+
author: 'GTreader2',
|
|
16
|
+
date_text: 'Reviewed in the United States on February 21, 2026',
|
|
17
|
+
body: 'Small but mighty.',
|
|
18
|
+
verified: true,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.asin).toBe('B0FJS72893');
|
|
24
|
+
expect(result.average_rating_value).toBe(3.9);
|
|
25
|
+
expect(result.total_review_count).toBe(27);
|
|
26
|
+
expect(result.review_samples).toEqual([
|
|
27
|
+
{
|
|
28
|
+
title: 'Great value and quality',
|
|
29
|
+
rating_text: '5.0 out of 5 stars',
|
|
30
|
+
rating_value: 5,
|
|
31
|
+
author: 'GTreader2',
|
|
32
|
+
date_text: 'Reviewed in the United States on February 21, 2026',
|
|
33
|
+
body: 'Small but mighty.',
|
|
34
|
+
verified_purchase: true,
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
});
|