@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
site: stackoverflow
|
|
2
|
+
name: hot
|
|
3
|
+
description: Hot Stack Overflow questions
|
|
4
|
+
domain: stackoverflow.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 10
|
|
12
|
+
description: Max number of results
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://api.stackexchange.com/2.3/questions?order=desc&sort=hot&site=stackoverflow
|
|
17
|
+
|
|
18
|
+
- select: items
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
title: "${{ item.title }}"
|
|
22
|
+
score: "${{ item.score }}"
|
|
23
|
+
answers: "${{ item.answer_count }}"
|
|
24
|
+
url: "${{ item.link }}"
|
|
25
|
+
|
|
26
|
+
- limit: ${{ args.limit }}
|
|
27
|
+
|
|
28
|
+
columns: [title, score, answers, url]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
site: stackoverflow
|
|
2
|
+
name: search
|
|
3
|
+
description: Search Stack Overflow questions
|
|
4
|
+
domain: stackoverflow.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
query:
|
|
10
|
+
type: string
|
|
11
|
+
required: true
|
|
12
|
+
description: Search query
|
|
13
|
+
limit:
|
|
14
|
+
type: int
|
|
15
|
+
default: 10
|
|
16
|
+
description: Max number of results
|
|
17
|
+
|
|
18
|
+
pipeline:
|
|
19
|
+
- fetch:
|
|
20
|
+
url: https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&q=${{ args.query }}&site=stackoverflow
|
|
21
|
+
|
|
22
|
+
- select: items
|
|
23
|
+
|
|
24
|
+
- map:
|
|
25
|
+
title: "${{ item.title }}"
|
|
26
|
+
score: "${{ item.score }}"
|
|
27
|
+
answers: "${{ item.answer_count }}"
|
|
28
|
+
url: "${{ item.link }}"
|
|
29
|
+
|
|
30
|
+
- limit: ${{ args.limit }}
|
|
31
|
+
|
|
32
|
+
columns: [title, score, answers, url]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
site: stackoverflow
|
|
2
|
+
name: unanswered
|
|
3
|
+
description: Top voted unanswered questions on Stack Overflow
|
|
4
|
+
domain: stackoverflow.com
|
|
5
|
+
strategy: public
|
|
6
|
+
browser: false
|
|
7
|
+
|
|
8
|
+
args:
|
|
9
|
+
limit:
|
|
10
|
+
type: int
|
|
11
|
+
default: 10
|
|
12
|
+
description: Max number of results
|
|
13
|
+
|
|
14
|
+
pipeline:
|
|
15
|
+
- fetch:
|
|
16
|
+
url: https://api.stackexchange.com/2.3/questions/unanswered?order=desc&sort=votes&site=stackoverflow
|
|
17
|
+
|
|
18
|
+
- select: items
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
title: "${{ item.title }}"
|
|
22
|
+
score: "${{ item.score }}"
|
|
23
|
+
answers: "${{ item.answer_count }}"
|
|
24
|
+
url: "${{ item.link }}"
|
|
25
|
+
|
|
26
|
+
- limit: ${{ args.limit }}
|
|
27
|
+
|
|
28
|
+
columns: [title, score, answers, url]
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { cli, Strategy } from '../../registry.js';
|
|
11
|
-
import { httpDownload, ytdlpDownload, checkYtdlp, getTempDir, exportCookiesToNetscape, } from '../../download/index.js';
|
|
11
|
+
import { httpDownload, ytdlpDownload, checkYtdlp, getTempDir, exportCookiesToNetscape, formatCookieHeader, } from '../../download/index.js';
|
|
12
12
|
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
13
13
|
cli({
|
|
14
14
|
site: 'twitter',
|
|
@@ -95,7 +95,8 @@ cli({
|
|
|
95
95
|
}];
|
|
96
96
|
}
|
|
97
97
|
// Extract cookies
|
|
98
|
-
const
|
|
98
|
+
const cookies = await page.getCookies({ domain: 'x.com' });
|
|
99
|
+
const cookieString = formatCookieHeader(cookies);
|
|
99
100
|
// Create output directory
|
|
100
101
|
const outputDir = tweetUrl
|
|
101
102
|
? path.join(output, 'tweets')
|
|
@@ -103,21 +104,10 @@ cli({
|
|
|
103
104
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
104
105
|
// Export cookies for yt-dlp
|
|
105
106
|
let cookiesFile;
|
|
106
|
-
if (
|
|
107
|
+
if (cookies.length > 0) {
|
|
107
108
|
const tempDir = getTempDir();
|
|
108
109
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
109
110
|
cookiesFile = path.join(tempDir, `twitter_cookies_${Date.now()}.txt`);
|
|
110
|
-
const cookies = cookieString.split(';').map((c) => {
|
|
111
|
-
const [name, ...rest] = c.trim().split('=');
|
|
112
|
-
return {
|
|
113
|
-
name: name || '',
|
|
114
|
-
value: rest.join('=') || '',
|
|
115
|
-
domain: '.x.com',
|
|
116
|
-
path: '/',
|
|
117
|
-
secure: true,
|
|
118
|
-
httpOnly: false,
|
|
119
|
-
};
|
|
120
|
-
}).filter((c) => c.name);
|
|
121
111
|
exportCookiesToNetscape(cookies, cookiesFile);
|
|
122
112
|
}
|
|
123
113
|
// Deduplicate media
|
|
@@ -152,7 +142,7 @@ cli({
|
|
|
152
142
|
else if (media.type === 'image') {
|
|
153
143
|
// Direct HTTP download for images
|
|
154
144
|
result = await httpDownload(media.url, destPath, {
|
|
155
|
-
cookies:
|
|
145
|
+
cookies: cookieString,
|
|
156
146
|
timeout: 30000,
|
|
157
147
|
onProgress: (received, total) => {
|
|
158
148
|
if (progressBar)
|
|
@@ -163,7 +153,7 @@ cli({
|
|
|
163
153
|
else {
|
|
164
154
|
// Direct HTTP download for direct video URLs
|
|
165
155
|
result = await httpDownload(media.url, destPath, {
|
|
166
|
-
cookies:
|
|
156
|
+
cookies: cookieString,
|
|
167
157
|
timeout: 60000,
|
|
168
158
|
onProgress: (received, total) => {
|
|
169
159
|
if (progressBar)
|
|
@@ -23,8 +23,15 @@ cli({
|
|
|
23
23
|
const box = document.querySelector('[data-testid="tweetTextarea_0"]');
|
|
24
24
|
if (box) {
|
|
25
25
|
box.focus();
|
|
26
|
-
//
|
|
27
|
-
|
|
26
|
+
// Simulate a paste event to properly handle newlines in Draft.js/React
|
|
27
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
28
|
+
const dataTransfer = new DataTransfer();
|
|
29
|
+
dataTransfer.setData('text/plain', textToInsert);
|
|
30
|
+
box.dispatchEvent(new ClipboardEvent('paste', {
|
|
31
|
+
clipboardData: dataTransfer,
|
|
32
|
+
bubbles: true,
|
|
33
|
+
cancelable: true
|
|
34
|
+
}));
|
|
28
35
|
} else {
|
|
29
36
|
return { ok: false, message: 'Could not find the tweet composer text area.' };
|
|
30
37
|
}
|
|
@@ -20,45 +20,26 @@ cli({
|
|
|
20
20
|
// SPA navigation preserves the JS context, so the monkey-patched
|
|
21
21
|
// fetch will capture the SearchTimeline API call.
|
|
22
22
|
await page.installInterceptor('SearchTimeline');
|
|
23
|
-
// 3.
|
|
24
|
-
//
|
|
23
|
+
// 3. Trigger SPA navigation to search results via history API.
|
|
24
|
+
// pushState + popstate triggers React Router's listener without
|
|
25
|
+
// a full page reload, so the interceptor stays alive.
|
|
26
|
+
// Note: the previous approach (nativeSetter + Enter keydown on the
|
|
27
|
+
// search input) does not reliably trigger Twitter's form submission.
|
|
28
|
+
const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=top`);
|
|
25
29
|
await page.evaluate(`
|
|
26
30
|
(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
input.focus();
|
|
30
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
31
|
-
HTMLInputElement.prototype, 'value'
|
|
32
|
-
).set;
|
|
33
|
-
nativeSetter.call(input, ${JSON.stringify(query)});
|
|
34
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
35
|
-
})()
|
|
36
|
-
`);
|
|
37
|
-
await page.wait(0.5);
|
|
38
|
-
// Press Enter to submit
|
|
39
|
-
await page.evaluate(`
|
|
40
|
-
(() => {
|
|
41
|
-
const input = document.querySelector('input[data-testid="SearchBox_Search_Input"]');
|
|
42
|
-
if (!input) throw new Error('Search input not found');
|
|
43
|
-
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
31
|
+
window.history.pushState({}, '', ${searchUrl});
|
|
32
|
+
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
44
33
|
})()
|
|
45
34
|
`);
|
|
46
35
|
await page.wait(5);
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const tabs = document.querySelectorAll('[role="tab"]');
|
|
52
|
-
for (const tab of tabs) {
|
|
53
|
-
if (tab.textContent.trim() === 'Top') { tab.click(); break; }
|
|
54
|
-
}
|
|
55
|
-
})()
|
|
56
|
-
`);
|
|
57
|
-
await page.wait(2);
|
|
36
|
+
// Verify SPA navigation succeeded
|
|
37
|
+
const currentPath = await page.evaluate('() => window.location.pathname');
|
|
38
|
+
if (!currentPath?.startsWith('/search')) {
|
|
39
|
+
throw new Error('SPA navigation to /search failed. Twitter may have changed its routing.');
|
|
58
40
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await page.autoScroll({ times: 2, delayMs: 2000 });
|
|
41
|
+
// 4. Scroll to trigger additional pagination
|
|
42
|
+
await page.autoScroll({ times: 3, delayMs: 2000 });
|
|
62
43
|
// 6. Retrieve captured data
|
|
63
44
|
const requests = await page.getInterceptedRequests();
|
|
64
45
|
if (!requests || requests.length === 0)
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Xiaohongshu download — download images and videos from a note.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* opencli xiaohongshu download --
|
|
5
|
+
* opencli xiaohongshu download --note_id abc123 --output ./xhs
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { cli, Strategy } from '../../registry.js';
|
|
10
|
-
import { httpDownload, } from '../../download/index.js';
|
|
10
|
+
import { httpDownload, formatCookieHeader, } from '../../download/index.js';
|
|
11
11
|
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
12
12
|
cli({
|
|
13
13
|
site: 'xiaohongshu',
|
|
@@ -104,7 +104,7 @@ cli({
|
|
|
104
104
|
return [{ index: 0, type: '-', status: 'failed', size: 'No media found' }];
|
|
105
105
|
}
|
|
106
106
|
// Extract cookies for authenticated downloads
|
|
107
|
-
const cookies = await page.
|
|
107
|
+
const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
|
|
108
108
|
// Create output directory
|
|
109
109
|
const outputDir = path.join(output, noteId);
|
|
110
110
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -119,7 +119,7 @@ cli({
|
|
|
119
119
|
const progressBar = tracker.onFileStart(filename, i);
|
|
120
120
|
try {
|
|
121
121
|
const result = await httpDownload(media.url, destPath, {
|
|
122
|
-
cookies
|
|
122
|
+
cookies,
|
|
123
123
|
timeout: 60000,
|
|
124
124
|
onProgress: (received, total) => {
|
|
125
125
|
if (progressBar)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { cli, Strategy } from '../../registry.js';
|
|
10
|
-
import { sanitizeFilename, httpDownload } from '../../download/index.js';
|
|
10
|
+
import { sanitizeFilename, httpDownload, formatCookieHeader } from '../../download/index.js';
|
|
11
11
|
import { formatBytes } from '../../download/progress.js';
|
|
12
12
|
/**
|
|
13
13
|
* Convert HTML content to Markdown.
|
|
@@ -150,7 +150,7 @@ cli({
|
|
|
150
150
|
if (downloadImages && data.images && data.images.length > 0) {
|
|
151
151
|
const imagesDir = path.join(output, 'images');
|
|
152
152
|
fs.mkdirSync(imagesDir, { recursive: true });
|
|
153
|
-
const cookies = await page.
|
|
153
|
+
const cookies = formatCookieHeader(await page.getCookies({ domain: 'zhihu.com' }));
|
|
154
154
|
for (let i = 0; i < data.images.length; i++) {
|
|
155
155
|
const imgUrl = data.images[i];
|
|
156
156
|
const ext = imgUrl.match(/\.(jpg|jpeg|png|gif|webp)/i)?.[1] || 'jpg';
|
|
@@ -158,7 +158,7 @@ cli({
|
|
|
158
158
|
const imgPath = path.join(imagesDir, imgFilename);
|
|
159
159
|
try {
|
|
160
160
|
await httpDownload(imgUrl, imgPath, {
|
|
161
|
-
cookies
|
|
161
|
+
cookies,
|
|
162
162
|
timeout: 30000,
|
|
163
163
|
});
|
|
164
164
|
// Replace image URL in markdown with local path
|
package/dist/doctor.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type DoctorOptions = {
|
|
|
8
8
|
fix?: boolean;
|
|
9
9
|
yes?: boolean;
|
|
10
10
|
live?: boolean;
|
|
11
|
+
sessions?: boolean;
|
|
11
12
|
cliVersion?: string;
|
|
12
13
|
};
|
|
13
14
|
export type ConnectivityResult = {
|
|
@@ -20,6 +21,12 @@ export type DoctorReport = {
|
|
|
20
21
|
daemonRunning: boolean;
|
|
21
22
|
extensionConnected: boolean;
|
|
22
23
|
connectivity?: ConnectivityResult;
|
|
24
|
+
sessions?: Array<{
|
|
25
|
+
workspace: string;
|
|
26
|
+
windowId: number;
|
|
27
|
+
tabCount: number;
|
|
28
|
+
idleMsRemaining: number;
|
|
29
|
+
}>;
|
|
23
30
|
issues: string[];
|
|
24
31
|
};
|
|
25
32
|
/**
|
package/dist/doctor.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
9
9
|
import { BrowserBridge } from './browser/index.js';
|
|
10
|
+
import { listSessions } from './browser/daemon-client.js';
|
|
10
11
|
/**
|
|
11
12
|
* Test connectivity by attempting a real browser command.
|
|
12
13
|
*/
|
|
@@ -30,6 +31,9 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
30
31
|
if (opts.live) {
|
|
31
32
|
connectivity = await checkConnectivity();
|
|
32
33
|
}
|
|
34
|
+
const sessions = opts.sessions && status.running && status.extensionConnected
|
|
35
|
+
? await listSessions()
|
|
36
|
+
: undefined;
|
|
33
37
|
const issues = [];
|
|
34
38
|
if (!status.running) {
|
|
35
39
|
issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
|
|
@@ -49,6 +53,7 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
49
53
|
daemonRunning: status.running,
|
|
50
54
|
extensionConnected: status.extensionConnected,
|
|
51
55
|
connectivity,
|
|
56
|
+
sessions,
|
|
52
57
|
issues,
|
|
53
58
|
};
|
|
54
59
|
}
|
|
@@ -71,6 +76,17 @@ export function renderBrowserDoctorReport(report) {
|
|
|
71
76
|
else {
|
|
72
77
|
lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
|
|
73
78
|
}
|
|
79
|
+
if (report.sessions) {
|
|
80
|
+
lines.push('', chalk.bold('Sessions:'));
|
|
81
|
+
if (report.sessions.length === 0) {
|
|
82
|
+
lines.push(chalk.dim(' • no active automation sessions'));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const session of report.sessions) {
|
|
86
|
+
lines.push(chalk.dim(` • ${session.workspace} → window ${session.windowId}, tabs=${session.tabCount}, idle=${Math.ceil(session.idleMsRemaining / 1000)}s`));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
74
90
|
if (report.issues.length) {
|
|
75
91
|
lines.push('', chalk.yellow('Issues:'));
|
|
76
92
|
for (const issue of report.issues) {
|
package/dist/download/index.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export interface YtdlpOptions {
|
|
|
14
14
|
extraArgs?: string[];
|
|
15
15
|
onProgress?: (percent: number) => void;
|
|
16
16
|
}
|
|
17
|
+
export interface BrowserCookie {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
secure?: boolean;
|
|
23
|
+
httpOnly?: boolean;
|
|
24
|
+
expirationDate?: number;
|
|
25
|
+
}
|
|
17
26
|
/**
|
|
18
27
|
* Check if yt-dlp is available in PATH.
|
|
19
28
|
*/
|
|
@@ -38,17 +47,12 @@ export declare function httpDownload(url: string, destPath: string, options?: Do
|
|
|
38
47
|
size: number;
|
|
39
48
|
error?: string;
|
|
40
49
|
}>;
|
|
50
|
+
export declare function resolveRedirectUrl(currentUrl: string, location: string): string;
|
|
41
51
|
/**
|
|
42
52
|
* Export cookies to Netscape format for yt-dlp.
|
|
43
53
|
*/
|
|
44
|
-
export declare function exportCookiesToNetscape(cookies:
|
|
45
|
-
|
|
46
|
-
value: string;
|
|
47
|
-
domain: string;
|
|
48
|
-
path?: string;
|
|
49
|
-
secure?: boolean;
|
|
50
|
-
httpOnly?: boolean;
|
|
51
|
-
}>, filePath: string): void;
|
|
54
|
+
export declare function exportCookiesToNetscape(cookies: BrowserCookie[], filePath: string): void;
|
|
55
|
+
export declare function formatCookieHeader(cookies: BrowserCookie[]): string;
|
|
52
56
|
/**
|
|
53
57
|
* Download video using yt-dlp.
|
|
54
58
|
*/
|
package/dist/download/index.js
CHANGED
|
@@ -107,13 +107,15 @@ export async function httpDownload(url, destPath, options = {}) {
|
|
|
107
107
|
// Handle redirects
|
|
108
108
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
109
109
|
file.close();
|
|
110
|
-
fs.
|
|
111
|
-
|
|
110
|
+
if (fs.existsSync(tempPath))
|
|
111
|
+
fs.unlinkSync(tempPath);
|
|
112
|
+
httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
if (response.statusCode !== 200) {
|
|
115
116
|
file.close();
|
|
116
|
-
fs.
|
|
117
|
+
if (fs.existsSync(tempPath))
|
|
118
|
+
fs.unlinkSync(tempPath);
|
|
117
119
|
resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
@@ -147,6 +149,9 @@ export async function httpDownload(url, destPath, options = {}) {
|
|
|
147
149
|
});
|
|
148
150
|
});
|
|
149
151
|
}
|
|
152
|
+
export function resolveRedirectUrl(currentUrl, location) {
|
|
153
|
+
return new URL(location, currentUrl).toString();
|
|
154
|
+
}
|
|
150
155
|
/**
|
|
151
156
|
* Export cookies to Netscape format for yt-dlp.
|
|
152
157
|
*/
|
|
@@ -168,6 +173,9 @@ export function exportCookiesToNetscape(cookies, filePath) {
|
|
|
168
173
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
169
174
|
fs.writeFileSync(filePath, lines.join('\n'));
|
|
170
175
|
}
|
|
176
|
+
export function formatCookieHeader(cookies) {
|
|
177
|
+
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
|
|
178
|
+
}
|
|
171
179
|
/**
|
|
172
180
|
* Download video using yt-dlp.
|
|
173
181
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatCookieHeader, resolveRedirectUrl } from './index.js';
|
|
3
|
+
describe('download helpers', () => {
|
|
4
|
+
it('resolves relative redirects against the original URL', () => {
|
|
5
|
+
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
6
|
+
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
7
|
+
});
|
|
8
|
+
it('formats browser cookies into a Cookie header', () => {
|
|
9
|
+
expect(formatCookieHeader([
|
|
10
|
+
{ name: 'sid', value: 'abc', domain: 'example.com' },
|
|
11
|
+
{ name: 'ct0', value: 'def', domain: 'example.com' },
|
|
12
|
+
])).toBe('sid=abc; ct0=def');
|
|
13
|
+
});
|
|
14
|
+
});
|
package/dist/engine.js
CHANGED
|
@@ -24,12 +24,15 @@ export async function discoverClis(...dirs) {
|
|
|
24
24
|
// Fast path: try manifest first (production / post-build)
|
|
25
25
|
for (const dir of dirs) {
|
|
26
26
|
const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
try {
|
|
28
|
+
await fs.promises.access(manifestPath);
|
|
29
|
+
await loadFromManifest(manifestPath, dir);
|
|
29
30
|
continue; // Skip filesystem scan for this directory
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
catch {
|
|
33
|
+
// Fallback: runtime filesystem scan (development)
|
|
34
|
+
await discoverClisFromFs(dir);
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
/**
|
|
@@ -37,9 +40,10 @@ export async function discoverClis(...dirs) {
|
|
|
37
40
|
* YAML pipelines are inlined — zero YAML parsing at runtime.
|
|
38
41
|
* TS modules are deferred — loaded lazily on first execution.
|
|
39
42
|
*/
|
|
40
|
-
function loadFromManifest(manifestPath, clisDir) {
|
|
43
|
+
async function loadFromManifest(manifestPath, clisDir) {
|
|
41
44
|
try {
|
|
42
|
-
const
|
|
45
|
+
const raw = await fs.promises.readFile(manifestPath, 'utf-8');
|
|
46
|
+
const manifest = JSON.parse(raw);
|
|
43
47
|
for (const entry of manifest) {
|
|
44
48
|
if (entry.type === 'yaml') {
|
|
45
49
|
// YAML pipelines fully inlined in manifest — register directly
|
|
@@ -90,17 +94,24 @@ function loadFromManifest(manifestPath, clisDir) {
|
|
|
90
94
|
* Fallback: traditional filesystem scan (used during development with tsx).
|
|
91
95
|
*/
|
|
92
96
|
async function discoverClisFromFs(dir) {
|
|
93
|
-
|
|
97
|
+
try {
|
|
98
|
+
await fs.promises.access(dir);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
94
101
|
return;
|
|
102
|
+
}
|
|
95
103
|
const promises = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!
|
|
104
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory())
|
|
99
107
|
continue;
|
|
100
|
-
|
|
108
|
+
const site = entry.name;
|
|
109
|
+
const siteDir = path.join(dir, site);
|
|
110
|
+
const files = await fs.promises.readdir(siteDir);
|
|
111
|
+
for (const file of files) {
|
|
101
112
|
const filePath = path.join(siteDir, file);
|
|
102
113
|
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
103
|
-
registerYamlCli(filePath, site);
|
|
114
|
+
promises.push(registerYamlCli(filePath, site));
|
|
104
115
|
}
|
|
105
116
|
else if ((file.endsWith('.js') && !file.endsWith('.d.js')) ||
|
|
106
117
|
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))) {
|
|
@@ -112,9 +123,9 @@ async function discoverClisFromFs(dir) {
|
|
|
112
123
|
}
|
|
113
124
|
await Promise.all(promises);
|
|
114
125
|
}
|
|
115
|
-
function registerYamlCli(filePath, defaultSite) {
|
|
126
|
+
async function registerYamlCli(filePath, defaultSite) {
|
|
116
127
|
try {
|
|
117
|
-
const raw = fs.
|
|
128
|
+
const raw = await fs.promises.readFile(filePath, 'utf-8');
|
|
118
129
|
const def = yaml.load(raw);
|
|
119
130
|
if (!def || typeof def !== 'object')
|
|
120
131
|
return;
|
package/dist/explore.d.ts
CHANGED
|
@@ -23,5 +23,6 @@ export declare function exploreUrl(url: string, opts: {
|
|
|
23
23
|
query?: string;
|
|
24
24
|
clickLabels?: string[];
|
|
25
25
|
auto?: boolean;
|
|
26
|
+
workspace?: string;
|
|
26
27
|
}): Promise<Record<string, any>>;
|
|
27
28
|
export declare function renderExploreSummary(result: Record<string, any>): string;
|