@jackwener/opencli 0.9.6 → 0.9.8

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 (221) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  4. package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
  5. package/.github/dependabot.yml +27 -0
  6. package/.github/pull_request_template.md +24 -0
  7. package/.github/workflows/ci.yml +14 -8
  8. package/.github/workflows/e2e-headed.yml +6 -2
  9. package/.github/workflows/pkg-pr-new.yml +2 -2
  10. package/.github/workflows/release-please.yml +25 -0
  11. package/.github/workflows/release.yml +2 -2
  12. package/.github/workflows/security.yml +36 -0
  13. package/CLI-ELECTRON.md +89 -36
  14. package/CONTRIBUTING.md +167 -0
  15. package/README.md +98 -32
  16. package/README.zh-CN.md +99 -33
  17. package/dist/browser/discover.js +22 -7
  18. package/dist/browser.test.js +23 -0
  19. package/dist/build-manifest.d.ts +26 -0
  20. package/dist/build-manifest.js +132 -60
  21. package/dist/build-manifest.test.d.ts +1 -0
  22. package/dist/build-manifest.test.js +26 -0
  23. package/dist/cli-manifest.json +1415 -29
  24. package/dist/clis/bilibili/download.d.ts +10 -0
  25. package/dist/clis/bilibili/download.js +135 -0
  26. package/dist/clis/chatwise/ask.d.ts +1 -0
  27. package/dist/clis/chatwise/ask.js +76 -0
  28. package/dist/clis/chatwise/export.d.ts +1 -0
  29. package/dist/clis/chatwise/export.js +46 -0
  30. package/dist/clis/chatwise/history.d.ts +1 -0
  31. package/dist/clis/chatwise/history.js +43 -0
  32. package/dist/clis/chatwise/model.d.ts +1 -0
  33. package/dist/clis/chatwise/model.js +81 -0
  34. package/dist/clis/chatwise/new.d.ts +1 -0
  35. package/dist/clis/chatwise/new.js +18 -0
  36. package/dist/clis/chatwise/read.d.ts +1 -0
  37. package/dist/clis/chatwise/read.js +39 -0
  38. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  39. package/dist/clis/chatwise/screenshot.js +27 -0
  40. package/dist/clis/chatwise/send.d.ts +1 -0
  41. package/dist/clis/chatwise/send.js +45 -0
  42. package/dist/clis/chatwise/status.d.ts +1 -0
  43. package/dist/clis/chatwise/status.js +22 -0
  44. package/dist/clis/discord-app/channels.d.ts +1 -0
  45. package/dist/clis/discord-app/channels.js +45 -0
  46. package/dist/clis/discord-app/members.d.ts +1 -0
  47. package/dist/clis/discord-app/members.js +38 -0
  48. package/dist/clis/discord-app/read.d.ts +1 -0
  49. package/dist/clis/discord-app/read.js +45 -0
  50. package/dist/clis/discord-app/search.d.ts +1 -0
  51. package/dist/clis/discord-app/search.js +56 -0
  52. package/dist/clis/discord-app/send.d.ts +1 -0
  53. package/dist/clis/discord-app/send.js +27 -0
  54. package/dist/clis/discord-app/servers.d.ts +1 -0
  55. package/dist/clis/discord-app/servers.js +36 -0
  56. package/dist/clis/discord-app/status.d.ts +1 -0
  57. package/dist/clis/discord-app/status.js +16 -0
  58. package/dist/clis/feishu/new.d.ts +1 -0
  59. package/dist/clis/feishu/new.js +27 -0
  60. package/dist/clis/feishu/read.d.ts +1 -0
  61. package/dist/clis/feishu/read.js +40 -0
  62. package/dist/clis/feishu/search.d.ts +1 -0
  63. package/dist/clis/feishu/search.js +30 -0
  64. package/dist/clis/feishu/send.d.ts +1 -0
  65. package/dist/clis/feishu/send.js +39 -0
  66. package/dist/clis/feishu/status.d.ts +1 -0
  67. package/dist/clis/feishu/status.js +28 -0
  68. package/dist/clis/grok/ask.d.ts +1 -0
  69. package/dist/clis/grok/ask.js +82 -0
  70. package/dist/clis/grok/debug.d.ts +1 -0
  71. package/dist/clis/grok/debug.js +45 -0
  72. package/dist/clis/jimeng/generate.yaml +84 -0
  73. package/dist/clis/jimeng/history.yaml +47 -0
  74. package/dist/clis/linux-do/categories.yaml +41 -0
  75. package/dist/clis/linux-do/category.yaml +49 -0
  76. package/dist/clis/linux-do/hot.yaml +50 -0
  77. package/dist/clis/linux-do/latest.yaml +40 -0
  78. package/dist/clis/linux-do/search.yaml +45 -0
  79. package/dist/clis/linux-do/topic.yaml +38 -0
  80. package/dist/clis/notion/export.d.ts +1 -0
  81. package/dist/clis/notion/export.js +31 -0
  82. package/dist/clis/notion/favorites.d.ts +1 -0
  83. package/dist/clis/notion/favorites.js +84 -0
  84. package/dist/clis/notion/new.d.ts +1 -0
  85. package/dist/clis/notion/new.js +34 -0
  86. package/dist/clis/notion/read.d.ts +1 -0
  87. package/dist/clis/notion/read.js +30 -0
  88. package/dist/clis/notion/search.d.ts +1 -0
  89. package/dist/clis/notion/search.js +46 -0
  90. package/dist/clis/notion/sidebar.d.ts +1 -0
  91. package/dist/clis/notion/sidebar.js +41 -0
  92. package/dist/clis/notion/status.d.ts +1 -0
  93. package/dist/clis/notion/status.js +16 -0
  94. package/dist/clis/notion/write.d.ts +1 -0
  95. package/dist/clis/notion/write.js +40 -0
  96. package/dist/clis/twitter/download.d.ts +8 -0
  97. package/dist/clis/twitter/download.js +204 -0
  98. package/dist/clis/wechat/chats.d.ts +1 -0
  99. package/dist/clis/wechat/chats.js +28 -0
  100. package/dist/clis/wechat/contacts.d.ts +1 -0
  101. package/dist/clis/wechat/contacts.js +28 -0
  102. package/dist/clis/wechat/read.d.ts +1 -0
  103. package/dist/clis/wechat/read.js +58 -0
  104. package/dist/clis/wechat/search.d.ts +1 -0
  105. package/dist/clis/wechat/search.js +31 -0
  106. package/dist/clis/wechat/send.d.ts +1 -0
  107. package/dist/clis/wechat/send.js +42 -0
  108. package/dist/clis/wechat/status.d.ts +1 -0
  109. package/dist/clis/wechat/status.js +29 -0
  110. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  111. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  112. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  113. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  114. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  115. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  116. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  117. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  118. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  119. package/dist/clis/xiaohongshu/download.js +155 -0
  120. package/dist/clis/xiaohongshu/search.js +1 -1
  121. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  122. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  123. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  124. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  125. package/dist/clis/xiaohongshu/user.js +46 -29
  126. package/dist/clis/zhihu/download.d.ts +11 -0
  127. package/dist/clis/zhihu/download.js +186 -0
  128. package/dist/clis/zhihu/download.test.d.ts +1 -0
  129. package/dist/clis/zhihu/download.test.js +10 -0
  130. package/dist/download/index.d.ts +79 -0
  131. package/dist/download/index.js +325 -0
  132. package/dist/download/progress.d.ts +36 -0
  133. package/dist/download/progress.js +111 -0
  134. package/dist/engine.test.js +15 -0
  135. package/dist/main.js +16 -3
  136. package/dist/pipeline/registry.js +2 -0
  137. package/dist/pipeline/steps/download.d.ts +34 -0
  138. package/dist/pipeline/steps/download.js +251 -0
  139. package/dist/pipeline/template.js +28 -0
  140. package/package.json +4 -3
  141. package/scripts/test-site.mjs +70 -0
  142. package/src/browser/discover.ts +23 -7
  143. package/src/browser.test.ts +23 -0
  144. package/src/build-manifest.test.ts +28 -0
  145. package/src/build-manifest.ts +147 -57
  146. package/src/clis/bilibili/download.ts +161 -0
  147. package/src/clis/chatwise/README.md +38 -0
  148. package/src/clis/chatwise/README.zh-CN.md +38 -0
  149. package/src/clis/chatwise/ask.ts +87 -0
  150. package/src/clis/chatwise/export.ts +51 -0
  151. package/src/clis/chatwise/history.ts +47 -0
  152. package/src/clis/chatwise/model.ts +87 -0
  153. package/src/clis/chatwise/new.ts +21 -0
  154. package/src/clis/chatwise/read.ts +42 -0
  155. package/src/clis/chatwise/screenshot.ts +33 -0
  156. package/src/clis/chatwise/send.ts +50 -0
  157. package/src/clis/chatwise/status.ts +25 -0
  158. package/src/clis/discord-app/README.md +28 -0
  159. package/src/clis/discord-app/README.zh-CN.md +28 -0
  160. package/src/clis/discord-app/channels.ts +48 -0
  161. package/src/clis/discord-app/members.ts +41 -0
  162. package/src/clis/discord-app/read.ts +49 -0
  163. package/src/clis/discord-app/search.ts +64 -0
  164. package/src/clis/discord-app/send.ts +32 -0
  165. package/src/clis/discord-app/servers.ts +39 -0
  166. package/src/clis/discord-app/status.ts +18 -0
  167. package/src/clis/feishu/README.md +20 -0
  168. package/src/clis/feishu/README.zh-CN.md +20 -0
  169. package/src/clis/feishu/new.ts +32 -0
  170. package/src/clis/feishu/read.ts +48 -0
  171. package/src/clis/feishu/search.ts +35 -0
  172. package/src/clis/feishu/send.ts +46 -0
  173. package/src/clis/feishu/status.ts +34 -0
  174. package/src/clis/grok/ask.ts +90 -0
  175. package/src/clis/grok/debug.ts +49 -0
  176. package/src/clis/jimeng/generate.yaml +84 -0
  177. package/src/clis/jimeng/history.yaml +47 -0
  178. package/src/clis/linux-do/categories.yaml +41 -0
  179. package/src/clis/linux-do/category.yaml +49 -0
  180. package/src/clis/linux-do/hot.yaml +50 -0
  181. package/src/clis/linux-do/latest.yaml +40 -0
  182. package/src/clis/linux-do/search.yaml +45 -0
  183. package/src/clis/linux-do/topic.yaml +38 -0
  184. package/src/clis/notion/README.md +29 -0
  185. package/src/clis/notion/README.zh-CN.md +29 -0
  186. package/src/clis/notion/export.ts +36 -0
  187. package/src/clis/notion/favorites.ts +87 -0
  188. package/src/clis/notion/new.ts +39 -0
  189. package/src/clis/notion/read.ts +33 -0
  190. package/src/clis/notion/search.ts +54 -0
  191. package/src/clis/notion/sidebar.ts +44 -0
  192. package/src/clis/notion/status.ts +18 -0
  193. package/src/clis/notion/write.ts +45 -0
  194. package/src/clis/twitter/download.ts +227 -0
  195. package/src/clis/wechat/README.md +28 -0
  196. package/src/clis/wechat/README.zh-CN.md +28 -0
  197. package/src/clis/wechat/chats.ts +33 -0
  198. package/src/clis/wechat/contacts.ts +33 -0
  199. package/src/clis/wechat/read.ts +72 -0
  200. package/src/clis/wechat/search.ts +36 -0
  201. package/src/clis/wechat/send.ts +49 -0
  202. package/src/clis/wechat/status.ts +35 -0
  203. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  204. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  205. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  206. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  207. package/src/clis/xiaohongshu/download.ts +173 -0
  208. package/src/clis/xiaohongshu/search.ts +1 -1
  209. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  210. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  211. package/src/clis/xiaohongshu/user.ts +52 -32
  212. package/src/clis/zhihu/download.test.ts +12 -0
  213. package/src/clis/zhihu/download.ts +223 -0
  214. package/src/download/index.ts +395 -0
  215. package/src/download/progress.ts +125 -0
  216. package/src/engine.test.ts +17 -0
  217. package/src/main.ts +12 -3
  218. package/src/pipeline/registry.ts +2 -0
  219. package/src/pipeline/steps/download.ts +310 -0
  220. package/src/pipeline/template.ts +26 -0
  221. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -0,0 +1,32 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const newCommand = cli({
6
+ site: 'feishu',
7
+ name: 'new',
8
+ description: 'Create a new message or document in Feishu',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [],
13
+ columns: ['Status'],
14
+ func: async (page: IPage | null) => {
15
+ try {
16
+ execSync("osascript -e 'tell application \"Lark\" to activate'");
17
+ execSync("osascript -e 'delay 0.3'");
18
+
19
+ // Cmd+N for new conversation/document
20
+ execSync(
21
+ "osascript " +
22
+ "-e 'tell application \"System Events\"' " +
23
+ "-e 'keystroke \"n\" using command down' " +
24
+ "-e 'end tell'"
25
+ );
26
+
27
+ return [{ Status: 'New item dialog opened (Cmd+N)' }];
28
+ } catch (err: any) {
29
+ return [{ Status: 'Error: ' + err.message }];
30
+ }
31
+ },
32
+ });
@@ -0,0 +1,48 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const readCommand = cli({
6
+ site: 'feishu',
7
+ name: 'read',
8
+ description: 'Read the current chat content by selecting all and copying',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [],
13
+ columns: ['Content'],
14
+ func: async (page: IPage | null) => {
15
+ try {
16
+ let clipBackup = '';
17
+ try {
18
+ clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
19
+ } catch { /* empty */ }
20
+
21
+ execSync("osascript -e 'tell application \"Lark\" to activate'");
22
+ execSync("osascript -e 'delay 0.3'");
23
+
24
+ execSync(
25
+ "osascript " +
26
+ "-e 'tell application \"System Events\"' " +
27
+ "-e 'keystroke \"a\" using command down' " +
28
+ "-e 'delay 0.2' " +
29
+ "-e 'keystroke \"c\" using command down' " +
30
+ "-e 'delay 0.2' " +
31
+ "-e 'end tell'"
32
+ );
33
+
34
+ const content = execSync('pbpaste', { encoding: 'utf-8' }).trim();
35
+
36
+ if (clipBackup) {
37
+ spawnSync('pbcopy', { input: clipBackup });
38
+ }
39
+
40
+ // Deselect
41
+ execSync("osascript -e 'tell application \"System Events\" to key code 53'");
42
+
43
+ return [{ Content: content || '(no content captured)' }];
44
+ } catch (err: any) {
45
+ return [{ Content: 'Error: ' + err.message }];
46
+ }
47
+ },
48
+ });
@@ -0,0 +1,35 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const searchCommand = cli({
6
+ site: 'feishu',
7
+ name: 'search',
8
+ description: 'Open Feishu global search and type a query (Cmd+K)',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [{ name: 'query', required: true, positional: true, help: 'Search query' }],
13
+ columns: ['Status'],
14
+ func: async (page: IPage | null, kwargs: any) => {
15
+ const query = kwargs.query as string;
16
+ try {
17
+ execSync("osascript -e 'tell application \"Lark\" to activate'");
18
+ execSync("osascript -e 'delay 0.3'");
19
+
20
+ // Feishu uses Cmd+K for global search (similar to Slack/Notion)
21
+ execSync(
22
+ "osascript " +
23
+ "-e 'tell application \"System Events\"' " +
24
+ "-e 'keystroke \"k\" using command down' " +
25
+ "-e 'delay 0.5' " +
26
+ `-e 'keystroke ${JSON.stringify(query)}' ` +
27
+ "-e 'end tell'"
28
+ );
29
+
30
+ return [{ Status: `Searching for: ${query}` }];
31
+ } catch (err: any) {
32
+ return [{ Status: 'Error: ' + err.message }];
33
+ }
34
+ },
35
+ });
@@ -0,0 +1,46 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const sendCommand = cli({
6
+ site: 'feishu',
7
+ name: 'send',
8
+ description: 'Send a message in the active Feishu (Lark) conversation',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
13
+ columns: ['Status'],
14
+ func: async (page: IPage | null, kwargs: any) => {
15
+ const text = kwargs.text as string;
16
+ try {
17
+ // Backup clipboard
18
+ let clipBackup = '';
19
+ try {
20
+ clipBackup = execSync('pbpaste', { encoding: 'utf-8' });
21
+ } catch { /* empty */ }
22
+
23
+ spawnSync('pbcopy', { input: text });
24
+
25
+ execSync("osascript -e 'tell application \"Lark\" to activate'");
26
+ execSync("osascript -e 'delay 0.5'");
27
+
28
+ execSync(
29
+ "osascript " +
30
+ "-e 'tell application \"System Events\"' " +
31
+ "-e 'keystroke \"v\" using command down' " +
32
+ "-e 'delay 0.2' " +
33
+ "-e 'keystroke return' " +
34
+ "-e 'end tell'"
35
+ );
36
+
37
+ if (clipBackup) {
38
+ spawnSync('pbcopy', { input: clipBackup });
39
+ }
40
+
41
+ return [{ Status: 'Success' }];
42
+ } catch (err: any) {
43
+ return [{ Status: 'Error: ' + err.message }];
44
+ }
45
+ },
46
+ });
@@ -0,0 +1,34 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const statusCommand = cli({
6
+ site: 'feishu',
7
+ name: 'status',
8
+ description: 'Check if Feishu (Lark) Desktop is running on macOS',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [],
13
+ columns: ['Status', 'Detail'],
14
+ func: async (page: IPage | null) => {
15
+ try {
16
+ const running = execSync("osascript -e 'application \"Lark\" is running'", { encoding: 'utf-8' }).trim();
17
+ if (running !== 'true') {
18
+ return [{ Status: 'Stopped', Detail: 'Feishu/Lark is not running' }];
19
+ }
20
+
21
+ const windowCount = execSync(
22
+ "osascript -e 'tell application \"System Events\" to count windows of application process \"Lark\"'",
23
+ { encoding: 'utf-8' }
24
+ ).trim();
25
+
26
+ return [{
27
+ Status: 'Running',
28
+ Detail: `${windowCount} window(s) open`,
29
+ }];
30
+ } catch (err: any) {
31
+ return [{ Status: 'Error', Detail: err.message }];
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,90 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const askCommand = cli({
5
+ site: 'grok',
6
+ name: 'ask',
7
+ description: 'Send a message to Grok and get response',
8
+ domain: 'grok.com',
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ args: [
12
+ { name: 'prompt', type: 'string', required: true },
13
+ { name: 'timeout', type: 'int', default: 120 },
14
+ { name: 'new', type: 'boolean', default: false },
15
+ ],
16
+ columns: ['response'],
17
+ func: async (page: IPage, kwargs: Record<string, any>) => {
18
+ const prompt = kwargs.prompt as string;
19
+ const timeoutMs = ((kwargs.timeout as number) || 120) * 1000;
20
+ const newChat = kwargs.new as boolean;
21
+
22
+ if (newChat) {
23
+ await page.goto('https://grok.com');
24
+ await page.wait(2);
25
+ await page.evaluate(`(() => {
26
+ const btn = [...document.querySelectorAll('a, button')].find(b => {
27
+ const t = (b.textContent || '').trim().toLowerCase();
28
+ return t.includes('new') || b.getAttribute('href') === '/';
29
+ });
30
+ if (btn) btn.click();
31
+ })()`);
32
+ await page.wait(2);
33
+ }
34
+
35
+ await page.goto('https://grok.com');
36
+ await page.wait(3);
37
+
38
+ const promptJson = JSON.stringify(prompt);
39
+
40
+ const sendResult = await page.evaluate(`(async () => {
41
+ try {
42
+ const box = document.querySelector('textarea');
43
+ if (!box) return { ok: false, msg: 'no textarea' };
44
+ box.focus(); box.value = '';
45
+ document.execCommand('selectAll');
46
+ document.execCommand('insertText', false, ${promptJson});
47
+ await new Promise(r => setTimeout(r, 1500));
48
+ const btn = document.querySelector('button[aria-label="\\u63d0\\u4ea4"]');
49
+ if (btn && !btn.disabled) { btn.click(); return { ok: true, msg: 'clicked' }; }
50
+ const sub = [...document.querySelectorAll('button[type="submit"]')].find(b => !b.disabled);
51
+ if (sub) { sub.click(); return { ok: true, msg: 'clicked-submit' }; }
52
+ box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
53
+ return { ok: true, msg: 'enter' };
54
+ } catch (e) { return { ok: false, msg: e.toString() }; }
55
+ })()`);
56
+
57
+ if (!sendResult || !sendResult.ok) {
58
+ return [{ response: '[SEND FAILED] ' + JSON.stringify(sendResult) }];
59
+ }
60
+
61
+ const startTime = Date.now();
62
+ let lastText = '';
63
+ let stableCount = 0;
64
+
65
+ while (Date.now() - startTime < timeoutMs) {
66
+ await page.wait(3);
67
+ const response = await page.evaluate(`(() => {
68
+ const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]');
69
+ if (bubbles.length < 2) return '';
70
+ const last = bubbles[bubbles.length - 1];
71
+ const text = (last.innerText || '').trim();
72
+ if (!text || text.length < 2) return '';
73
+ return text;
74
+ })()`);
75
+
76
+ if (response && response.length > 2) {
77
+ if (response === lastText) {
78
+ stableCount++;
79
+ if (stableCount >= 2) return [{ response }];
80
+ } else {
81
+ stableCount = 0;
82
+ }
83
+ }
84
+ lastText = response || '';
85
+ }
86
+
87
+ if (lastText) return [{ response: lastText }];
88
+ return [{ response: '[NO RESPONSE]' }];
89
+ },
90
+ });
@@ -0,0 +1,49 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const debugCommand = cli({
5
+ site: 'grok',
6
+ name: 'debug',
7
+ description: 'Debug grok page structure',
8
+ domain: 'grok.com',
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ columns: ['data'],
12
+ func: async (page: IPage, _kwargs: Record<string, any>) => {
13
+ await page.goto('https://grok.com');
14
+ await page.wait(3);
15
+
16
+ // Get all button-like elements near textarea
17
+ const debug = await page.evaluate(`(() => {
18
+ const ta = document.querySelector('textarea');
19
+ if (!ta) return { error: 'no textarea' };
20
+
21
+ // Get parent containers
22
+ let parent = ta.parentElement;
23
+ const parents = [];
24
+ for (let i = 0; i < 5 && parent; i++) {
25
+ parents.push({
26
+ tag: parent.tagName,
27
+ class: parent.className?.substring(0, 80),
28
+ childCount: parent.children.length,
29
+ });
30
+ parent = parent.parentElement;
31
+ }
32
+
33
+ // Find buttons in the form/container near textarea
34
+ const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement;
35
+ const buttons = form ? [...form.querySelectorAll('button')].map(b => ({
36
+ testid: b.getAttribute('data-testid'),
37
+ type: b.type,
38
+ disabled: b.disabled,
39
+ text: (b.textContent || '').substring(0, 30),
40
+ html: b.outerHTML.substring(0, 200),
41
+ rect: b.getBoundingClientRect().toJSON(),
42
+ })) : [];
43
+
44
+ return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) };
45
+ })()`);
46
+
47
+ return [{ data: JSON.stringify(debug, null, 2) }];
48
+ },
49
+ });
@@ -0,0 +1,84 @@
1
+ site: jimeng
2
+ name: generate
3
+ description: 即梦AI 文生图 — 输入 prompt 生成图片
4
+ domain: jimeng.jianying.com
5
+ strategy: cookie
6
+ browser: true
7
+
8
+ args:
9
+ prompt:
10
+ type: string
11
+ required: true
12
+ description: "图片描述 prompt"
13
+ model:
14
+ type: string
15
+ default: "high_aes_general_v50"
16
+ description: "模型: high_aes_general_v50 (5.0 Lite), high_aes_general_v42 (4.6), high_aes_general_v40 (4.0)"
17
+ wait:
18
+ type: int
19
+ default: 40
20
+ description: "等待生成完成的秒数"
21
+
22
+ columns: [status, prompt, image_count, image_urls]
23
+
24
+ pipeline:
25
+ - navigate: https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0
26
+ - wait: 3
27
+ - evaluate: |
28
+ (async () => {
29
+ const prompt = ${{ args.prompt | json }};
30
+ const waitSec = ${{ args.wait }};
31
+
32
+ // Step 1: Count existing images before generation
33
+ const beforeImgs = document.querySelectorAll('img[src*="dreamina-sign"], img[src*="tb4s082cfz"]').length;
34
+
35
+ // Step 2: Clear and set prompt
36
+ const editors = document.querySelectorAll('[contenteditable="true"]');
37
+ const editor = editors[0];
38
+ if (!editor) return [{ status: 'failed', prompt: prompt, image_count: 0, image_urls: 'Editor not found' }];
39
+
40
+ editor.focus();
41
+ await new Promise(r => setTimeout(r, 200));
42
+ document.execCommand('selectAll');
43
+ await new Promise(r => setTimeout(r, 100));
44
+ document.execCommand('delete');
45
+ await new Promise(r => setTimeout(r, 200));
46
+ document.execCommand('insertText', false, prompt);
47
+ await new Promise(r => setTimeout(r, 500));
48
+
49
+ // Step 3: Click generate
50
+ const btn = document.querySelector('.lv-btn.lv-btn-primary[class*="circle"]');
51
+ if (!btn) return [{ status: 'failed', prompt: prompt, image_count: 0, image_urls: 'Generate button not found' }];
52
+ btn.click();
53
+
54
+ // Step 4: Wait for new images to appear
55
+ let newImgs = [];
56
+ for (let i = 0; i < waitSec; i++) {
57
+ await new Promise(r => setTimeout(r, 1000));
58
+ const allImgs = document.querySelectorAll('img[src*="dreamina-sign"], img[src*="tb4s082cfz"]');
59
+ if (allImgs.length > beforeImgs) {
60
+ // New images appeared — generation complete
61
+ newImgs = Array.from(allImgs).slice(0, allImgs.length - beforeImgs);
62
+ break;
63
+ }
64
+ }
65
+
66
+ if (newImgs.length === 0) {
67
+ return [{ status: 'timeout', prompt: prompt, image_count: 0, image_urls: 'Generation may still be in progress' }];
68
+ }
69
+
70
+ // Step 5: Extract image URLs (use thumbnail URLs which are accessible)
71
+ const urls = newImgs.map(img => img.src);
72
+
73
+ return [{
74
+ status: 'success',
75
+ prompt: prompt.substring(0, 80),
76
+ image_count: urls.length,
77
+ image_urls: urls.join('\n')
78
+ }];
79
+ })()
80
+ - map:
81
+ status: ${{ item.status }}
82
+ prompt: ${{ item.prompt }}
83
+ image_count: ${{ item.image_count }}
84
+ image_urls: ${{ item.image_urls }}
@@ -0,0 +1,47 @@
1
+ site: jimeng
2
+ name: history
3
+ description: 即梦AI 查看最近生成的作品
4
+ domain: jimeng.jianying.com
5
+ strategy: cookie
6
+ browser: true
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 5
12
+
13
+ columns: [prompt, model, status, image_url, created_at]
14
+
15
+ pipeline:
16
+ - navigate: https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0
17
+ - wait: 3
18
+ - evaluate: |
19
+ (async () => {
20
+ const limit = ${{ args.limit }};
21
+ const res = await fetch('/mweb/v1/get_history?aid=513695&device_platform=web&region=cn&da_version=3.3.11&web_version=7.5.0&aigc_features=app_lip_sync', {
22
+ method: 'POST',
23
+ credentials: 'include',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ cursor: '', count: limit, need_page_item: true, need_aigc_data: true, aigc_mode_list: ['workbench'] })
26
+ });
27
+ const data = await res.json();
28
+ const items = data?.data?.history_list || [];
29
+ return items.slice(0, limit).map(item => {
30
+ const params = item.aigc_image_params?.text2image_params || {};
31
+ const images = item.image?.large_images || [];
32
+ return {
33
+ prompt: params.prompt || item.common_attr?.title || 'N/A',
34
+ model: params.model_config?.model_name || 'unknown',
35
+ status: item.common_attr?.status === 102 ? 'completed' : 'pending',
36
+ image_url: images[0]?.image_url || '',
37
+ created_at: new Date((item.common_attr?.create_time || 0) * 1000).toLocaleString('zh-CN'),
38
+ };
39
+ });
40
+ })()
41
+ - map:
42
+ prompt: ${{ item.prompt }}
43
+ model: ${{ item.model }}
44
+ status: ${{ item.status }}
45
+ image_url: ${{ item.image_url }}
46
+ created_at: ${{ item.created_at }}
47
+ - limit: ${{ args.limit }}
@@ -0,0 +1,41 @@
1
+ site: linux-do
2
+ name: categories
3
+ description: linux.do 分类列表
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ limit:
9
+ type: int
10
+ default: 20
11
+ description: Number of categories
12
+
13
+ pipeline:
14
+ - navigate: https://linux.do
15
+
16
+ - evaluate: |
17
+ (async () => {
18
+ const res = await fetch('/categories.json', { credentials: 'include' });
19
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
20
+ let data;
21
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
22
+ const cats = data?.category_list?.categories || [];
23
+ return cats.slice(0, ${{ args.limit }}).map(c => ({
24
+ name: c.name,
25
+ slug: c.slug,
26
+ id: c.id,
27
+ topics: c.topic_count,
28
+ description: (c.description_text || '').slice(0, 80),
29
+ }));
30
+ })()
31
+
32
+ - map:
33
+ name: ${{ item.name }}
34
+ slug: ${{ item.slug }}
35
+ id: ${{ item.id }}
36
+ topics: ${{ item.topics }}
37
+ description: ${{ item.description }}
38
+
39
+ - limit: ${{ args.limit }}
40
+
41
+ columns: [name, slug, id, topics, description]
@@ -0,0 +1,49 @@
1
+ site: linux-do
2
+ name: category
3
+ description: linux.do 分类内话题
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ slug:
9
+ type: str
10
+ required: true
11
+ description: Category slug (use 'categories' command to find)
12
+ id:
13
+ type: int
14
+ required: true
15
+ description: Category ID (use 'categories' command to find)
16
+ limit:
17
+ type: int
18
+ default: 20
19
+ description: Number of topics
20
+
21
+ pipeline:
22
+ - navigate: https://linux.do
23
+
24
+ - evaluate: |
25
+ (async () => {
26
+ const slug = ${{ args.slug | json }};
27
+ const res = await fetch('/c/' + encodeURIComponent(slug) + '/${{ args.id }}.json', { credentials: 'include' });
28
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
29
+ let data;
30
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
31
+ const topics = data?.topic_list?.topics || [];
32
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
33
+ title: t.title,
34
+ replies: (t.posts_count || 1) - 1,
35
+ views: t.views,
36
+ likes: t.like_count,
37
+ }));
38
+ })()
39
+
40
+ - map:
41
+ rank: ${{ index + 1 }}
42
+ title: ${{ item.title }}
43
+ replies: ${{ item.replies }}
44
+ views: ${{ item.views }}
45
+ likes: ${{ item.likes }}
46
+
47
+ - limit: ${{ args.limit }}
48
+
49
+ columns: [rank, title, replies, views, likes]
@@ -0,0 +1,50 @@
1
+ site: linux-do
2
+ name: hot
3
+ description: linux.do 热门话题
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ limit:
9
+ type: int
10
+ default: 20
11
+ description: Number of topics
12
+ period:
13
+ type: str
14
+ default: weekly
15
+ description: Time period
16
+ choices: [all, daily, weekly, monthly, yearly]
17
+
18
+ pipeline:
19
+ - navigate: https://linux.do
20
+
21
+ - evaluate: |
22
+ (async () => {
23
+ const period = ${{ args.period | json }};
24
+ const res = await fetch('/top.json?period=' + encodeURIComponent(period), { credentials: 'include' });
25
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
26
+ let data;
27
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
28
+ const topics = data?.topic_list?.topics || [];
29
+ const cats = data?.topic_list?.categories || data?.categories || [];
30
+ const catMap = Object.fromEntries(cats.map(c => [c.id, c.name]));
31
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
32
+ title: t.title,
33
+ replies: (t.posts_count || 1) - 1,
34
+ views: t.views,
35
+ likes: t.like_count,
36
+ category: catMap[t.category_id] || String(t.category_id),
37
+ }));
38
+ })()
39
+
40
+ - map:
41
+ rank: ${{ index + 1 }}
42
+ title: ${{ item.title }}
43
+ replies: ${{ item.replies }}
44
+ views: ${{ item.views }}
45
+ likes: ${{ item.likes }}
46
+ category: ${{ item.category }}
47
+
48
+ - limit: ${{ args.limit }}
49
+
50
+ columns: [rank, title, replies, views, likes, category]
@@ -0,0 +1,40 @@
1
+ site: linux-do
2
+ name: latest
3
+ description: linux.do 最新话题
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ limit:
9
+ type: int
10
+ default: 20
11
+ description: Number of topics
12
+
13
+ pipeline:
14
+ - navigate: https://linux.do
15
+
16
+ - evaluate: |
17
+ (async () => {
18
+ const res = await fetch('/latest.json', { credentials: 'include' });
19
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
20
+ let data;
21
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
22
+ const topics = data?.topic_list?.topics || [];
23
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
24
+ title: t.title,
25
+ replies: (t.posts_count || 1) - 1,
26
+ views: t.views,
27
+ likes: t.like_count,
28
+ }));
29
+ })()
30
+
31
+ - map:
32
+ rank: ${{ index + 1 }}
33
+ title: ${{ item.title }}
34
+ replies: ${{ item.replies }}
35
+ views: ${{ item.views }}
36
+ likes: ${{ item.likes }}
37
+
38
+ - limit: ${{ args.limit }}
39
+
40
+ columns: [rank, title, replies, views, likes]