@jackwener/opencli 1.0.3 → 1.0.5
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 +48 -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 +30 -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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM operation JS generators.
|
|
3
|
+
*
|
|
4
|
+
* Used by both Page (daemon mode) and CDPPage (direct CDP mode)
|
|
5
|
+
* to eliminate code duplication for click, type, press, wait, scroll, etc.
|
|
6
|
+
*/
|
|
7
|
+
/** Generate JS to click an element by ref */
|
|
8
|
+
export function clickJs(ref) {
|
|
9
|
+
const safeRef = JSON.stringify(ref);
|
|
10
|
+
return `
|
|
11
|
+
(() => {
|
|
12
|
+
const ref = ${safeRef};
|
|
13
|
+
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
14
|
+
|| document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
|
|
15
|
+
if (!el) throw new Error('Element not found: ' + ref);
|
|
16
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
17
|
+
el.click();
|
|
18
|
+
return 'clicked';
|
|
19
|
+
})()
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
/** Generate JS to type text into an element by ref */
|
|
23
|
+
export function typeTextJs(ref, text) {
|
|
24
|
+
const safeRef = JSON.stringify(ref);
|
|
25
|
+
const safeText = JSON.stringify(text);
|
|
26
|
+
return `
|
|
27
|
+
(() => {
|
|
28
|
+
const ref = ${safeRef};
|
|
29
|
+
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
30
|
+
|| document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
|
|
31
|
+
if (!el) throw new Error('Element not found: ' + ref);
|
|
32
|
+
el.focus();
|
|
33
|
+
el.value = ${safeText};
|
|
34
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
35
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
36
|
+
return 'typed';
|
|
37
|
+
})()
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
/** Generate JS to press a keyboard key */
|
|
41
|
+
export function pressKeyJs(key) {
|
|
42
|
+
return `
|
|
43
|
+
(() => {
|
|
44
|
+
const el = document.activeElement || document.body;
|
|
45
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
46
|
+
el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
47
|
+
return 'pressed';
|
|
48
|
+
})()
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
/** Generate JS to wait for text to appear in the page */
|
|
52
|
+
export function waitForTextJs(text, timeoutMs) {
|
|
53
|
+
return `
|
|
54
|
+
new Promise((resolve, reject) => {
|
|
55
|
+
const deadline = Date.now() + ${timeoutMs};
|
|
56
|
+
const check = () => {
|
|
57
|
+
if (document.body.innerText.includes(${JSON.stringify(text)})) return resolve('found');
|
|
58
|
+
if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(text)}));
|
|
59
|
+
setTimeout(check, 200);
|
|
60
|
+
};
|
|
61
|
+
check();
|
|
62
|
+
})
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
/** Generate JS for scroll */
|
|
66
|
+
export function scrollJs(direction, amount) {
|
|
67
|
+
const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
|
|
68
|
+
const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
|
|
69
|
+
return `window.scrollBy(${dx}, ${dy})`;
|
|
70
|
+
}
|
|
71
|
+
/** Generate JS for auto-scroll with lazy-load detection */
|
|
72
|
+
export function autoScrollJs(times, delayMs) {
|
|
73
|
+
return `
|
|
74
|
+
(async () => {
|
|
75
|
+
for (let i = 0; i < ${times}; i++) {
|
|
76
|
+
const lastHeight = document.body.scrollHeight;
|
|
77
|
+
window.scrollTo(0, lastHeight);
|
|
78
|
+
await new Promise(resolve => {
|
|
79
|
+
let timeoutId;
|
|
80
|
+
const observer = new MutationObserver(() => {
|
|
81
|
+
if (document.body.scrollHeight > lastHeight) {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
setTimeout(resolve, 100);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
88
|
+
timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
})()
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
/** Generate JS to read performance resource entries as network requests */
|
|
95
|
+
export function networkRequestsJs(includeStatic) {
|
|
96
|
+
return `
|
|
97
|
+
(() => {
|
|
98
|
+
const entries = performance.getEntriesByType('resource');
|
|
99
|
+
return entries
|
|
100
|
+
${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
|
|
101
|
+
.map(e => ({
|
|
102
|
+
url: e.name,
|
|
103
|
+
type: e.initiatorType,
|
|
104
|
+
duration: Math.round(e.duration),
|
|
105
|
+
size: e.transferSize || 0,
|
|
106
|
+
}));
|
|
107
|
+
})()
|
|
108
|
+
`;
|
|
109
|
+
}
|
package/dist/browser/mcp.d.ts
CHANGED
package/dist/browser/mcp.js
CHANGED
|
@@ -29,8 +29,8 @@ export class BrowserBridge {
|
|
|
29
29
|
throw new Error('Session is closed');
|
|
30
30
|
this._state = 'connecting';
|
|
31
31
|
try {
|
|
32
|
-
await this._ensureDaemon();
|
|
33
|
-
this._page = new Page();
|
|
32
|
+
await this._ensureDaemon(opts.timeout);
|
|
33
|
+
this._page = new Page(opts.workspace);
|
|
34
34
|
this._state = 'connected';
|
|
35
35
|
return this._page;
|
|
36
36
|
}
|
|
@@ -48,9 +48,14 @@ export class BrowserBridge {
|
|
|
48
48
|
this._page = null;
|
|
49
49
|
this._state = 'closed';
|
|
50
50
|
}
|
|
51
|
-
async _ensureDaemon() {
|
|
52
|
-
|
|
51
|
+
async _ensureDaemon(timeoutSeconds) {
|
|
52
|
+
const timeoutMs = Math.max(1, timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000)) * 1000;
|
|
53
|
+
if (await isExtensionConnected())
|
|
53
54
|
return;
|
|
55
|
+
if (await isDaemonRunning()) {
|
|
56
|
+
throw new Error('Daemon is running but the Browser Extension is not connected.\n' +
|
|
57
|
+
'Please install and enable the opencli Browser Bridge extension in Chrome.');
|
|
58
|
+
}
|
|
54
59
|
// Find daemon relative to this file — works for both:
|
|
55
60
|
// npx tsx src/main.ts → src/browser/mcp.ts → src/daemon.ts
|
|
56
61
|
// node dist/main.js → dist/browser/mcp.js → dist/daemon.js
|
|
@@ -75,7 +80,7 @@ export class BrowserBridge {
|
|
|
75
80
|
});
|
|
76
81
|
this._daemonProc.unref();
|
|
77
82
|
// Wait for daemon to be ready AND extension to connect
|
|
78
|
-
const deadline = Date.now() +
|
|
83
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
84
|
while (Date.now() < deadline) {
|
|
80
85
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
81
86
|
if (await isExtensionConnected())
|
package/dist/browser/page.d.ts
CHANGED
|
@@ -14,14 +14,21 @@ import type { IPage } from '../types.js';
|
|
|
14
14
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
15
15
|
*/
|
|
16
16
|
export declare class Page implements IPage {
|
|
17
|
+
private readonly workspace;
|
|
18
|
+
constructor(workspace?: string);
|
|
17
19
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
18
20
|
private _tabId;
|
|
19
21
|
/** Helper: spread tabId into command params if we have one */
|
|
20
22
|
private _tabOpt;
|
|
23
|
+
private _workspaceOpt;
|
|
21
24
|
goto(url: string): Promise<void>;
|
|
22
25
|
/** Close the automation window in the extension */
|
|
23
26
|
closeWindow(): Promise<void>;
|
|
24
27
|
evaluate(js: string): Promise<any>;
|
|
28
|
+
getCookies(opts?: {
|
|
29
|
+
domain?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
}): Promise<any[]>;
|
|
25
32
|
snapshot(opts?: {
|
|
26
33
|
interactive?: boolean;
|
|
27
34
|
compact?: boolean;
|
package/dist/browser/page.js
CHANGED
|
@@ -12,19 +12,28 @@
|
|
|
12
12
|
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
13
13
|
import { sendCommand } from './daemon-client.js';
|
|
14
14
|
import { wrapForEval } from './utils.js';
|
|
15
|
+
import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, } from './dom-helpers.js';
|
|
15
16
|
/**
|
|
16
17
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
17
18
|
*/
|
|
18
19
|
export class Page {
|
|
20
|
+
workspace;
|
|
21
|
+
constructor(workspace = 'default') {
|
|
22
|
+
this.workspace = workspace;
|
|
23
|
+
}
|
|
19
24
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
20
25
|
_tabId;
|
|
21
26
|
/** Helper: spread tabId into command params if we have one */
|
|
22
27
|
_tabOpt() {
|
|
23
28
|
return this._tabId !== undefined ? { tabId: this._tabId } : {};
|
|
24
29
|
}
|
|
30
|
+
_workspaceOpt() {
|
|
31
|
+
return { workspace: this.workspace };
|
|
32
|
+
}
|
|
25
33
|
async goto(url) {
|
|
26
34
|
const result = await sendCommand('navigate', {
|
|
27
35
|
url,
|
|
36
|
+
...this._workspaceOpt(),
|
|
28
37
|
...this._tabOpt(),
|
|
29
38
|
});
|
|
30
39
|
// Remember the tabId for subsequent exec calls
|
|
@@ -35,7 +44,7 @@ export class Page {
|
|
|
35
44
|
/** Close the automation window in the extension */
|
|
36
45
|
async closeWindow() {
|
|
37
46
|
try {
|
|
38
|
-
await sendCommand('close-window', {});
|
|
47
|
+
await sendCommand('close-window', { ...this._workspaceOpt() });
|
|
39
48
|
}
|
|
40
49
|
catch {
|
|
41
50
|
// Window may already be closed or daemon may be down
|
|
@@ -43,7 +52,11 @@ export class Page {
|
|
|
43
52
|
}
|
|
44
53
|
async evaluate(js) {
|
|
45
54
|
const code = wrapForEval(js);
|
|
46
|
-
return sendCommand('exec', { code, ...this._tabOpt() });
|
|
55
|
+
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
56
|
+
}
|
|
57
|
+
async getCookies(opts = {}) {
|
|
58
|
+
const result = await sendCommand('cookies', { ...this._workspaceOpt(), ...opts });
|
|
59
|
+
return Array.isArray(result) ? result : [];
|
|
47
60
|
}
|
|
48
61
|
async snapshot(opts = {}) {
|
|
49
62
|
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
|
|
@@ -74,7 +87,7 @@ export class Page {
|
|
|
74
87
|
return buildTree(document.body, 0);
|
|
75
88
|
})()
|
|
76
89
|
`;
|
|
77
|
-
const raw = await sendCommand('exec', { code, ...this._tabOpt() });
|
|
90
|
+
const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
78
91
|
if (opts.raw)
|
|
79
92
|
return raw;
|
|
80
93
|
if (typeof raw === 'string')
|
|
@@ -82,48 +95,16 @@ export class Page {
|
|
|
82
95
|
return raw;
|
|
83
96
|
}
|
|
84
97
|
async click(ref) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
(() => {
|
|
88
|
-
const ref = ${safeRef};
|
|
89
|
-
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
90
|
-
|| document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
|
|
91
|
-
if (!el) throw new Error('Element not found: ' + ref);
|
|
92
|
-
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
93
|
-
el.click();
|
|
94
|
-
return 'clicked';
|
|
95
|
-
})()
|
|
96
|
-
`;
|
|
97
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
98
|
+
const code = clickJs(ref);
|
|
99
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
98
100
|
}
|
|
99
101
|
async typeText(ref, text) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const code = `
|
|
103
|
-
(() => {
|
|
104
|
-
const ref = ${safeRef};
|
|
105
|
-
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
106
|
-
|| document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
|
|
107
|
-
if (!el) throw new Error('Element not found: ' + ref);
|
|
108
|
-
el.focus();
|
|
109
|
-
el.value = ${safeText};
|
|
110
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
111
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
112
|
-
return 'typed';
|
|
113
|
-
})()
|
|
114
|
-
`;
|
|
115
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
102
|
+
const code = typeTextJs(ref, text);
|
|
103
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
116
104
|
}
|
|
117
105
|
async pressKey(key) {
|
|
118
|
-
const code =
|
|
119
|
-
|
|
120
|
-
const el = document.activeElement || document.body;
|
|
121
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
122
|
-
el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
123
|
-
return 'pressed';
|
|
124
|
-
})()
|
|
125
|
-
`;
|
|
126
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
106
|
+
const code = pressKeyJs(key);
|
|
107
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
127
108
|
}
|
|
128
109
|
async wait(options) {
|
|
129
110
|
if (typeof options === 'number') {
|
|
@@ -136,47 +117,25 @@ export class Page {
|
|
|
136
117
|
}
|
|
137
118
|
if (options.text) {
|
|
138
119
|
const timeout = (options.timeout ?? 30) * 1000;
|
|
139
|
-
const code =
|
|
140
|
-
|
|
141
|
-
const deadline = Date.now() + ${timeout};
|
|
142
|
-
const check = () => {
|
|
143
|
-
if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
|
|
144
|
-
if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
|
|
145
|
-
setTimeout(check, 200);
|
|
146
|
-
};
|
|
147
|
-
check();
|
|
148
|
-
})
|
|
149
|
-
`;
|
|
150
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
120
|
+
const code = waitForTextJs(options.text, timeout);
|
|
121
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
151
122
|
}
|
|
152
123
|
}
|
|
153
124
|
async tabs() {
|
|
154
|
-
return sendCommand('tabs', { op: 'list' });
|
|
125
|
+
return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
|
|
155
126
|
}
|
|
156
127
|
async closeTab(index) {
|
|
157
|
-
await sendCommand('tabs', { op: 'close', ...(index !== undefined ? { index } : {}) });
|
|
128
|
+
await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
|
|
158
129
|
}
|
|
159
130
|
async newTab() {
|
|
160
|
-
await sendCommand('tabs', { op: 'new' });
|
|
131
|
+
await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() });
|
|
161
132
|
}
|
|
162
133
|
async selectTab(index) {
|
|
163
|
-
await sendCommand('tabs', { op: 'select', index });
|
|
134
|
+
await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
|
|
164
135
|
}
|
|
165
136
|
async networkRequests(includeStatic = false) {
|
|
166
|
-
const code =
|
|
167
|
-
|
|
168
|
-
const entries = performance.getEntriesByType('resource');
|
|
169
|
-
return entries
|
|
170
|
-
${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
|
|
171
|
-
.map(e => ({
|
|
172
|
-
url: e.name,
|
|
173
|
-
type: e.initiatorType,
|
|
174
|
-
duration: Math.round(e.duration),
|
|
175
|
-
size: e.transferSize || 0,
|
|
176
|
-
}));
|
|
177
|
-
})()
|
|
178
|
-
`;
|
|
179
|
-
return sendCommand('exec', { code, ...this._tabOpt() });
|
|
137
|
+
const code = networkRequestsJs(includeStatic);
|
|
138
|
+
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
180
139
|
}
|
|
181
140
|
/**
|
|
182
141
|
* Console messages are not available in lightweight daemon mode.
|
|
@@ -195,6 +154,7 @@ export class Page {
|
|
|
195
154
|
*/
|
|
196
155
|
async screenshot(options = {}) {
|
|
197
156
|
const base64 = await sendCommand('screenshot', {
|
|
157
|
+
...this._workspaceOpt(),
|
|
198
158
|
format: options.format,
|
|
199
159
|
quality: options.quality,
|
|
200
160
|
fullPage: options.fullPage,
|
|
@@ -204,43 +164,20 @@ export class Page {
|
|
|
204
164
|
const fs = await import('node:fs');
|
|
205
165
|
const path = await import('node:path');
|
|
206
166
|
const dir = path.dirname(options.path);
|
|
207
|
-
fs.
|
|
208
|
-
fs.
|
|
167
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
168
|
+
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
|
|
209
169
|
}
|
|
210
170
|
return base64;
|
|
211
171
|
}
|
|
212
172
|
async scroll(direction = 'down', amount = 500) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
await sendCommand('exec', {
|
|
216
|
-
code: `window.scrollBy(${dx}, ${dy})`,
|
|
217
|
-
...this._tabOpt(),
|
|
218
|
-
});
|
|
173
|
+
const code = scrollJs(direction, amount);
|
|
174
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
219
175
|
}
|
|
220
176
|
async autoScroll(options = {}) {
|
|
221
177
|
const times = options.times ?? 3;
|
|
222
178
|
const delayMs = options.delayMs ?? 2000;
|
|
223
|
-
const code =
|
|
224
|
-
|
|
225
|
-
for (let i = 0; i < ${times}; i++) {
|
|
226
|
-
const lastHeight = document.body.scrollHeight;
|
|
227
|
-
window.scrollTo(0, lastHeight);
|
|
228
|
-
await new Promise(resolve => {
|
|
229
|
-
let timeoutId;
|
|
230
|
-
const observer = new MutationObserver(() => {
|
|
231
|
-
if (document.body.scrollHeight > lastHeight) {
|
|
232
|
-
clearTimeout(timeoutId);
|
|
233
|
-
observer.disconnect();
|
|
234
|
-
setTimeout(resolve, 100);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
238
|
-
timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
})()
|
|
242
|
-
`;
|
|
243
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
179
|
+
const code = autoScrollJs(times, delayMs);
|
|
180
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
244
181
|
}
|
|
245
182
|
async installInterceptor(pattern) {
|
|
246
183
|
const { generateInterceptorJs } = await import('../interceptor.js');
|
package/dist/browser.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { 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
|
describe('browser helpers', () => {
|
|
4
5
|
it('extracts tab entries from string snapshots', () => {
|
|
5
6
|
const entries = __test__.extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
|
|
@@ -94,4 +95,10 @@ describe('BrowserBridge state', () => {
|
|
|
94
95
|
mcp._state = 'closing';
|
|
95
96
|
await expect(mcp.connect()).rejects.toThrow('Session is closing');
|
|
96
97
|
});
|
|
98
|
+
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
99
|
+
vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
|
|
100
|
+
vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
|
|
101
|
+
const mcp = new BrowserBridge();
|
|
102
|
+
await expect(mcp.connect()).rejects.toThrow('Browser Extension is not connected');
|
|
103
|
+
});
|
|
97
104
|
});
|
package/dist/build-manifest.js
CHANGED
|
@@ -73,7 +73,7 @@ export function parseTsArgsBlock(argsBlock) {
|
|
|
73
73
|
const args = [];
|
|
74
74
|
let cursor = 0;
|
|
75
75
|
while (cursor < argsBlock.length) {
|
|
76
|
-
const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`](
|
|
76
|
+
const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
77
77
|
if (!nameMatch || nameMatch.index === undefined)
|
|
78
78
|
break;
|
|
79
79
|
const objectStart = cursor + nameMatch.index;
|
|
@@ -186,6 +186,8 @@ function scanTs(filePath, site) {
|
|
|
186
186
|
const browserMatch = src.match(/browser\s*:\s*(true|false)/);
|
|
187
187
|
if (browserMatch)
|
|
188
188
|
entry.browser = browserMatch[1] === 'true';
|
|
189
|
+
else
|
|
190
|
+
entry.browser = entry.strategy !== 'public';
|
|
189
191
|
// Extract columns
|
|
190
192
|
const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
|
|
191
193
|
if (colMatch) {
|
|
@@ -23,4 +23,38 @@ describe('parseTsArgsBlock', () => {
|
|
|
23
23
|
},
|
|
24
24
|
]);
|
|
25
25
|
});
|
|
26
|
+
it('keeps hyphenated arg names from TS adapters', () => {
|
|
27
|
+
const args = parseTsArgsBlock(`
|
|
28
|
+
{
|
|
29
|
+
name: 'tweet-url',
|
|
30
|
+
help: 'Single tweet URL to download',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'download-images',
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
default: false,
|
|
36
|
+
help: 'Download images locally',
|
|
37
|
+
},
|
|
38
|
+
`);
|
|
39
|
+
expect(args).toEqual([
|
|
40
|
+
{
|
|
41
|
+
name: 'tweet-url',
|
|
42
|
+
type: 'str',
|
|
43
|
+
default: undefined,
|
|
44
|
+
required: false,
|
|
45
|
+
positional: undefined,
|
|
46
|
+
help: 'Single tweet URL to download',
|
|
47
|
+
choices: undefined,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'download-images',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: false,
|
|
53
|
+
required: false,
|
|
54
|
+
positional: undefined,
|
|
55
|
+
help: 'Download images locally',
|
|
56
|
+
choices: undefined,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
26
60
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Strategy } from './registry.js';
|
|
2
|
+
const BROWSER_ONLY_STEPS = new Set([
|
|
3
|
+
'navigate',
|
|
4
|
+
'click',
|
|
5
|
+
'type',
|
|
6
|
+
'wait',
|
|
7
|
+
'press',
|
|
8
|
+
'snapshot',
|
|
9
|
+
'evaluate',
|
|
10
|
+
'intercept',
|
|
11
|
+
'tap',
|
|
12
|
+
]);
|
|
13
|
+
function pipelineNeedsBrowserSession(pipeline) {
|
|
14
|
+
return pipeline.some((step) => {
|
|
15
|
+
if (!step || typeof step !== 'object')
|
|
16
|
+
return false;
|
|
17
|
+
return Object.keys(step).some((op) => BROWSER_ONLY_STEPS.has(op));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function shouldUseBrowserSession(cmd) {
|
|
21
|
+
if (!cmd.browser)
|
|
22
|
+
return false;
|
|
23
|
+
if (cmd.func)
|
|
24
|
+
return true;
|
|
25
|
+
if (!cmd.pipeline || cmd.pipeline.length === 0)
|
|
26
|
+
return true;
|
|
27
|
+
if (cmd.strategy !== Strategy.PUBLIC)
|
|
28
|
+
return true;
|
|
29
|
+
return pipelineNeedsBrowserSession(cmd.pipeline);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Strategy } from './registry.js';
|
|
3
|
+
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
4
|
+
function makeCmd(partial) {
|
|
5
|
+
return {
|
|
6
|
+
site: 'test',
|
|
7
|
+
name: 'command',
|
|
8
|
+
description: '',
|
|
9
|
+
args: [],
|
|
10
|
+
...partial,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('shouldUseBrowserSession', () => {
|
|
14
|
+
it('skips browser session for public fetch-only pipelines', () => {
|
|
15
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
16
|
+
browser: true,
|
|
17
|
+
strategy: Strategy.PUBLIC,
|
|
18
|
+
pipeline: [{ fetch: 'https://example.com/api' }, { select: 'items' }],
|
|
19
|
+
}))).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('keeps browser session for public pipelines with browser-only steps', () => {
|
|
22
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
23
|
+
browser: true,
|
|
24
|
+
strategy: Strategy.PUBLIC,
|
|
25
|
+
pipeline: [{ navigate: 'https://example.com' }, { evaluate: '() => []' }],
|
|
26
|
+
}))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it('keeps browser session for non-public strategies', () => {
|
|
29
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
30
|
+
browser: true,
|
|
31
|
+
strategy: Strategy.COOKIE,
|
|
32
|
+
pipeline: [{ fetch: 'https://example.com/api' }],
|
|
33
|
+
}))).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('keeps browser session for function adapters', () => {
|
|
36
|
+
expect(shouldUseBrowserSession(makeCmd({
|
|
37
|
+
browser: true,
|
|
38
|
+
strategy: Strategy.PUBLIC,
|
|
39
|
+
func: async () => [],
|
|
40
|
+
}))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/dist/chaoxing.test.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { formatTimestamp, workStatusLabel } from './chaoxing.js';
|
|
3
|
+
function localDatePrefixFromMillis(ts) {
|
|
4
|
+
const d = new Date(ts);
|
|
5
|
+
const yyyy = d.getFullYear();
|
|
6
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
7
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
8
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
9
|
+
}
|
|
3
10
|
describe('formatTimestamp', () => {
|
|
4
11
|
it('formats millisecond timestamp', () => {
|
|
5
|
-
// 2026-01-15 08:30 UTC+8
|
|
6
12
|
const ts = new Date('2026-01-15T00:30:00Z').getTime();
|
|
7
13
|
const result = formatTimestamp(ts);
|
|
8
|
-
expect(result).toMatch(
|
|
14
|
+
expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(ts)}\\s`));
|
|
9
15
|
});
|
|
10
16
|
it('formats second timestamp', () => {
|
|
11
|
-
const
|
|
17
|
+
const millis = new Date('2026-06-01T12:00:00Z').getTime();
|
|
18
|
+
const ts = Math.floor(millis / 1000);
|
|
12
19
|
const result = formatTimestamp(ts);
|
|
13
|
-
expect(result).toMatch(
|
|
20
|
+
expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(millis)}\\s`));
|
|
14
21
|
});
|
|
15
22
|
it('returns empty for null/undefined/0', () => {
|
|
16
23
|
expect(formatTimestamp(null)).toBe('');
|