@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,51 @@
|
|
|
1
|
+
site: jike
|
|
2
|
+
name: user
|
|
3
|
+
description: 即刻用户动态
|
|
4
|
+
domain: m.okjike.com
|
|
5
|
+
browser: true
|
|
6
|
+
|
|
7
|
+
args:
|
|
8
|
+
username:
|
|
9
|
+
type: string
|
|
10
|
+
required: true
|
|
11
|
+
description: Username from profile URL (e.g. wenhao1996)
|
|
12
|
+
limit:
|
|
13
|
+
type: int
|
|
14
|
+
default: 20
|
|
15
|
+
description: Number of posts
|
|
16
|
+
|
|
17
|
+
pipeline:
|
|
18
|
+
- navigate: https://m.okjike.com/users/${{ args.username }}
|
|
19
|
+
|
|
20
|
+
# 从 Next.js SSR 内嵌 JSON 中提取用户动态
|
|
21
|
+
- evaluate: |
|
|
22
|
+
(() => {
|
|
23
|
+
try {
|
|
24
|
+
const el = document.querySelector('script[type="application/json"]');
|
|
25
|
+
if (!el) return [];
|
|
26
|
+
const data = JSON.parse(el.textContent);
|
|
27
|
+
const posts = data?.props?.pageProps?.posts || [];
|
|
28
|
+
return posts.map(p => ({
|
|
29
|
+
content: (p.content || '').replace(/\n/g, ' ').slice(0, 80),
|
|
30
|
+
type: p.type === 'ORIGINAL_POST' ? 'post' : p.type === 'REPOST' ? 'repost' : p.type || '',
|
|
31
|
+
likes: p.likeCount || 0,
|
|
32
|
+
comments: p.commentCount || 0,
|
|
33
|
+
time: p.actionTime || p.createdAt || '',
|
|
34
|
+
id: p.id || '',
|
|
35
|
+
}));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
40
|
+
|
|
41
|
+
- map:
|
|
42
|
+
content: ${{ item.content }}
|
|
43
|
+
type: ${{ item.type }}
|
|
44
|
+
likes: ${{ item.likes }}
|
|
45
|
+
comments: ${{ item.comments }}
|
|
46
|
+
time: ${{ item.time }}
|
|
47
|
+
url: https://web.okjike.com/originalPost/${{ item.id }}
|
|
48
|
+
|
|
49
|
+
- limit: ${{ args.limit }}
|
|
50
|
+
|
|
51
|
+
columns: [content, type, likes, comments, time, url]
|
package/src/clis/smzdm/search.ts
CHANGED
|
@@ -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
|
|
|
@@ -18,46 +21,34 @@ cli({
|
|
|
18
21
|
func: async (page, kwargs) => {
|
|
19
22
|
const q = encodeURIComponent(kwargs.keyword);
|
|
20
23
|
const limit = kwargs.limit || 20;
|
|
21
|
-
|
|
24
|
+
|
|
25
|
+
// Navigate directly to search results page
|
|
26
|
+
await page.goto(`https://search.smzdm.com/?c=home&s=${q}&v=b`);
|
|
22
27
|
await page.wait(2);
|
|
28
|
+
|
|
23
29
|
const data = await page.evaluate(`
|
|
24
|
-
(
|
|
25
|
-
const q = '${q}';
|
|
30
|
+
(() => {
|
|
26
31
|
const limit = ${limit};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const url = titleEl.getAttribute('href') || '';
|
|
48
|
-
const priceEl = li.querySelector('.z-highlight');
|
|
49
|
-
const price = priceEl ? priceEl.textContent.trim() : '';
|
|
50
|
-
let mall = '';
|
|
51
|
-
const extrasSpan = li.querySelector('.z-feed-foot-r .feed-block-extras span');
|
|
52
|
-
if (extrasSpan) mall = extrasSpan.textContent.trim();
|
|
53
|
-
const commentEl = li.querySelector('.feed-btn-comment');
|
|
54
|
-
const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
|
|
55
|
-
results.push({rank: results.length + 1, title, price, mall, comments, url});
|
|
56
|
-
});
|
|
57
|
-
if (results.length > 0) return results;
|
|
58
|
-
} catch(e) { continue; }
|
|
59
|
-
}
|
|
60
|
-
return {error: 'No results'};
|
|
32
|
+
const items = document.querySelectorAll('li.feed-row-wide');
|
|
33
|
+
const results = [];
|
|
34
|
+
items.forEach((li) => {
|
|
35
|
+
if (results.length >= limit) return;
|
|
36
|
+
const titleEl = li.querySelector('h5.feed-block-title > a')
|
|
37
|
+
|| li.querySelector('h5 > a');
|
|
38
|
+
if (!titleEl) return;
|
|
39
|
+
const title = (titleEl.getAttribute('title') || titleEl.textContent || '').trim();
|
|
40
|
+
const url = titleEl.getAttribute('href') || titleEl.href || '';
|
|
41
|
+
const priceEl = li.querySelector('.z-highlight');
|
|
42
|
+
const price = priceEl ? priceEl.textContent.trim() : '';
|
|
43
|
+
let mall = '';
|
|
44
|
+
const mallEl = li.querySelector('.z-feed-foot-r .feed-block-extras span')
|
|
45
|
+
|| li.querySelector('.z-feed-foot-r span');
|
|
46
|
+
if (mallEl) mall = mallEl.textContent.trim();
|
|
47
|
+
const commentEl = li.querySelector('.feed-btn-comment');
|
|
48
|
+
const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
|
|
49
|
+
results.push({ rank: results.length + 1, title, price, mall, comments, url });
|
|
50
|
+
});
|
|
51
|
+
return results;
|
|
61
52
|
})()
|
|
62
53
|
`);
|
|
63
54
|
if (!Array.isArray(data)) return [];
|
|
@@ -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]
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
sanitizeFilename,
|
|
17
17
|
getTempDir,
|
|
18
18
|
exportCookiesToNetscape,
|
|
19
|
+
formatCookieHeader,
|
|
19
20
|
} from '../../download/index.js';
|
|
20
21
|
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
21
22
|
|
|
@@ -109,7 +110,8 @@ cli({
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
// Extract cookies
|
|
112
|
-
const
|
|
113
|
+
const cookies = await page.getCookies({ domain: 'x.com' });
|
|
114
|
+
const cookieString = formatCookieHeader(cookies);
|
|
113
115
|
|
|
114
116
|
// Create output directory
|
|
115
117
|
const outputDir = tweetUrl
|
|
@@ -119,23 +121,10 @@ cli({
|
|
|
119
121
|
|
|
120
122
|
// Export cookies for yt-dlp
|
|
121
123
|
let cookiesFile: string | undefined;
|
|
122
|
-
if (
|
|
124
|
+
if (cookies.length > 0) {
|
|
123
125
|
const tempDir = getTempDir();
|
|
124
126
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
125
127
|
cookiesFile = path.join(tempDir, `twitter_cookies_${Date.now()}.txt`);
|
|
126
|
-
|
|
127
|
-
const cookies = cookieString.split(';').map((c) => {
|
|
128
|
-
const [name, ...rest] = c.trim().split('=');
|
|
129
|
-
return {
|
|
130
|
-
name: name || '',
|
|
131
|
-
value: rest.join('=') || '',
|
|
132
|
-
domain: '.x.com',
|
|
133
|
-
path: '/',
|
|
134
|
-
secure: true,
|
|
135
|
-
httpOnly: false,
|
|
136
|
-
};
|
|
137
|
-
}).filter((c) => c.name);
|
|
138
|
-
|
|
139
128
|
exportCookiesToNetscape(cookies, cookiesFile);
|
|
140
129
|
}
|
|
141
130
|
|
|
@@ -173,7 +162,7 @@ cli({
|
|
|
173
162
|
} else if (media.type === 'image') {
|
|
174
163
|
// Direct HTTP download for images
|
|
175
164
|
result = await httpDownload(media.url, destPath, {
|
|
176
|
-
cookies:
|
|
165
|
+
cookies: cookieString,
|
|
177
166
|
timeout: 30000,
|
|
178
167
|
onProgress: (received, total) => {
|
|
179
168
|
if (progressBar) progressBar.update(received, total);
|
|
@@ -182,7 +171,7 @@ cli({
|
|
|
182
171
|
} else {
|
|
183
172
|
// Direct HTTP download for direct video URLs
|
|
184
173
|
result = await httpDownload(media.url, destPath, {
|
|
185
|
-
cookies:
|
|
174
|
+
cookies: cookieString,
|
|
186
175
|
timeout: 60000,
|
|
187
176
|
onProgress: (received, total) => {
|
|
188
177
|
if (progressBar) progressBar.update(received, total);
|
package/src/clis/twitter/post.ts
CHANGED
|
@@ -26,8 +26,15 @@ cli({
|
|
|
26
26
|
const box = document.querySelector('[data-testid="tweetTextarea_0"]');
|
|
27
27
|
if (box) {
|
|
28
28
|
box.focus();
|
|
29
|
-
//
|
|
30
|
-
|
|
29
|
+
// Simulate a paste event to properly handle newlines in Draft.js/React
|
|
30
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
31
|
+
const dataTransfer = new DataTransfer();
|
|
32
|
+
dataTransfer.setData('text/plain', textToInsert);
|
|
33
|
+
box.dispatchEvent(new ClipboardEvent('paste', {
|
|
34
|
+
clipboardData: dataTransfer,
|
|
35
|
+
bubbles: true,
|
|
36
|
+
cancelable: true
|
|
37
|
+
}));
|
|
31
38
|
} else {
|
|
32
39
|
return { ok: false, message: 'Could not find the tweet composer text area.' };
|
|
33
40
|
}
|
|
@@ -24,46 +24,28 @@ cli({
|
|
|
24
24
|
// fetch will capture the SearchTimeline API call.
|
|
25
25
|
await page.installInterceptor('SearchTimeline');
|
|
26
26
|
|
|
27
|
-
// 3.
|
|
28
|
-
//
|
|
27
|
+
// 3. Trigger SPA navigation to search results via history API.
|
|
28
|
+
// pushState + popstate triggers React Router's listener without
|
|
29
|
+
// a full page reload, so the interceptor stays alive.
|
|
30
|
+
// Note: the previous approach (nativeSetter + Enter keydown on the
|
|
31
|
+
// search input) does not reliably trigger Twitter's form submission.
|
|
32
|
+
const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=top`);
|
|
29
33
|
await page.evaluate(`
|
|
30
34
|
(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
input.focus();
|
|
34
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
35
|
-
HTMLInputElement.prototype, 'value'
|
|
36
|
-
).set;
|
|
37
|
-
nativeSetter.call(input, ${JSON.stringify(query)});
|
|
38
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
39
|
-
})()
|
|
40
|
-
`);
|
|
41
|
-
await page.wait(0.5);
|
|
42
|
-
// Press Enter to submit
|
|
43
|
-
await page.evaluate(`
|
|
44
|
-
(() => {
|
|
45
|
-
const input = document.querySelector('input[data-testid="SearchBox_Search_Input"]');
|
|
46
|
-
if (!input) throw new Error('Search input not found');
|
|
47
|
-
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
35
|
+
window.history.pushState({}, '', ${searchUrl});
|
|
36
|
+
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
48
37
|
})()
|
|
49
38
|
`);
|
|
50
39
|
await page.wait(5);
|
|
51
40
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
(
|
|
56
|
-
|
|
57
|
-
for (const tab of tabs) {
|
|
58
|
-
if (tab.textContent.trim() === 'Top') { tab.click(); break; }
|
|
59
|
-
}
|
|
60
|
-
})()
|
|
61
|
-
`);
|
|
62
|
-
await page.wait(2);
|
|
63
|
-
} catch { /* ignore if tab not found */ }
|
|
41
|
+
// Verify SPA navigation succeeded
|
|
42
|
+
const currentPath = await page.evaluate('() => window.location.pathname');
|
|
43
|
+
if (!currentPath?.startsWith('/search')) {
|
|
44
|
+
throw new Error('SPA navigation to /search failed. Twitter may have changed its routing.');
|
|
45
|
+
}
|
|
64
46
|
|
|
65
|
-
//
|
|
66
|
-
await page.autoScroll({ times:
|
|
47
|
+
// 4. Scroll to trigger additional pagination
|
|
48
|
+
await page.autoScroll({ times: 3, delayMs: 2000 });
|
|
67
49
|
|
|
68
50
|
// 6. Retrieve captured data
|
|
69
51
|
const requests = await page.getInterceptedRequests();
|
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
|
8
8
|
import * as fs from 'node:fs';
|
|
@@ -11,7 +11,7 @@ import { cli, Strategy } from '../../registry.js';
|
|
|
11
11
|
import {
|
|
12
12
|
httpDownload,
|
|
13
13
|
sanitizeFilename,
|
|
14
|
-
|
|
14
|
+
formatCookieHeader,
|
|
15
15
|
} from '../../download/index.js';
|
|
16
16
|
import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
|
|
17
17
|
|
|
@@ -114,7 +114,7 @@ cli({
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// Extract cookies for authenticated downloads
|
|
117
|
-
const cookies = await page.
|
|
117
|
+
const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
|
|
118
118
|
|
|
119
119
|
// Create output directory
|
|
120
120
|
const outputDir = path.join(output, noteId);
|
|
@@ -134,7 +134,7 @@ cli({
|
|
|
134
134
|
|
|
135
135
|
try {
|
|
136
136
|
const result = await httpDownload(media.url, destPath, {
|
|
137
|
-
cookies
|
|
137
|
+
cookies,
|
|
138
138
|
timeout: 60000,
|
|
139
139
|
onProgress: (received, total) => {
|
|
140
140
|
if (progressBar) progressBar.update(received, total);
|
|
@@ -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 { sanitizeFilename, httpDownload } from '../../download/index.js';
|
|
11
|
+
import { sanitizeFilename, httpDownload, formatCookieHeader } from '../../download/index.js';
|
|
12
12
|
import { formatBytes } from '../../download/progress.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -178,7 +178,7 @@ cli({
|
|
|
178
178
|
const imagesDir = path.join(output, 'images');
|
|
179
179
|
fs.mkdirSync(imagesDir, { recursive: true });
|
|
180
180
|
|
|
181
|
-
const cookies = await page.
|
|
181
|
+
const cookies = formatCookieHeader(await page.getCookies({ domain: 'zhihu.com' }));
|
|
182
182
|
|
|
183
183
|
for (let i = 0; i < data.images.length; i++) {
|
|
184
184
|
const imgUrl = data.images[i];
|
|
@@ -188,7 +188,7 @@ cli({
|
|
|
188
188
|
|
|
189
189
|
try {
|
|
190
190
|
await httpDownload(imgUrl, imgPath, {
|
|
191
|
-
cookies
|
|
191
|
+
cookies,
|
|
192
192
|
timeout: 30000,
|
|
193
193
|
});
|
|
194
194
|
|
package/src/doctor.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
10
10
|
import { BrowserBridge } from './browser/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { listSessions } from './browser/daemon-client.js';
|
|
12
12
|
|
|
13
13
|
export type DoctorOptions = {
|
|
14
14
|
fix?: boolean;
|
|
15
15
|
yes?: boolean;
|
|
16
16
|
live?: boolean;
|
|
17
|
+
sessions?: boolean;
|
|
17
18
|
cliVersion?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -28,6 +29,7 @@ export type DoctorReport = {
|
|
|
28
29
|
daemonRunning: boolean;
|
|
29
30
|
extensionConnected: boolean;
|
|
30
31
|
connectivity?: ConnectivityResult;
|
|
32
|
+
sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
|
|
31
33
|
issues: string[];
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -55,6 +57,9 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
55
57
|
if (opts.live) {
|
|
56
58
|
connectivity = await checkConnectivity();
|
|
57
59
|
}
|
|
60
|
+
const sessions = opts.sessions && status.running && status.extensionConnected
|
|
61
|
+
? await listSessions() as Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>
|
|
62
|
+
: undefined;
|
|
58
63
|
|
|
59
64
|
const issues: string[] = [];
|
|
60
65
|
if (!status.running) {
|
|
@@ -78,6 +83,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
|
|
|
78
83
|
daemonRunning: status.running,
|
|
79
84
|
extensionConnected: status.extensionConnected,
|
|
80
85
|
connectivity,
|
|
86
|
+
sessions,
|
|
81
87
|
issues,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
@@ -104,6 +110,17 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
104
110
|
lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
if (report.sessions) {
|
|
114
|
+
lines.push('', chalk.bold('Sessions:'));
|
|
115
|
+
if (report.sessions.length === 0) {
|
|
116
|
+
lines.push(chalk.dim(' • no active automation sessions'));
|
|
117
|
+
} else {
|
|
118
|
+
for (const session of report.sessions) {
|
|
119
|
+
lines.push(chalk.dim(` • ${session.workspace} → window ${session.windowId}, tabs=${session.tabCount}, idle=${Math.ceil(session.idleMsRemaining / 1000)}s`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
if (report.issues.length) {
|
|
108
125
|
lines.push('', chalk.yellow('Issues:'));
|
|
109
126
|
for (const issue of report.issues) {
|
|
@@ -115,4 +132,3 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
|
|
|
115
132
|
|
|
116
133
|
return lines.join('\n');
|
|
117
134
|
}
|
|
118
|
-
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatCookieHeader, resolveRedirectUrl } from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('download helpers', () => {
|
|
5
|
+
it('resolves relative redirects against the original URL', () => {
|
|
6
|
+
expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
|
|
7
|
+
expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('formats browser cookies into a Cookie header', () => {
|
|
11
|
+
expect(formatCookieHeader([
|
|
12
|
+
{ name: 'sid', value: 'abc', domain: 'example.com' },
|
|
13
|
+
{ name: 'ct0', value: 'def', domain: 'example.com' },
|
|
14
|
+
])).toBe('sid=abc; ct0=def');
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/download/index.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface YtdlpOptions {
|
|
|
26
26
|
onProgress?: (percent: number) => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface BrowserCookie {
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
domain: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
secure?: boolean;
|
|
35
|
+
httpOnly?: boolean;
|
|
36
|
+
expirationDate?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
/**
|
|
30
40
|
* Check if yt-dlp is available in PATH.
|
|
31
41
|
*/
|
|
@@ -142,14 +152,14 @@ export async function httpDownload(
|
|
|
142
152
|
// Handle redirects
|
|
143
153
|
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
144
154
|
file.close();
|
|
145
|
-
fs.unlinkSync(tempPath);
|
|
146
|
-
httpDownload(response.headers.location, destPath, options).then(resolve);
|
|
155
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
156
|
+
httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
if (response.statusCode !== 200) {
|
|
151
161
|
file.close();
|
|
152
|
-
fs.unlinkSync(tempPath);
|
|
162
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
153
163
|
resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
|
|
154
164
|
return;
|
|
155
165
|
}
|
|
@@ -187,11 +197,15 @@ export async function httpDownload(
|
|
|
187
197
|
});
|
|
188
198
|
}
|
|
189
199
|
|
|
200
|
+
export function resolveRedirectUrl(currentUrl: string, location: string): string {
|
|
201
|
+
return new URL(location, currentUrl).toString();
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
/**
|
|
191
205
|
* Export cookies to Netscape format for yt-dlp.
|
|
192
206
|
*/
|
|
193
207
|
export function exportCookiesToNetscape(
|
|
194
|
-
cookies:
|
|
208
|
+
cookies: BrowserCookie[],
|
|
195
209
|
filePath: string,
|
|
196
210
|
): void {
|
|
197
211
|
const lines = [
|
|
@@ -214,6 +228,10 @@ export function exportCookiesToNetscape(
|
|
|
214
228
|
fs.writeFileSync(filePath, lines.join('\n'));
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
export function formatCookieHeader(cookies: BrowserCookie[]): string {
|
|
232
|
+
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
|
|
233
|
+
}
|
|
234
|
+
|
|
217
235
|
/**
|
|
218
236
|
* Download video using yt-dlp.
|
|
219
237
|
*/
|