@jackwener/opencli 1.0.1 → 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 +80 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/docs.yml +52 -0
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +42 -34
- package/README.zh-CN.md +42 -34
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +42 -0
- package/dist/browser/cdp.js +339 -0
- 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/index.d.ts +3 -0
- package/dist/browser/index.js +4 -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 +39 -123
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +49 -1
- 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.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +45 -0
- package/dist/cli-manifest.json +885 -48
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- 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/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- 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/twitter/post.js +9 -2
- package/dist/clis/twitter/search.js +14 -33
- package/dist/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +4 -4
- 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 +25 -14
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +48 -103
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/main.js +4 -193
- package/dist/output.d.ts +2 -1
- 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 +4 -3
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/synthesize.js +1 -1
- 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 +386 -438
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +124 -53
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +367 -0
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/index.ts +4 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +47 -124
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +56 -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 +53 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +205 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- 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/twitter/post.ts +9 -2
- package/src/clis/twitter/search.ts +15 -33
- package/src/clis/xiaohongshu/download.ts +4 -4
- 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 +20 -13
- package/src/explore.ts +54 -103
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/main.ts +4 -180
- package/src/output.ts +15 -13
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +9 -5
- package/src/runtime.ts +3 -2
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/synthesize.ts +1 -1
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
package/src/browser/page.ts
CHANGED
|
@@ -13,11 +13,23 @@
|
|
|
13
13
|
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
14
14
|
import type { IPage } from '../types.js';
|
|
15
15
|
import { sendCommand } from './daemon-client.js';
|
|
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';
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
28
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
19
29
|
*/
|
|
20
30
|
export class Page implements IPage {
|
|
31
|
+
constructor(private readonly workspace: string = 'default') {}
|
|
32
|
+
|
|
21
33
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
22
34
|
private _tabId: number | undefined;
|
|
23
35
|
|
|
@@ -26,9 +38,14 @@ export class Page implements IPage {
|
|
|
26
38
|
return this._tabId !== undefined ? { tabId: this._tabId } : {};
|
|
27
39
|
}
|
|
28
40
|
|
|
41
|
+
private _workspaceOpt(): { workspace: string } {
|
|
42
|
+
return { workspace: this.workspace };
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
async goto(url: string): Promise<void> {
|
|
30
46
|
const result = await sendCommand('navigate', {
|
|
31
47
|
url,
|
|
48
|
+
...this._workspaceOpt(),
|
|
32
49
|
...this._tabOpt(),
|
|
33
50
|
}) as { tabId?: number };
|
|
34
51
|
// Remember the tabId for subsequent exec calls
|
|
@@ -40,7 +57,7 @@ export class Page implements IPage {
|
|
|
40
57
|
/** Close the automation window in the extension */
|
|
41
58
|
async closeWindow(): Promise<void> {
|
|
42
59
|
try {
|
|
43
|
-
await sendCommand('close-window', {});
|
|
60
|
+
await sendCommand('close-window', { ...this._workspaceOpt() });
|
|
44
61
|
} catch {
|
|
45
62
|
// Window may already be closed or daemon may be down
|
|
46
63
|
}
|
|
@@ -48,7 +65,12 @@ export class Page implements IPage {
|
|
|
48
65
|
|
|
49
66
|
async evaluate(js: string): Promise<any> {
|
|
50
67
|
const code = wrapForEval(js);
|
|
51
|
-
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 : [];
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
|
|
@@ -80,57 +102,25 @@ export class Page implements IPage {
|
|
|
80
102
|
return buildTree(document.body, 0);
|
|
81
103
|
})()
|
|
82
104
|
`;
|
|
83
|
-
const raw = await sendCommand('exec', { code, ...this._tabOpt() });
|
|
105
|
+
const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
84
106
|
if (opts.raw) return raw;
|
|
85
107
|
if (typeof raw === 'string') return formatSnapshot(raw, opts);
|
|
86
108
|
return raw;
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
async click(ref: string): Promise<void> {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
(() => {
|
|
93
|
-
const ref = ${safeRef};
|
|
94
|
-
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
95
|
-
|| document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
|
|
96
|
-
if (!el) throw new Error('Element not found: ' + ref);
|
|
97
|
-
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
98
|
-
el.click();
|
|
99
|
-
return 'clicked';
|
|
100
|
-
})()
|
|
101
|
-
`;
|
|
102
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
112
|
+
const code = clickJs(ref);
|
|
113
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
async typeText(ref: string, text: string): Promise<void> {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const code = `
|
|
109
|
-
(() => {
|
|
110
|
-
const ref = ${safeRef};
|
|
111
|
-
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
112
|
-
|| document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
|
|
113
|
-
if (!el) throw new Error('Element not found: ' + ref);
|
|
114
|
-
el.focus();
|
|
115
|
-
el.value = ${safeText};
|
|
116
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
117
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
118
|
-
return 'typed';
|
|
119
|
-
})()
|
|
120
|
-
`;
|
|
121
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
117
|
+
const code = typeTextJs(ref, text);
|
|
118
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
async pressKey(key: string): Promise<void> {
|
|
125
|
-
const code =
|
|
126
|
-
|
|
127
|
-
const el = document.activeElement || document.body;
|
|
128
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
129
|
-
el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
130
|
-
return 'pressed';
|
|
131
|
-
})()
|
|
132
|
-
`;
|
|
133
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
122
|
+
const code = pressKeyJs(key);
|
|
123
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
134
124
|
}
|
|
135
125
|
|
|
136
126
|
async wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void> {
|
|
@@ -144,52 +134,30 @@ export class Page implements IPage {
|
|
|
144
134
|
}
|
|
145
135
|
if (options.text) {
|
|
146
136
|
const timeout = (options.timeout ?? 30) * 1000;
|
|
147
|
-
const code =
|
|
148
|
-
|
|
149
|
-
const deadline = Date.now() + ${timeout};
|
|
150
|
-
const check = () => {
|
|
151
|
-
if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
|
|
152
|
-
if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
|
|
153
|
-
setTimeout(check, 200);
|
|
154
|
-
};
|
|
155
|
-
check();
|
|
156
|
-
})
|
|
157
|
-
`;
|
|
158
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
137
|
+
const code = waitForTextJs(options.text, timeout);
|
|
138
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
159
139
|
}
|
|
160
140
|
}
|
|
161
141
|
|
|
162
142
|
async tabs(): Promise<any> {
|
|
163
|
-
return sendCommand('tabs', { op: 'list' });
|
|
143
|
+
return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
|
|
164
144
|
}
|
|
165
145
|
|
|
166
146
|
async closeTab(index?: number): Promise<void> {
|
|
167
|
-
await sendCommand('tabs', { op: 'close', ...(index !== undefined ? { index } : {}) });
|
|
147
|
+
await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
|
|
168
148
|
}
|
|
169
149
|
|
|
170
150
|
async newTab(): Promise<void> {
|
|
171
|
-
await sendCommand('tabs', { op: 'new' });
|
|
151
|
+
await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() });
|
|
172
152
|
}
|
|
173
153
|
|
|
174
154
|
async selectTab(index: number): Promise<void> {
|
|
175
|
-
await sendCommand('tabs', { op: 'select', index });
|
|
155
|
+
await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
|
|
176
156
|
}
|
|
177
157
|
|
|
178
158
|
async networkRequests(includeStatic: boolean = false): Promise<any> {
|
|
179
|
-
const code =
|
|
180
|
-
|
|
181
|
-
const entries = performance.getEntriesByType('resource');
|
|
182
|
-
return entries
|
|
183
|
-
${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
|
|
184
|
-
.map(e => ({
|
|
185
|
-
url: e.name,
|
|
186
|
-
type: e.initiatorType,
|
|
187
|
-
duration: Math.round(e.duration),
|
|
188
|
-
size: e.transferSize || 0,
|
|
189
|
-
}));
|
|
190
|
-
})()
|
|
191
|
-
`;
|
|
192
|
-
return sendCommand('exec', { code, ...this._tabOpt() });
|
|
159
|
+
const code = networkRequestsJs(includeStatic);
|
|
160
|
+
return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
193
161
|
}
|
|
194
162
|
|
|
195
163
|
/**
|
|
@@ -215,6 +183,7 @@ export class Page implements IPage {
|
|
|
215
183
|
path?: string;
|
|
216
184
|
} = {}): Promise<string> {
|
|
217
185
|
const base64 = await sendCommand('screenshot', {
|
|
186
|
+
...this._workspaceOpt(),
|
|
218
187
|
format: options.format,
|
|
219
188
|
quality: options.quality,
|
|
220
189
|
fullPage: options.fullPage,
|
|
@@ -225,46 +194,23 @@ export class Page implements IPage {
|
|
|
225
194
|
const fs = await import('node:fs');
|
|
226
195
|
const path = await import('node:path');
|
|
227
196
|
const dir = path.dirname(options.path);
|
|
228
|
-
fs.
|
|
229
|
-
fs.
|
|
197
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
198
|
+
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
|
|
230
199
|
}
|
|
231
200
|
|
|
232
201
|
return base64;
|
|
233
202
|
}
|
|
234
203
|
|
|
235
204
|
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
await sendCommand('exec', {
|
|
239
|
-
code: `window.scrollBy(${dx}, ${dy})`,
|
|
240
|
-
...this._tabOpt(),
|
|
241
|
-
});
|
|
205
|
+
const code = scrollJs(direction, amount);
|
|
206
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
242
207
|
}
|
|
243
208
|
|
|
244
209
|
async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
|
|
245
210
|
const times = options.times ?? 3;
|
|
246
211
|
const delayMs = options.delayMs ?? 2000;
|
|
247
|
-
const code =
|
|
248
|
-
|
|
249
|
-
for (let i = 0; i < ${times}; i++) {
|
|
250
|
-
const lastHeight = document.body.scrollHeight;
|
|
251
|
-
window.scrollTo(0, lastHeight);
|
|
252
|
-
await new Promise(resolve => {
|
|
253
|
-
let timeoutId;
|
|
254
|
-
const observer = new MutationObserver(() => {
|
|
255
|
-
if (document.body.scrollHeight > lastHeight) {
|
|
256
|
-
clearTimeout(timeoutId);
|
|
257
|
-
observer.disconnect();
|
|
258
|
-
setTimeout(resolve, 100);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
262
|
-
timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
})()
|
|
266
|
-
`;
|
|
267
|
-
await sendCommand('exec', { code, ...this._tabOpt() });
|
|
212
|
+
const code = autoScrollJs(times, delayMs);
|
|
213
|
+
await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
|
|
268
214
|
}
|
|
269
215
|
|
|
270
216
|
async installInterceptor(pattern: string): Promise<void> {
|
|
@@ -285,27 +231,4 @@ export class Page implements IPage {
|
|
|
285
231
|
}
|
|
286
232
|
}
|
|
287
233
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Wrap JS code for CDP Runtime.evaluate:
|
|
292
|
-
* - Already an IIFE `(...)()` → send as-is
|
|
293
|
-
* - Arrow/function literal → wrap as IIFE `(code)()`
|
|
294
|
-
* - `new Promise(...)` or raw expression → send as-is (expression)
|
|
295
|
-
*/
|
|
296
|
-
function wrapForEval(js: string): string {
|
|
297
|
-
const code = js.trim();
|
|
298
|
-
if (!code) return 'undefined';
|
|
299
|
-
|
|
300
|
-
// Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
|
|
301
|
-
if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code)) return code;
|
|
302
|
-
|
|
303
|
-
// Arrow function: `() => ...` or `async () => ...`
|
|
304
|
-
if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code)) return `(${code})()`;
|
|
305
|
-
|
|
306
|
-
// Function declaration: `function ...` or `async function ...`
|
|
307
|
-
if (/^(async\s+)?function[\s(]/.test(code)) return `(${code})()`;
|
|
308
|
-
|
|
309
|
-
// Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
|
|
310
|
-
return code;
|
|
311
|
-
}
|
|
234
|
+
// (End of file)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for browser operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wrap JS code for CDP Runtime.evaluate:
|
|
7
|
+
* - Already an IIFE `(...)()` → send as-is
|
|
8
|
+
* - Arrow/function literal → wrap as IIFE `(code)()`
|
|
9
|
+
* - `new Promise(...)` or raw expression → send as-is (expression)
|
|
10
|
+
*/
|
|
11
|
+
export function wrapForEval(js: string): string {
|
|
12
|
+
if (typeof js !== 'string') return 'undefined';
|
|
13
|
+
const code = js.trim();
|
|
14
|
+
if (!code) return 'undefined';
|
|
15
|
+
|
|
16
|
+
// Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
|
|
17
|
+
if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code)) return code;
|
|
18
|
+
|
|
19
|
+
// Arrow function: `() => ...` or `async () => ...`
|
|
20
|
+
if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code)) return `(${code})()`;
|
|
21
|
+
|
|
22
|
+
// Function declaration: `function ...` or `async function ...`
|
|
23
|
+
if (/^(async\s+)?function[\s(]/.test(code)) return `(${code})()`;
|
|
24
|
+
|
|
25
|
+
// Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
|
|
26
|
+
return code;
|
|
27
|
+
}
|
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', () => {
|
|
@@ -43,6 +44,52 @@ describe('browser helpers', () => {
|
|
|
43
44
|
it('times out slow promises', async () => {
|
|
44
45
|
await expect(__test__.withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
it('prefers the real Electron app target over DevTools and blank pages', () => {
|
|
49
|
+
const target = __test__.selectCDPTarget([
|
|
50
|
+
{
|
|
51
|
+
type: 'page',
|
|
52
|
+
title: 'DevTools - localhost:9224',
|
|
53
|
+
url: 'devtools://devtools/bundled/inspector.html',
|
|
54
|
+
webSocketDebuggerUrl: 'ws://127.0.0.1:9224/devtools',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'page',
|
|
58
|
+
title: '',
|
|
59
|
+
url: 'about:blank',
|
|
60
|
+
webSocketDebuggerUrl: 'ws://127.0.0.1:9224/blank',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'app',
|
|
64
|
+
title: 'Antigravity',
|
|
65
|
+
url: 'http://localhost:3000/',
|
|
66
|
+
webSocketDebuggerUrl: 'ws://127.0.0.1:9224/app',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9224/app');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('honors OPENCLI_CDP_TARGET when multiple inspectable targets exist', () => {
|
|
74
|
+
vi.stubEnv('OPENCLI_CDP_TARGET', 'codex');
|
|
75
|
+
|
|
76
|
+
const target = __test__.selectCDPTarget([
|
|
77
|
+
{
|
|
78
|
+
type: 'app',
|
|
79
|
+
title: 'Cursor',
|
|
80
|
+
url: 'http://localhost:3000/cursor',
|
|
81
|
+
webSocketDebuggerUrl: 'ws://127.0.0.1:9226/cursor',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'app',
|
|
85
|
+
title: 'OpenAI Codex',
|
|
86
|
+
url: 'http://localhost:3000/codex',
|
|
87
|
+
webSocketDebuggerUrl: 'ws://127.0.0.1:9226/codex',
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9226/codex');
|
|
92
|
+
});
|
|
46
93
|
});
|
|
47
94
|
|
|
48
95
|
describe('BrowserBridge state', () => {
|
|
@@ -76,4 +123,13 @@ describe('BrowserBridge state', () => {
|
|
|
76
123
|
|
|
77
124
|
await expect(mcp.connect()).rejects.toThrow('Session is closing');
|
|
78
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
|
+
});
|
|
79
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatTimestamp, workStatusLabel } from './chaoxing.js';
|
|
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
|
+
|
|
12
|
+
describe('formatTimestamp', () => {
|
|
13
|
+
it('formats millisecond timestamp', () => {
|
|
14
|
+
const ts = new Date('2026-01-15T00:30:00Z').getTime();
|
|
15
|
+
const result = formatTimestamp(ts);
|
|
16
|
+
expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(ts)}\\s`));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('formats second timestamp', () => {
|
|
20
|
+
const millis = new Date('2026-06-01T12:00:00Z').getTime();
|
|
21
|
+
const ts = Math.floor(millis / 1000);
|
|
22
|
+
const result = formatTimestamp(ts);
|
|
23
|
+
expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(millis)}\\s`));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns empty for null/undefined/0', () => {
|
|
27
|
+
expect(formatTimestamp(null)).toBe('');
|
|
28
|
+
expect(formatTimestamp(undefined)).toBe('');
|
|
29
|
+
expect(formatTimestamp(0)).toBe('');
|
|
30
|
+
expect(formatTimestamp('')).toBe('');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('passes through readable date strings', () => {
|
|
34
|
+
expect(formatTimestamp('2026-03-20 23:59')).toBe('2026-03-20 23:59');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('workStatusLabel', () => {
|
|
39
|
+
it('maps numeric status codes', () => {
|
|
40
|
+
expect(workStatusLabel(0)).toBe('未交');
|
|
41
|
+
expect(workStatusLabel(1)).toBe('已交');
|
|
42
|
+
expect(workStatusLabel(2)).toBe('已批阅');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('passes through string status', () => {
|
|
46
|
+
expect(workStatusLabel('已交')).toBe('已交');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns 未知 for empty/null', () => {
|
|
50
|
+
expect(workStatusLabel(null)).toBe('未知');
|
|
51
|
+
expect(workStatusLabel('')).toBe('未知');
|
|
52
|
+
});
|
|
53
|
+
});
|