@jackwener/opencli 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-extension.yml +21 -3
- package/.github/workflows/docs.yml +52 -0
- package/README.md +28 -28
- package/README.zh-CN.md +28 -28
- package/dist/browser/cdp.d.ts +16 -1
- package/dist/browser/cdp.js +124 -80
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +37 -100
- package/dist/browser.test.js +7 -0
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.test.js +11 -4
- package/dist/cli-manifest.json +635 -1
- package/dist/cli.js +45 -8
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/xiaohongshu/download.js +3 -3
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +5 -5
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +3 -3
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +92 -52
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +122 -51
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +154 -82
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +45 -100
- package/src/browser.test.ts +10 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +12 -4
- package/src/cli.ts +28 -8
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/xiaohongshu/download.ts +3 -3
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +4 -4
- package/src/explore.ts +4 -4
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/output.ts +3 -1
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +6 -2
- package/src/runtime.ts +3 -2
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 什么值得买搜索好价 — browser cookie,
|
|
3
|
-
*
|
|
2
|
+
* 什么值得买搜索好价 — browser cookie, DOM scraping.
|
|
3
|
+
*
|
|
4
|
+
* Fix: The old adapter used `search.smzdm.com/ajax/` which returns 404.
|
|
5
|
+
* New approach: navigate to `search.smzdm.com/?c=home&s=<keyword>&v=b`
|
|
6
|
+
* and scrape the rendered DOM directly.
|
|
4
7
|
*/
|
|
5
8
|
import { cli, Strategy } from '../../registry.js';
|
|
6
9
|
cli({
|
|
@@ -17,46 +20,32 @@ cli({
|
|
|
17
20
|
func: async (page, kwargs) => {
|
|
18
21
|
const q = encodeURIComponent(kwargs.keyword);
|
|
19
22
|
const limit = kwargs.limit || 20;
|
|
20
|
-
|
|
23
|
+
// Navigate directly to search results page
|
|
24
|
+
await page.goto(`https://search.smzdm.com/?c=home&s=${q}&v=b`);
|
|
21
25
|
await page.wait(2);
|
|
22
26
|
const data = await page.evaluate(`
|
|
23
|
-
(
|
|
24
|
-
const q = '${q}';
|
|
27
|
+
(() => {
|
|
25
28
|
const limit = ${limit};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const url = titleEl.getAttribute('href') || '';
|
|
47
|
-
const priceEl = li.querySelector('.z-highlight');
|
|
48
|
-
const price = priceEl ? priceEl.textContent.trim() : '';
|
|
49
|
-
let mall = '';
|
|
50
|
-
const extrasSpan = li.querySelector('.z-feed-foot-r .feed-block-extras span');
|
|
51
|
-
if (extrasSpan) mall = extrasSpan.textContent.trim();
|
|
52
|
-
const commentEl = li.querySelector('.feed-btn-comment');
|
|
53
|
-
const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
|
|
54
|
-
results.push({rank: results.length + 1, title, price, mall, comments, url});
|
|
55
|
-
});
|
|
56
|
-
if (results.length > 0) return results;
|
|
57
|
-
} catch(e) { continue; }
|
|
58
|
-
}
|
|
59
|
-
return {error: 'No results'};
|
|
29
|
+
const items = document.querySelectorAll('li.feed-row-wide');
|
|
30
|
+
const results = [];
|
|
31
|
+
items.forEach((li) => {
|
|
32
|
+
if (results.length >= limit) return;
|
|
33
|
+
const titleEl = li.querySelector('h5.feed-block-title > a')
|
|
34
|
+
|| li.querySelector('h5 > a');
|
|
35
|
+
if (!titleEl) return;
|
|
36
|
+
const title = (titleEl.getAttribute('title') || titleEl.textContent || '').trim();
|
|
37
|
+
const url = titleEl.getAttribute('href') || titleEl.href || '';
|
|
38
|
+
const priceEl = li.querySelector('.z-highlight');
|
|
39
|
+
const price = priceEl ? priceEl.textContent.trim() : '';
|
|
40
|
+
let mall = '';
|
|
41
|
+
const mallEl = li.querySelector('.z-feed-foot-r .feed-block-extras span')
|
|
42
|
+
|| li.querySelector('.z-feed-foot-r span');
|
|
43
|
+
if (mallEl) mall = mallEl.textContent.trim();
|
|
44
|
+
const commentEl = li.querySelector('.feed-btn-comment');
|
|
45
|
+
const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
|
|
46
|
+
results.push({ rank: results.length + 1, title, price, mall, comments, url });
|
|
47
|
+
});
|
|
48
|
+
return results;
|
|
60
49
|
})()
|
|
61
50
|
`);
|
|
62
51
|
if (!Array.isArray(data))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
site: stackoverflow
|
|
2
|
+
name: bounties
|
|
3
|
+
description: Active bounties 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/featured?order=desc&sort=activity&site=stackoverflow
|
|
17
|
+
|
|
18
|
+
- select: items
|
|
19
|
+
|
|
20
|
+
- map:
|
|
21
|
+
title: "${{ item.title }}"
|
|
22
|
+
bounty: "${{ item.bounty_amount }}"
|
|
23
|
+
score: "${{ item.score }}"
|
|
24
|
+
answers: "${{ item.answer_count }}"
|
|
25
|
+
url: "${{ item.link }}"
|
|
26
|
+
|
|
27
|
+
- limit: ${{ args.limit }}
|
|
28
|
+
|
|
29
|
+
columns: [bounty, title, score, answers, url]
|
|
@@ -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)
|
|
@@ -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 { 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
|
@@ -101,12 +101,12 @@ async function discoverClisFromFs(dir) {
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
const promises = [];
|
|
104
|
-
const
|
|
105
|
-
for (const
|
|
106
|
-
|
|
107
|
-
const stat = await fs.promises.stat(siteDir);
|
|
108
|
-
if (!stat.isDirectory())
|
|
104
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory())
|
|
109
107
|
continue;
|
|
108
|
+
const site = entry.name;
|
|
109
|
+
const siteDir = path.join(dir, site);
|
|
110
110
|
const files = await fs.promises.readdir(siteDir);
|
|
111
111
|
for (const file of files) {
|
|
112
112
|
const filePath = path.join(siteDir, file);
|
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;
|
package/dist/explore.js
CHANGED
|
@@ -253,7 +253,7 @@ export async function exploreUrl(url, opts) {
|
|
|
253
253
|
const networkEntries = parseNetworkRequests(rawNetwork);
|
|
254
254
|
// Step 5: For JSON endpoints missing a body, carefully re-fetch in-browser via a pristine iframe
|
|
255
255
|
const jsonEndpoints = networkEntries.filter(e => e.contentType.includes('json') && e.method === 'GET' && e.status === 200 && !e.responseBody);
|
|
256
|
-
|
|
256
|
+
await Promise.allSettled(jsonEndpoints.slice(0, 5).map(async (ep) => {
|
|
257
257
|
try {
|
|
258
258
|
const body = await page.evaluate(`async () => {
|
|
259
259
|
let iframe = null;
|
|
@@ -282,7 +282,7 @@ export async function exploreUrl(url, opts) {
|
|
|
282
282
|
ep.responseBody = body;
|
|
283
283
|
}
|
|
284
284
|
catch { }
|
|
285
|
-
}
|
|
285
|
+
}));
|
|
286
286
|
// Step 6: Detect framework
|
|
287
287
|
let framework = {};
|
|
288
288
|
try {
|
|
@@ -417,7 +417,7 @@ export async function exploreUrl(url, opts) {
|
|
|
417
417
|
await Promise.all(writeTasks);
|
|
418
418
|
return { ...result, out_dir: targetDir };
|
|
419
419
|
})(), { timeout: exploreTimeout, label: `Explore ${url}` });
|
|
420
|
-
});
|
|
420
|
+
}, { workspace: opts.workspace });
|
|
421
421
|
}
|
|
422
422
|
export function renderExploreSummary(result) {
|
|
423
423
|
const lines = [
|
package/dist/generate.js
CHANGED
|
@@ -60,6 +60,7 @@ export async function generateCliFromUrl(opts) {
|
|
|
60
60
|
site: opts.site,
|
|
61
61
|
goal: normalizeGoal(opts.goal) ?? opts.goal,
|
|
62
62
|
waitSeconds: opts.waitSeconds ?? 3,
|
|
63
|
+
workspace: opts.workspace,
|
|
63
64
|
});
|
|
64
65
|
// Step 2: Synthesize candidates
|
|
65
66
|
const synthesizeResult = synthesizeFromExplore(exploreResult.out_dir, {
|
package/dist/interceptor.js
CHANGED
|
@@ -22,6 +22,7 @@ export function generateInterceptorJs(patternExpr, opts = {}) {
|
|
|
22
22
|
return `
|
|
23
23
|
() => {
|
|
24
24
|
window.${arr} = window.${arr} || [];
|
|
25
|
+
window.${arr}_errors = window.${arr}_errors || [];
|
|
25
26
|
const __pattern = ${patternExpr};
|
|
26
27
|
|
|
27
28
|
if (!window.${guard}) {
|
|
@@ -38,7 +39,7 @@ export function generateInterceptorJs(patternExpr, opts = {}) {
|
|
|
38
39
|
const clone = response.clone();
|
|
39
40
|
const json = await clone.json();
|
|
40
41
|
window.${arr}.push(json);
|
|
41
|
-
} catch(e) {}
|
|
42
|
+
} catch(e) { window.${arr}_errors.push({ url: reqUrl, error: String(e) }); }
|
|
42
43
|
}
|
|
43
44
|
return response;
|
|
44
45
|
};
|
|
@@ -56,7 +57,7 @@ export function generateInterceptorJs(patternExpr, opts = {}) {
|
|
|
56
57
|
this.addEventListener('load', function() {
|
|
57
58
|
try {
|
|
58
59
|
window.${arr}.push(JSON.parse(this.responseText));
|
|
59
|
-
} catch(e) {}
|
|
60
|
+
} catch(e) { window.${arr}_errors.push({ url: this.__opencli_url, error: String(e) }); }
|
|
60
61
|
});
|
|
61
62
|
}
|
|
62
63
|
return __origSend.apply(this, arguments);
|
package/dist/output.d.ts
CHANGED
package/dist/output.js
CHANGED
|
@@ -60,6 +60,8 @@ function renderTable(data, opts) {
|
|
|
60
60
|
footer.push(`${opts.elapsed.toFixed(1)}s`);
|
|
61
61
|
if (opts.source)
|
|
62
62
|
footer.push(opts.source);
|
|
63
|
+
if (opts.footerExtra)
|
|
64
|
+
footer.push(opts.footerExtra);
|
|
63
65
|
console.log(chalk.dim(footer.join(' · ')));
|
|
64
66
|
}
|
|
65
67
|
function renderJson(data) {
|
|
@@ -85,7 +87,7 @@ function renderCsv(data, opts) {
|
|
|
85
87
|
for (const row of rows) {
|
|
86
88
|
console.log(columns.map(c => {
|
|
87
89
|
const v = String(row[c] ?? '');
|
|
88
|
-
return v.includes(',') || v.includes('"') || v.includes('\n')
|
|
90
|
+
return v.includes(',') || v.includes('"') || v.includes('\n') || v.includes('\r')
|
|
89
91
|
? `"${v.replace(/"/g, '""')}"` : v;
|
|
90
92
|
}).join(','));
|
|
91
93
|
}
|