@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.
Files changed (190) hide show
  1. package/.github/workflows/build-extension.yml +21 -3
  2. package/.github/workflows/docs.yml +52 -0
  3. package/README.md +28 -28
  4. package/README.zh-CN.md +28 -28
  5. package/dist/browser/cdp.d.ts +16 -1
  6. package/dist/browser/cdp.js +124 -80
  7. package/dist/browser/daemon-client.d.ts +3 -1
  8. package/dist/browser/daemon-client.js +4 -0
  9. package/dist/browser/dom-helpers.d.ts +20 -0
  10. package/dist/browser/dom-helpers.js +109 -0
  11. package/dist/browser/mcp.d.ts +1 -0
  12. package/dist/browser/mcp.js +10 -5
  13. package/dist/browser/page.d.ts +7 -0
  14. package/dist/browser/page.js +37 -100
  15. package/dist/browser.test.js +7 -0
  16. package/dist/build-manifest.js +3 -1
  17. package/dist/build-manifest.test.js +34 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +30 -0
  20. package/dist/capabilityRouting.test.d.ts +1 -0
  21. package/dist/capabilityRouting.test.js +42 -0
  22. package/dist/chaoxing.test.js +11 -4
  23. package/dist/cli-manifest.json +635 -1
  24. package/dist/cli.js +45 -8
  25. package/dist/clis/antigravity/serve.d.ts +14 -0
  26. package/dist/clis/antigravity/serve.js +263 -0
  27. package/dist/clis/bilibili/download.js +4 -14
  28. package/dist/clis/boss/resume.d.ts +1 -0
  29. package/dist/clis/boss/resume.js +249 -0
  30. package/dist/clis/hf/top.d.ts +1 -0
  31. package/dist/clis/hf/top.js +119 -0
  32. package/dist/clis/jike/comment.d.ts +1 -0
  33. package/dist/clis/jike/comment.js +107 -0
  34. package/dist/clis/jike/create.d.ts +1 -0
  35. package/dist/clis/jike/create.js +106 -0
  36. package/dist/clis/jike/feed.d.ts +1 -0
  37. package/dist/clis/jike/feed.js +67 -0
  38. package/dist/clis/jike/like.d.ts +1 -0
  39. package/dist/clis/jike/like.js +61 -0
  40. package/dist/clis/jike/notifications.d.ts +1 -0
  41. package/dist/clis/jike/notifications.js +169 -0
  42. package/dist/clis/jike/post.yaml +58 -0
  43. package/dist/clis/jike/repost.d.ts +1 -0
  44. package/dist/clis/jike/repost.js +103 -0
  45. package/dist/clis/jike/search.d.ts +1 -0
  46. package/dist/clis/jike/search.js +67 -0
  47. package/dist/clis/jike/shared.d.ts +19 -0
  48. package/dist/clis/jike/shared.js +25 -0
  49. package/dist/clis/jike/topic.yaml +52 -0
  50. package/dist/clis/jike/user.yaml +51 -0
  51. package/dist/clis/smzdm/search.js +28 -39
  52. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  53. package/dist/clis/stackoverflow/hot.yaml +28 -0
  54. package/dist/clis/stackoverflow/search.yaml +32 -0
  55. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  56. package/dist/clis/twitter/download.js +6 -16
  57. package/dist/clis/xiaohongshu/download.js +3 -3
  58. package/dist/clis/zhihu/download.js +3 -3
  59. package/dist/doctor.d.ts +7 -0
  60. package/dist/doctor.js +16 -0
  61. package/dist/download/index.d.ts +12 -8
  62. package/dist/download/index.js +11 -3
  63. package/dist/download/index.test.d.ts +1 -0
  64. package/dist/download/index.test.js +14 -0
  65. package/dist/engine.js +5 -5
  66. package/dist/explore.d.ts +1 -0
  67. package/dist/explore.js +3 -3
  68. package/dist/generate.js +1 -0
  69. package/dist/interceptor.js +3 -2
  70. package/dist/output.d.ts +1 -0
  71. package/dist/output.js +3 -1
  72. package/dist/pipeline/executor.test.js +1 -0
  73. package/dist/pipeline/steps/download.js +14 -18
  74. package/dist/registry.d.ts +1 -0
  75. package/dist/registry.js +5 -2
  76. package/dist/runtime.d.ts +4 -1
  77. package/dist/runtime.js +2 -2
  78. package/dist/types.d.ts +12 -0
  79. package/dist/verify.d.ts +6 -1
  80. package/dist/verify.js +54 -2
  81. package/docs/.vitepress/config.mts +193 -0
  82. package/docs/adapters/browser/apple-podcasts.md +28 -0
  83. package/docs/adapters/browser/bbc.md +26 -0
  84. package/docs/adapters/browser/bilibili.md +38 -0
  85. package/docs/adapters/browser/boss.md +28 -0
  86. package/docs/adapters/browser/coupang.md +28 -0
  87. package/docs/adapters/browser/ctrip.md +27 -0
  88. package/docs/adapters/browser/github.md +26 -0
  89. package/docs/adapters/browser/hackernews.md +26 -0
  90. package/docs/adapters/browser/linkedin.md +27 -0
  91. package/docs/adapters/browser/reddit.md +41 -0
  92. package/docs/adapters/browser/reuters.md +27 -0
  93. package/docs/adapters/browser/smzdm.md +27 -0
  94. package/docs/adapters/browser/twitter.md +47 -0
  95. package/docs/adapters/browser/v2ex.md +32 -0
  96. package/docs/adapters/browser/weibo.md +27 -0
  97. package/docs/adapters/browser/xiaohongshu.md +32 -0
  98. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  99. package/docs/adapters/browser/xueqiu.md +32 -0
  100. package/docs/adapters/browser/yahoo-finance.md +26 -0
  101. package/docs/adapters/browser/youtube.md +29 -0
  102. package/docs/adapters/browser/zhihu.md +30 -0
  103. package/docs/adapters/desktop/antigravity.md +46 -0
  104. package/docs/adapters/desktop/chatgpt.md +43 -0
  105. package/docs/adapters/desktop/chatwise.md +38 -0
  106. package/docs/adapters/desktop/codex.md +32 -0
  107. package/docs/adapters/desktop/cursor.md +33 -0
  108. package/docs/adapters/desktop/discord.md +28 -0
  109. package/docs/adapters/desktop/feishu.md +20 -0
  110. package/docs/adapters/desktop/neteasemusic.md +31 -0
  111. package/docs/adapters/desktop/notion.md +29 -0
  112. package/docs/adapters/desktop/wechat.md +28 -0
  113. package/docs/adapters/index.md +49 -0
  114. package/docs/advanced/cdp.md +103 -0
  115. package/docs/advanced/download.md +63 -0
  116. package/docs/advanced/electron.md +125 -0
  117. package/docs/advanced/remote-chrome.md +72 -0
  118. package/docs/developer/ai-workflow.md +66 -0
  119. package/docs/developer/architecture.md +90 -0
  120. package/docs/developer/contributing.md +136 -0
  121. package/docs/developer/testing.md +237 -0
  122. package/docs/developer/ts-adapter.md +87 -0
  123. package/docs/developer/yaml-adapter.md +108 -0
  124. package/docs/guide/browser-bridge.md +38 -0
  125. package/docs/guide/getting-started.md +56 -0
  126. package/docs/guide/installation.md +37 -0
  127. package/docs/guide/troubleshooting.md +56 -0
  128. package/docs/index.md +35 -0
  129. package/docs/zh/adapters/index.md +5 -0
  130. package/docs/zh/advanced/cdp.md +3 -0
  131. package/docs/zh/developer/contributing.md +24 -0
  132. package/docs/zh/guide/browser-bridge.md +25 -0
  133. package/docs/zh/guide/getting-started.md +40 -0
  134. package/docs/zh/guide/installation.md +37 -0
  135. package/docs/zh/index.md +29 -0
  136. package/extension/dist/background.js +92 -52
  137. package/extension/package-lock.json +1156 -0
  138. package/extension/src/background.test.ts +151 -0
  139. package/extension/src/background.ts +122 -51
  140. package/extension/src/protocol.ts +3 -1
  141. package/package.json +7 -3
  142. package/src/browser/cdp.ts +154 -82
  143. package/src/browser/daemon-client.ts +7 -1
  144. package/src/browser/dom-helpers.ts +116 -0
  145. package/src/browser/mcp.ts +14 -6
  146. package/src/browser/page.ts +45 -100
  147. package/src/browser.test.ts +10 -0
  148. package/src/build-manifest.test.ts +36 -0
  149. package/src/build-manifest.ts +2 -1
  150. package/src/capabilityRouting.test.ts +47 -0
  151. package/src/capabilityRouting.ts +28 -0
  152. package/src/chaoxing.test.ts +12 -4
  153. package/src/cli.ts +28 -8
  154. package/src/clis/antigravity/serve.ts +329 -0
  155. package/src/clis/bilibili/download.ts +4 -15
  156. package/src/clis/boss/resume.ts +262 -0
  157. package/src/clis/hf/top.ts +141 -0
  158. package/src/clis/jike/comment.ts +113 -0
  159. package/src/clis/jike/create.ts +113 -0
  160. package/src/clis/jike/feed.ts +74 -0
  161. package/src/clis/jike/like.ts +65 -0
  162. package/src/clis/jike/notifications.ts +185 -0
  163. package/src/clis/jike/post.yaml +58 -0
  164. package/src/clis/jike/repost.ts +114 -0
  165. package/src/clis/jike/search.ts +74 -0
  166. package/src/clis/jike/shared.ts +36 -0
  167. package/src/clis/jike/topic.yaml +52 -0
  168. package/src/clis/jike/user.yaml +51 -0
  169. package/src/clis/smzdm/search.ts +30 -39
  170. package/src/clis/stackoverflow/bounties.yaml +29 -0
  171. package/src/clis/stackoverflow/hot.yaml +28 -0
  172. package/src/clis/stackoverflow/search.yaml +32 -0
  173. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  174. package/src/clis/twitter/download.ts +6 -17
  175. package/src/clis/xiaohongshu/download.ts +3 -3
  176. package/src/clis/zhihu/download.ts +3 -3
  177. package/src/doctor.ts +18 -2
  178. package/src/download/index.test.ts +16 -0
  179. package/src/download/index.ts +22 -4
  180. package/src/engine.ts +4 -4
  181. package/src/explore.ts +4 -4
  182. package/src/generate.ts +1 -0
  183. package/src/interceptor.ts +3 -2
  184. package/src/output.ts +3 -1
  185. package/src/pipeline/executor.test.ts +1 -0
  186. package/src/pipeline/steps/download.ts +14 -17
  187. package/src/registry.ts +6 -2
  188. package/src/runtime.ts +3 -2
  189. package/src/types.ts +9 -0
  190. 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]
@@ -1,6 +1,9 @@
1
1
  /**
2
- * 什么值得买搜索好价 — browser cookie, HTML parse.
3
- * Source: bb-sites/smzdm/search.js
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
- await page.goto('https://www.smzdm.com');
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
- (async () => {
25
- const q = '${q}';
30
+ (() => {
26
31
  const limit = ${limit};
27
- // Try youhui channel first, then home
28
- for (const channel of ['youhui', 'home']) {
29
- try {
30
- const resp = await fetch('https://search.smzdm.com/ajax/?c=' + channel + '&s=' + q + '&p=1&v=b', {
31
- credentials: 'include',
32
- headers: {'X-Requested-With': 'XMLHttpRequest'}
33
- });
34
- if (!resp.ok) continue;
35
- const html = await resp.text();
36
- if (html.indexOf('feed-row-wide') === -1) continue;
37
- const parser = new DOMParser();
38
- const doc = parser.parseFromString(html, 'text/html');
39
- const items = doc.querySelectorAll('li.feed-row-wide');
40
- const results = [];
41
- items.forEach((li, i) => {
42
- if (results.length >= limit) return;
43
- const titleEl = li.querySelector('h5.feed-block-title > a')
44
- || li.querySelector('h5 > a');
45
- if (!titleEl) return;
46
- const title = (titleEl.getAttribute('title') || titleEl.textContent || '').trim();
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 cookieString = await page.evaluate(`(() => document.cookie)()`);
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 (typeof cookieString === 'string' && cookieString) {
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: typeof cookieString === 'string' ? cookieString : '',
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: typeof cookieString === 'string' ? cookieString : '',
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
- detectContentType,
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.evaluate(`(() => document.cookie)()`);
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: typeof cookies === 'string' ? cookies : '',
137
+ cookies,
138
138
  timeout: 60000,
139
139
  onProgress: (received, total) => {
140
140
  if (progressBar) progressBar.update(received, total);