@jackwener/opencli 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-extension.yml +21 -3
- package/.github/workflows/docs.yml +52 -0
- package/README.md +28 -28
- package/README.zh-CN.md +28 -28
- package/dist/browser/cdp.d.ts +16 -1
- package/dist/browser/cdp.js +124 -80
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +37 -100
- package/dist/browser.test.js +7 -0
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.test.js +11 -4
- package/dist/cli-manifest.json +635 -1
- package/dist/cli.js +48 -8
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/xiaohongshu/download.js +3 -3
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +5 -5
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +3 -3
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +92 -52
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +122 -51
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +154 -82
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +45 -100
- package/src/browser.test.ts +10 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +12 -4
- package/src/cli.ts +30 -8
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/xiaohongshu/download.ts +3 -3
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +4 -4
- package/src/explore.ts +4 -4
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/output.ts +3 -1
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +6 -2
- package/src/runtime.ts +3 -2
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
site: jike
|
|
2
|
+
name: post
|
|
3
|
+
description: 即刻帖子详情及评论
|
|
4
|
+
domain: m.okjike.com
|
|
5
|
+
browser: true
|
|
6
|
+
|
|
7
|
+
args:
|
|
8
|
+
id:
|
|
9
|
+
type: string
|
|
10
|
+
required: true
|
|
11
|
+
description: Post ID (from post URL)
|
|
12
|
+
|
|
13
|
+
pipeline:
|
|
14
|
+
- navigate: https://m.okjike.com/originalPosts/${{ args.id }}
|
|
15
|
+
|
|
16
|
+
# 从 Next.js SSR 内嵌 JSON 中提取帖子和评论
|
|
17
|
+
- evaluate: |
|
|
18
|
+
(() => {
|
|
19
|
+
try {
|
|
20
|
+
const el = document.querySelector('script[type="application/json"]');
|
|
21
|
+
if (!el) return [];
|
|
22
|
+
const data = JSON.parse(el.textContent);
|
|
23
|
+
const pageProps = data?.props?.pageProps || {};
|
|
24
|
+
const post = pageProps.post || {};
|
|
25
|
+
const comments = pageProps.comments || [];
|
|
26
|
+
|
|
27
|
+
const result = [{
|
|
28
|
+
type: 'post',
|
|
29
|
+
author: post.user?.screenName || '',
|
|
30
|
+
content: post.content || '',
|
|
31
|
+
likes: post.likeCount || 0,
|
|
32
|
+
time: post.createdAt || '',
|
|
33
|
+
}];
|
|
34
|
+
|
|
35
|
+
for (const c of comments) {
|
|
36
|
+
result.push({
|
|
37
|
+
type: 'comment',
|
|
38
|
+
author: c.user?.screenName || '',
|
|
39
|
+
content: (c.content || '').replace(/\n/g, ' '),
|
|
40
|
+
likes: c.likeCount || 0,
|
|
41
|
+
time: c.createdAt || '',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
})()
|
|
50
|
+
|
|
51
|
+
- map:
|
|
52
|
+
type: ${{ item.type }}
|
|
53
|
+
author: ${{ item.author }}
|
|
54
|
+
content: ${{ item.content }}
|
|
55
|
+
likes: ${{ item.likes }}
|
|
56
|
+
time: ${{ item.time }}
|
|
57
|
+
|
|
58
|
+
columns: [type, author, content, likes, time]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 转发即刻帖子
|
|
5
|
+
*
|
|
6
|
+
* 操作栏转发按钮点击后弹出 Popover 菜单,
|
|
7
|
+
* 选择"转发动态"后弹出编辑器弹窗(可添加附言),
|
|
8
|
+
* 再点击"发布"确认转发。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
cli({
|
|
12
|
+
site: 'jike',
|
|
13
|
+
name: 'repost',
|
|
14
|
+
description: '转发即刻帖子',
|
|
15
|
+
domain: 'web.okjike.com',
|
|
16
|
+
strategy: Strategy.UI,
|
|
17
|
+
browser: true,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'id', type: 'string', required: true, help: '帖子 ID' },
|
|
20
|
+
{ name: 'text', type: 'string', required: false, help: '转发附言(可选)' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['status', 'message'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
|
|
25
|
+
await page.wait(5);
|
|
26
|
+
|
|
27
|
+
// 1. 点击操作栏中的转发按钮(第三个子元素)
|
|
28
|
+
const clickResult = await page.evaluate(`(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const actions = document.querySelector('[class*="_actions_"]');
|
|
31
|
+
if (!actions) return { ok: false, message: '未找到操作栏' };
|
|
32
|
+
const children = Array.from(actions.children).filter(c => c.offsetHeight > 0);
|
|
33
|
+
if (!children[2]) return { ok: false, message: '未找到转发按钮' };
|
|
34
|
+
// 注意:按位置定位,即刻操作栏顺序变化时需调整
|
|
35
|
+
children[2].click();
|
|
36
|
+
return { ok: true };
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return { ok: false, message: e.toString() };
|
|
39
|
+
}
|
|
40
|
+
})()`);
|
|
41
|
+
|
|
42
|
+
if (!clickResult.ok) {
|
|
43
|
+
return [{ status: 'failed', message: clickResult.message }];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await page.wait(1);
|
|
47
|
+
|
|
48
|
+
// 2. 在弹出菜单中点击"转发动态"
|
|
49
|
+
const menuResult = await page.evaluate(`(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const btn = Array.from(document.querySelectorAll('button')).find(
|
|
52
|
+
b => b.textContent?.trim() === '转发动态'
|
|
53
|
+
);
|
|
54
|
+
if (!btn) return { ok: false, message: '未找到"转发动态"菜单项' };
|
|
55
|
+
btn.click();
|
|
56
|
+
return { ok: true };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { ok: false, message: e.toString() };
|
|
59
|
+
}
|
|
60
|
+
})()`);
|
|
61
|
+
|
|
62
|
+
if (!menuResult.ok) {
|
|
63
|
+
return [{ status: 'failed', message: menuResult.message }];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await page.wait(2);
|
|
67
|
+
|
|
68
|
+
// 3. 若有附言,在弹窗编辑器中填入
|
|
69
|
+
if (kwargs.text) {
|
|
70
|
+
const textResult = await page.evaluate(`(async () => {
|
|
71
|
+
try {
|
|
72
|
+
const textToInsert = ${JSON.stringify(kwargs.text)};
|
|
73
|
+
const editor = document.querySelector('[contenteditable="true"]');
|
|
74
|
+
if (!editor) return { ok: false, message: '未找到附言输入框' };
|
|
75
|
+
editor.focus();
|
|
76
|
+
const dt = new DataTransfer();
|
|
77
|
+
dt.setData('text/plain', textToInsert);
|
|
78
|
+
editor.dispatchEvent(new ClipboardEvent('paste', {
|
|
79
|
+
clipboardData: dt, bubbles: true, cancelable: true,
|
|
80
|
+
}));
|
|
81
|
+
await new Promise(r => setTimeout(r, 500));
|
|
82
|
+
return { ok: true };
|
|
83
|
+
} catch(e) { return { ok: false, message: '附言写入失败: ' + e.toString() }; }
|
|
84
|
+
})()`);
|
|
85
|
+
if (!textResult.ok) {
|
|
86
|
+
return [{ status: 'failed', message: textResult.message }];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 4. 点击"发送"按钮确认转发
|
|
91
|
+
const confirmResult = await page.evaluate(`(async () => {
|
|
92
|
+
try {
|
|
93
|
+
await new Promise(r => setTimeout(r, 500));
|
|
94
|
+
const btn = Array.from(document.querySelectorAll('button')).find(b => {
|
|
95
|
+
const text = b.textContent?.trim() || '';
|
|
96
|
+
// 不匹配"转发动态",避免重复触发 Popover 菜单项
|
|
97
|
+
return (text === '发送' || text === '发布') && !b.disabled;
|
|
98
|
+
});
|
|
99
|
+
if (!btn) return { ok: false, message: '未找到发送按钮' };
|
|
100
|
+
btn.click();
|
|
101
|
+
return { ok: true, message: '转发成功' };
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return { ok: false, message: e.toString() };
|
|
104
|
+
}
|
|
105
|
+
})()`);
|
|
106
|
+
|
|
107
|
+
if (confirmResult.ok) await page.wait(3);
|
|
108
|
+
|
|
109
|
+
return [{
|
|
110
|
+
status: confirmResult.ok ? 'success' : 'failed',
|
|
111
|
+
message: confirmResult.message,
|
|
112
|
+
}];
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
import { JikePost, getPostDataJs } from './shared.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 即刻搜索适配器
|
|
6
|
+
*
|
|
7
|
+
* 策略:直接导航到 web.okjike.com 搜索页,
|
|
8
|
+
* 通过 React fiber 树提取帖子数据。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
cli({
|
|
12
|
+
site: 'jike',
|
|
13
|
+
name: 'search',
|
|
14
|
+
description: '搜索即刻帖子',
|
|
15
|
+
domain: 'web.okjike.com',
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'keyword', type: 'string', required: true },
|
|
20
|
+
{ name: 'limit', type: 'int', default: 20 },
|
|
21
|
+
],
|
|
22
|
+
columns: ['author', 'content', 'likes', 'comments', 'time', 'url'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
const keyword = kwargs.keyword as string;
|
|
25
|
+
const limit = (kwargs.limit as number) || 20;
|
|
26
|
+
|
|
27
|
+
// 1. 直接导航到搜索页
|
|
28
|
+
const encodedKeyword = encodeURIComponent(keyword);
|
|
29
|
+
await page.goto(`https://web.okjike.com/search?q=${encodedKeyword}`);
|
|
30
|
+
await page.wait(5);
|
|
31
|
+
|
|
32
|
+
// 2. 通过 React fiber 提取帖子数据
|
|
33
|
+
const extract = async (): Promise<JikePost[]> => {
|
|
34
|
+
return (await page.evaluate(`(() => {
|
|
35
|
+
${getPostDataJs}
|
|
36
|
+
|
|
37
|
+
const results = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const elements = document.querySelectorAll('[class*="_post_"], [class*="_postItem_"]');
|
|
40
|
+
|
|
41
|
+
for (const el of elements) {
|
|
42
|
+
const data = getPostData(el);
|
|
43
|
+
if (!data || !data.id || seen.has(data.id)) continue;
|
|
44
|
+
seen.add(data.id);
|
|
45
|
+
|
|
46
|
+
const author = data.user?.screenName || data.target?.user?.screenName || '';
|
|
47
|
+
const content = data.content || data.target?.content || '';
|
|
48
|
+
if (!author && !content) continue;
|
|
49
|
+
|
|
50
|
+
results.push({
|
|
51
|
+
author,
|
|
52
|
+
content: content.replace(/\\n/g, ' ').slice(0, 120),
|
|
53
|
+
likes: data.likeCount || 0,
|
|
54
|
+
comments: data.commentCount || 0,
|
|
55
|
+
time: data.actionTime || data.createdAt || '',
|
|
56
|
+
url: 'https://web.okjike.com/originalPost/' + data.id,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
})()`)) as JikePost[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let posts = await extract();
|
|
65
|
+
|
|
66
|
+
// 3. 数量不足时自动滚动加载更多
|
|
67
|
+
if (posts.length < limit) {
|
|
68
|
+
await page.autoScroll({ times: Math.ceil(limit / 10), delayMs: 2000 });
|
|
69
|
+
posts = await extract();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return posts.slice(0, limit);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 即刻适配器公共定义
|
|
3
|
+
*
|
|
4
|
+
* JikePost 接口和 getPostData 函数在 feed.ts / search.ts 中复用,
|
|
5
|
+
* 统一维护于此文件避免重复。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// 即刻帖子的通用字段
|
|
9
|
+
export interface JikePost {
|
|
10
|
+
author: string;
|
|
11
|
+
content: string;
|
|
12
|
+
likes: number;
|
|
13
|
+
comments: number;
|
|
14
|
+
time: string;
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 注入浏览器 evaluate 的 JS 函数字符串。
|
|
20
|
+
* 从 React fiber 树中向上最多走 10 层,找到含 id 字段的 props.data。
|
|
21
|
+
*/
|
|
22
|
+
export const getPostDataJs = `
|
|
23
|
+
function getPostData(element) {
|
|
24
|
+
for (const key of Object.keys(element)) {
|
|
25
|
+
if (key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')) {
|
|
26
|
+
let fiber = element[key];
|
|
27
|
+
for (let i = 0; i < 10 && fiber; i++) {
|
|
28
|
+
const props = fiber.memoizedProps || fiber.pendingProps;
|
|
29
|
+
if (props && props.data && props.data.id) return props.data;
|
|
30
|
+
fiber = fiber.return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
`.trim();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
site: jike
|
|
2
|
+
name: topic
|
|
3
|
+
description: 即刻话题/圈子帖子
|
|
4
|
+
domain: m.okjike.com
|
|
5
|
+
browser: true
|
|
6
|
+
|
|
7
|
+
args:
|
|
8
|
+
id:
|
|
9
|
+
type: string
|
|
10
|
+
required: true
|
|
11
|
+
description: Topic ID (from topic URL, e.g. 553870e8e4b0cafb0a1bef68)
|
|
12
|
+
limit:
|
|
13
|
+
type: int
|
|
14
|
+
default: 20
|
|
15
|
+
description: Number of posts
|
|
16
|
+
|
|
17
|
+
pipeline:
|
|
18
|
+
- navigate: https://m.okjike.com/topics/${{ args.id }}
|
|
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 pageProps = data?.props?.pageProps || {};
|
|
28
|
+
const posts = pageProps.posts || [];
|
|
29
|
+
return posts.map(p => ({
|
|
30
|
+
content: (p.content || '').replace(/\n/g, ' ').slice(0, 80),
|
|
31
|
+
author: p.user?.screenName || '',
|
|
32
|
+
likes: p.likeCount || 0,
|
|
33
|
+
comments: p.commentCount || 0,
|
|
34
|
+
time: p.actionTime || p.createdAt || '',
|
|
35
|
+
id: p.id || '',
|
|
36
|
+
}));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
})()
|
|
41
|
+
|
|
42
|
+
- map:
|
|
43
|
+
content: ${{ item.content }}
|
|
44
|
+
author: ${{ item.author }}
|
|
45
|
+
likes: ${{ item.likes }}
|
|
46
|
+
comments: ${{ item.comments }}
|
|
47
|
+
time: ${{ item.time }}
|
|
48
|
+
url: https://web.okjike.com/originalPost/${{ item.id }}
|
|
49
|
+
|
|
50
|
+
- limit: ${{ args.limit }}
|
|
51
|
+
|
|
52
|
+
columns: [content, author, likes, comments, time, url]
|
|
@@ -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);
|
|
@@ -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);
|