@jackwener/opencli 1.0.3 → 1.0.4
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/.github/workflows/build-extension.yml +21 -3
- package/.github/workflows/docs.yml +52 -0
- package/README.md +28 -28
- package/README.zh-CN.md +28 -28
- package/dist/browser/cdp.d.ts +16 -1
- package/dist/browser/cdp.js +124 -80
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +37 -100
- package/dist/browser.test.js +7 -0
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.test.js +11 -4
- package/dist/cli-manifest.json +635 -1
- package/dist/cli.js +45 -8
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/xiaohongshu/download.js +3 -3
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +5 -5
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +3 -3
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +92 -52
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +122 -51
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +154 -82
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +45 -100
- package/src/browser.test.ts +10 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +12 -4
- package/src/cli.ts +28 -8
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/xiaohongshu/download.ts +3 -3
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +4 -4
- package/src/explore.ts +4 -4
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/output.ts +3 -1
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +6 -2
- package/src/runtime.ts +3 -2
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
package/src/browser/page.ts
CHANGED
|
@@ -14,11 +14,22 @@ import { formatSnapshot } from '../snapshotFormatter.js';
|
|
|
14
14
|
import type { IPage } from '../types.js';
|
|
15
15
|
import { sendCommand } from './daemon-client.js';
|
|
16
16
|
import { wrapForEval } from './utils.js';
|
|
17
|
+
import {
|
|
18
|
+
clickJs,
|
|
19
|
+
typeTextJs,
|
|
20
|
+
pressKeyJs,
|
|
21
|
+
waitForTextJs,
|
|
22
|
+
scrollJs,
|
|
23
|
+
autoScrollJs,
|
|
24
|
+
networkRequestsJs,
|
|
25
|
+
} from './dom-helpers.js';
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
28
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
20
29
|
*/
|
|
21
30
|
export class Page implements IPage {
|
|
31
|
+
constructor(private readonly workspace: string = 'default') {}
|
|
32
|
+
|
|
22
33
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
23
34
|
private _tabId: number | undefined;
|
|
24
35
|
|
|
@@ -27,9 +38,14 @@ export class Page implements IPage {
|
|
|
27
38
|
return this._tabId !== undefined ? { tabId: this._tabId } : {};
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
private _workspaceOpt(): { workspace: string } {
|
|
42
|
+
return { workspace: this.workspace };
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
async goto(url: string): Promise<void> {
|
|
31
46
|
const result = await sendCommand('navigate', {
|
|
32
47
|
url,
|
|
48
|
+
...this._workspaceOpt(),
|
|
33
49
|
...this._tabOpt(),
|
|
34
50
|
}) as { tabId?: number };
|
|
35
51
|
// Remember the tabId for subsequent exec calls
|
|
@@ -41,7 +57,7 @@ export class Page implements IPage {
|
|
|
41
57
|
/** Close the automation window in the extension */
|
|
42
58
|
async closeWindow(): Promise<void> {
|
|
43
59
|
try {
|
|
44
|
-
await sendCommand('close-window', {});
|
|
60
|
+
await sendCommand('close-window', { ...this._workspaceOpt() });
|
|
45
61
|
} catch {
|
|
46
62
|
// Window may already be closed or daemon may be down
|
|
47
63
|
}
|
|
@@ -49,7 +65,12 @@ export class Page implements IPage {
|
|
|
49
65
|
|
|
50
66
|
async evaluate(js: string): Promise<any> {
|
|
51
67
|
const code = wrapForEval(js);
|
|
52
|
-
return sendCommand('exec', { code, ...this._tabOpt() });
|
|
68
|
+
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getCookies(opts: { domain?: string; url?: string } = {}): Promise<any[]> {
|
|
72
|
+
const result = await sendCommand('cookies', { ...this._workspaceOpt(), ...opts });
|
|
73
|
+
return Array.isArray(result) ? result : [];
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
|
|
@@ -81,57 +102,25 @@ export class Page implements IPage {
|
|
|
81
102
|
return buildTree(document.body, 0);
|
|
82
103
|
})()
|
|
83
104
|
`;
|
|
84
|
-
const raw = await sendCommand('exec', { code, ...this._tabOpt() });
|
|
105
|
+
const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
85
106
|
if (opts.raw) return raw;
|
|
86
107
|
if (typeof raw === 'string') return formatSnapshot(raw, opts);
|
|
87
108
|
return raw;
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
async click(ref: string): Promise<void> {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
(() => {
|
|
94
|
-
const ref = ${safeRef};
|
|
95
|
-
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
96
|
-
|| document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
|
|
97
|
-
if (!el) throw new Error('Element not found: ' + ref);
|
|
98
|
-
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
99
|
-
el.click();
|
|
100
|
-
return 'clicked';
|
|
101
|
-
})()
|
|
102
|
-
`;
|
|
103
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
112
|
+
const code = clickJs(ref);
|
|
113
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
async typeText(ref: string, text: string): Promise<void> {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const code = `
|
|
110
|
-
(() => {
|
|
111
|
-
const ref = ${safeRef};
|
|
112
|
-
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
113
|
-
|| document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
|
|
114
|
-
if (!el) throw new Error('Element not found: ' + ref);
|
|
115
|
-
el.focus();
|
|
116
|
-
el.value = ${safeText};
|
|
117
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
118
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
119
|
-
return 'typed';
|
|
120
|
-
})()
|
|
121
|
-
`;
|
|
122
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
117
|
+
const code = typeTextJs(ref, text);
|
|
118
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
async pressKey(key: string): Promise<void> {
|
|
126
|
-
const code =
|
|
127
|
-
|
|
128
|
-
const el = document.activeElement || document.body;
|
|
129
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
130
|
-
el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
131
|
-
return 'pressed';
|
|
132
|
-
})()
|
|
133
|
-
`;
|
|
134
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
122
|
+
const code = pressKeyJs(key);
|
|
123
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
135
124
|
}
|
|
136
125
|
|
|
137
126
|
async wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void> {
|
|
@@ -145,52 +134,30 @@ export class Page implements IPage {
|
|
|
145
134
|
}
|
|
146
135
|
if (options.text) {
|
|
147
136
|
const timeout = (options.timeout ?? 30) * 1000;
|
|
148
|
-
const code =
|
|
149
|
-
|
|
150
|
-
const deadline = Date.now() + ${timeout};
|
|
151
|
-
const check = () => {
|
|
152
|
-
if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
|
|
153
|
-
if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
|
|
154
|
-
setTimeout(check, 200);
|
|
155
|
-
};
|
|
156
|
-
check();
|
|
157
|
-
})
|
|
158
|
-
`;
|
|
159
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
137
|
+
const code = waitForTextJs(options.text, timeout);
|
|
138
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
160
139
|
}
|
|
161
140
|
}
|
|
162
141
|
|
|
163
142
|
async tabs(): Promise<any> {
|
|
164
|
-
return sendCommand('tabs', { op: 'list' });
|
|
143
|
+
return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
|
|
165
144
|
}
|
|
166
145
|
|
|
167
146
|
async closeTab(index?: number): Promise<void> {
|
|
168
|
-
await sendCommand('tabs', { op: 'close', ...(index !== undefined ? { index } : {}) });
|
|
147
|
+
await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
|
|
169
148
|
}
|
|
170
149
|
|
|
171
150
|
async newTab(): Promise<void> {
|
|
172
|
-
await sendCommand('tabs', { op: 'new' });
|
|
151
|
+
await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() });
|
|
173
152
|
}
|
|
174
153
|
|
|
175
154
|
async selectTab(index: number): Promise<void> {
|
|
176
|
-
await sendCommand('tabs', { op: 'select', index });
|
|
155
|
+
await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
|
|
177
156
|
}
|
|
178
157
|
|
|
179
158
|
async networkRequests(includeStatic: boolean = false): Promise<any> {
|
|
180
|
-
const code =
|
|
181
|
-
|
|
182
|
-
const entries = performance.getEntriesByType('resource');
|
|
183
|
-
return entries
|
|
184
|
-
${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
|
|
185
|
-
.map(e => ({
|
|
186
|
-
url: e.name,
|
|
187
|
-
type: e.initiatorType,
|
|
188
|
-
duration: Math.round(e.duration),
|
|
189
|
-
size: e.transferSize || 0,
|
|
190
|
-
}));
|
|
191
|
-
})()
|
|
192
|
-
`;
|
|
193
|
-
return sendCommand('exec', { code, ...this._tabOpt() });
|
|
159
|
+
const code = networkRequestsJs(includeStatic);
|
|
160
|
+
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
194
161
|
}
|
|
195
162
|
|
|
196
163
|
/**
|
|
@@ -216,6 +183,7 @@ export class Page implements IPage {
|
|
|
216
183
|
path?: string;
|
|
217
184
|
} = {}): Promise<string> {
|
|
218
185
|
const base64 = await sendCommand('screenshot', {
|
|
186
|
+
...this._workspaceOpt(),
|
|
219
187
|
format: options.format,
|
|
220
188
|
quality: options.quality,
|
|
221
189
|
fullPage: options.fullPage,
|
|
@@ -226,46 +194,23 @@ export class Page implements IPage {
|
|
|
226
194
|
const fs = await import('node:fs');
|
|
227
195
|
const path = await import('node:path');
|
|
228
196
|
const dir = path.dirname(options.path);
|
|
229
|
-
fs.
|
|
230
|
-
fs.
|
|
197
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
198
|
+
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
|
|
231
199
|
}
|
|
232
200
|
|
|
233
201
|
return base64;
|
|
234
202
|
}
|
|
235
203
|
|
|
236
204
|
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
await sendCommand('exec', {
|
|
240
|
-
code: `window.scrollBy(${dx}, ${dy})`,
|
|
241
|
-
...this._tabOpt(),
|
|
242
|
-
});
|
|
205
|
+
const code = scrollJs(direction, amount);
|
|
206
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
243
207
|
}
|
|
244
208
|
|
|
245
209
|
async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
|
|
246
210
|
const times = options.times ?? 3;
|
|
247
211
|
const delayMs = options.delayMs ?? 2000;
|
|
248
|
-
const code =
|
|
249
|
-
|
|
250
|
-
for (let i = 0; i < ${times}; i++) {
|
|
251
|
-
const lastHeight = document.body.scrollHeight;
|
|
252
|
-
window.scrollTo(0, lastHeight);
|
|
253
|
-
await new Promise(resolve => {
|
|
254
|
-
let timeoutId;
|
|
255
|
-
const observer = new MutationObserver(() => {
|
|
256
|
-
if (document.body.scrollHeight > lastHeight) {
|
|
257
|
-
clearTimeout(timeoutId);
|
|
258
|
-
observer.disconnect();
|
|
259
|
-
setTimeout(resolve, 100);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
263
|
-
timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
})()
|
|
267
|
-
`;
|
|
268
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
212
|
+
const code = autoScrollJs(times, delayMs);
|
|
213
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
269
214
|
}
|
|
270
215
|
|
|
271
216
|
async installInterceptor(pattern: string): Promise<void> {
|
package/src/browser.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { BrowserBridge, __test__ } from './browser/index.js';
|
|
3
|
+
import * as daemonClient from './browser/daemon-client.js';
|
|
3
4
|
|
|
4
5
|
describe('browser helpers', () => {
|
|
5
6
|
it('extracts tab entries from string snapshots', () => {
|
|
@@ -122,4 +123,13 @@ describe('BrowserBridge state', () => {
|
|
|
122
123
|
|
|
123
124
|
await expect(mcp.connect()).rejects.toThrow('Session is closing');
|
|
124
125
|
});
|
|
126
|
+
|
|
127
|
+
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
128
|
+
vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
|
|
129
|
+
vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
|
|
130
|
+
|
|
131
|
+
const mcp = new BrowserBridge();
|
|
132
|
+
|
|
133
|
+
await expect(mcp.connect()).rejects.toThrow('Browser Extension is not connected');
|
|
134
|
+
});
|
|
125
135
|
});
|
|
@@ -25,4 +25,40 @@ describe('parseTsArgsBlock', () => {
|
|
|
25
25
|
},
|
|
26
26
|
]);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('keeps hyphenated arg names from TS adapters', () => {
|
|
30
|
+
const args = parseTsArgsBlock(`
|
|
31
|
+
{
|
|
32
|
+
name: 'tweet-url',
|
|
33
|
+
help: 'Single tweet URL to download',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'download-images',
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
default: false,
|
|
39
|
+
help: 'Download images locally',
|
|
40
|
+
},
|
|
41
|
+
`);
|
|
42
|
+
|
|
43
|
+
expect(args).toEqual([
|
|
44
|
+
{
|
|
45
|
+
name: 'tweet-url',
|
|
46
|
+
type: 'str',
|
|
47
|
+
default: undefined,
|
|
48
|
+
required: false,
|
|
49
|
+
positional: undefined,
|
|
50
|
+
help: 'Single tweet URL to download',
|
|
51
|
+
choices: undefined,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'download-images',
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
default: false,
|
|
57
|
+
required: false,
|
|
58
|
+
positional: undefined,
|
|
59
|
+
help: 'Download images locally',
|
|
60
|
+
choices: undefined,
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
28
64
|
});
|
package/src/build-manifest.ts
CHANGED
|
@@ -114,7 +114,7 @@ export function parseTsArgsBlock(argsBlock: string): ManifestEntry['args'] {
|
|
|
114
114
|
let cursor = 0;
|
|
115
115
|
|
|
116
116
|
while (cursor < argsBlock.length) {
|
|
117
|
-
const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`](
|
|
117
|
+
const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
118
118
|
if (!nameMatch || nameMatch.index === undefined) break;
|
|
119
119
|
|
|
120
120
|
const objectStart = cursor + nameMatch.index;
|
|
@@ -231,6 +231,7 @@ function scanTs(filePath: string, site: string): ManifestEntry {
|
|
|
231
231
|
// Extract browser: false (some adapters bypass browser entirely)
|
|
232
232
|
const browserMatch = src.match(/browser\s*:\s*(true|false)/);
|
|
233
233
|
if (browserMatch) entry.browser = browserMatch[1] === 'true';
|
|
234
|
+
else entry.browser = entry.strategy !== 'public';
|
|
234
235
|
|
|
235
236
|
// Extract columns
|
|
236
237
|
const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Strategy, type CliCommand } from './registry.js';
|
|
3
|
+
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
4
|
+
|
|
5
|
+
function makeCmd(partial: Partial<CliCommand>): CliCommand {
|
|
6
|
+
return {
|
|
7
|
+
site: 'test',
|
|
8
|
+
name: 'command',
|
|
9
|
+
description: '',
|
|
10
|
+
args: [],
|
|
11
|
+
...partial,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('shouldUseBrowserSession', () => {
|
|
16
|
+
it('skips browser session for public fetch-only pipelines', () => {
|
|
17
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
18
|
+
browser: true,
|
|
19
|
+
strategy: Strategy.PUBLIC,
|
|
20
|
+
pipeline: [{ fetch: 'https://example.com/api' }, { select: 'items' }],
|
|
21
|
+
}))).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps browser session for public pipelines with browser-only steps', () => {
|
|
25
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
26
|
+
browser: true,
|
|
27
|
+
strategy: Strategy.PUBLIC,
|
|
28
|
+
pipeline: [{ navigate: 'https://example.com' }, { evaluate: '() => []' }],
|
|
29
|
+
}))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps browser session for non-public strategies', () => {
|
|
33
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
34
|
+
browser: true,
|
|
35
|
+
strategy: Strategy.COOKIE,
|
|
36
|
+
pipeline: [{ fetch: 'https://example.com/api' }],
|
|
37
|
+
}))).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('keeps browser session for function adapters', () => {
|
|
41
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
42
|
+
browser: true,
|
|
43
|
+
strategy: Strategy.PUBLIC,
|
|
44
|
+
func: async () => [],
|
|
45
|
+
}))).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Strategy, type CliCommand } from './registry.js';
|
|
2
|
+
|
|
3
|
+
const BROWSER_ONLY_STEPS = new Set([
|
|
4
|
+
'navigate',
|
|
5
|
+
'click',
|
|
6
|
+
'type',
|
|
7
|
+
'wait',
|
|
8
|
+
'press',
|
|
9
|
+
'snapshot',
|
|
10
|
+
'evaluate',
|
|
11
|
+
'intercept',
|
|
12
|
+
'tap',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function pipelineNeedsBrowserSession(pipeline: Record<string, unknown>[]): boolean {
|
|
16
|
+
return pipeline.some((step) => {
|
|
17
|
+
if (!step || typeof step !== 'object') return false;
|
|
18
|
+
return Object.keys(step).some((op) => BROWSER_ONLY_STEPS.has(op));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shouldUseBrowserSession(cmd: CliCommand): boolean {
|
|
23
|
+
if (!cmd.browser) return false;
|
|
24
|
+
if (cmd.func) return true;
|
|
25
|
+
if (!cmd.pipeline || cmd.pipeline.length === 0) return true;
|
|
26
|
+
if (cmd.strategy !== Strategy.PUBLIC) return true;
|
|
27
|
+
return pipelineNeedsBrowserSession(cmd.pipeline as Record<string, unknown>[]);
|
|
28
|
+
}
|
package/src/chaoxing.test.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { formatTimestamp, workStatusLabel } from './chaoxing.js';
|
|
3
3
|
|
|
4
|
+
function localDatePrefixFromMillis(ts: number): string {
|
|
5
|
+
const d = new Date(ts);
|
|
6
|
+
const yyyy = d.getFullYear();
|
|
7
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
8
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
9
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
describe('formatTimestamp', () => {
|
|
5
13
|
it('formats millisecond timestamp', () => {
|
|
6
|
-
// 2026-01-15 08:30 UTC+8
|
|
7
14
|
const ts = new Date('2026-01-15T00:30:00Z').getTime();
|
|
8
15
|
const result = formatTimestamp(ts);
|
|
9
|
-
expect(result).toMatch(
|
|
16
|
+
expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(ts)}\\s`));
|
|
10
17
|
});
|
|
11
18
|
|
|
12
19
|
it('formats second timestamp', () => {
|
|
13
|
-
const
|
|
20
|
+
const millis = new Date('2026-06-01T12:00:00Z').getTime();
|
|
21
|
+
const ts = Math.floor(millis / 1000);
|
|
14
22
|
const result = formatTimestamp(ts);
|
|
15
|
-
expect(result).toMatch(
|
|
23
|
+
expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(millis)}\\s`));
|
|
16
24
|
});
|
|
17
25
|
|
|
18
26
|
it('returns empty for null/undefined/0', () => {
|
package/src/cli.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { executeCommand } from './engine.js';
|
|
4
|
-
import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
|
|
4
|
+
import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
|
|
5
5
|
import { render as renderOutput } from './output.js';
|
|
6
6
|
import { BrowserBridge, CDPBridge } from './browser/index.js';
|
|
7
7
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
8
8
|
import { PKG_VERSION } from './version.js';
|
|
9
9
|
import { printCompletionScript } from './completion.js';
|
|
10
10
|
import { CliError } from './errors.js';
|
|
11
|
+
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
11
12
|
|
|
12
13
|
export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
13
14
|
const program = new Command();
|
|
@@ -64,13 +65,13 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
|
|
67
|
-
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory as any, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
|
|
68
|
+
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `explore:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}`; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory as any, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels, workspace }))); });
|
|
68
69
|
|
|
69
70
|
program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
|
|
70
71
|
.action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
|
|
71
72
|
|
|
72
73
|
program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
|
|
73
|
-
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory as any, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
74
|
+
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `generate:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}`; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory as any, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site, workspace }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
74
75
|
|
|
75
76
|
program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
|
|
76
77
|
.action(async (url, opts) => {
|
|
@@ -80,16 +81,17 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
80
81
|
// Navigate to the site first for cookie context
|
|
81
82
|
try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {}
|
|
82
83
|
return cascadeProbe(page, url);
|
|
83
|
-
});
|
|
84
|
+
}, { workspace: `cascade:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}` });
|
|
84
85
|
console.log(renderCascadeResult(result));
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
program.command('doctor')
|
|
88
89
|
.description('Diagnose opencli browser bridge connectivity')
|
|
89
90
|
.option('--live', 'Test browser connectivity (requires Chrome running)', false)
|
|
91
|
+
.option('--sessions', 'Show active automation sessions', false)
|
|
90
92
|
.action(async (opts) => {
|
|
91
93
|
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
|
|
92
|
-
const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
|
|
94
|
+
const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
|
|
93
95
|
console.log(renderBrowserDoctorReport(report));
|
|
94
96
|
});
|
|
95
97
|
|
|
@@ -107,10 +109,23 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
107
109
|
printCompletionScript(shell);
|
|
108
110
|
});
|
|
109
111
|
|
|
112
|
+
// ── Antigravity serve (built-in, long-running) ──────────────────────────────
|
|
113
|
+
|
|
114
|
+
const antigravityCmd = program.command('antigravity').description('antigravity commands');
|
|
115
|
+
antigravityCmd.command('serve')
|
|
116
|
+
.description('Start Anthropic-compatible API proxy for Antigravity')
|
|
117
|
+
.option('--port <port>', 'Server port (default: 8082)', '8082')
|
|
118
|
+
.action(async (opts) => {
|
|
119
|
+
const { startServe } = await import('./clis/antigravity/serve.js');
|
|
120
|
+
await startServe({ port: parseInt(opts.port) });
|
|
121
|
+
});
|
|
122
|
+
|
|
110
123
|
// ── Dynamic site commands ──────────────────────────────────────────────────
|
|
111
124
|
|
|
112
125
|
const registry = getRegistry();
|
|
113
126
|
const siteGroups = new Map<string, Command>();
|
|
127
|
+
// Pre-seed with the antigravity command registered above to avoid duplicates
|
|
128
|
+
siteGroups.set('antigravity', antigravityCmd);
|
|
114
129
|
|
|
115
130
|
for (const [, cmd] of registry) {
|
|
116
131
|
let siteCmd = siteGroups.get(cmd.site);
|
|
@@ -157,16 +172,21 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
|
|
|
157
172
|
try {
|
|
158
173
|
if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
|
|
159
174
|
let result: any;
|
|
160
|
-
if (cmd
|
|
175
|
+
if (shouldUseBrowserSession(cmd)) {
|
|
161
176
|
const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
|
|
162
177
|
result = await browserSession(BrowserFactory as any, async (page) => {
|
|
178
|
+
// Cookie/header strategies require same-origin context for credentialed fetch.
|
|
179
|
+
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
|
|
180
|
+
try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
|
|
181
|
+
}
|
|
163
182
|
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
|
|
164
|
-
});
|
|
183
|
+
}, { workspace: `site:${cmd.site}` });
|
|
165
184
|
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
|
|
166
185
|
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
167
186
|
console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
|
|
168
187
|
}
|
|
169
|
-
|
|
188
|
+
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
|
|
189
|
+
renderOutput(result, { fmt: actionOpts.format, columns: resolved.columns, title: `${resolved.site}/${resolved.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(resolved), footerExtra: resolved.footerExtra?.(kwargs) });
|
|
170
190
|
} catch (err: any) {
|
|
171
191
|
if (err instanceof CliError) {
|
|
172
192
|
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|