@jackwener/opencli 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/.github/workflows/build-extension.yml +80 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/docs.yml +52 -0
  4. package/.github/workflows/e2e-headed.yml +2 -2
  5. package/.github/workflows/pkg-pr-new.yml +2 -2
  6. package/.github/workflows/release.yml +2 -5
  7. package/.github/workflows/security.yml +2 -2
  8. package/CDP.md +1 -1
  9. package/CDP.zh-CN.md +1 -1
  10. package/README.md +42 -34
  11. package/README.zh-CN.md +42 -34
  12. package/SKILL.md +3 -5
  13. package/dist/browser/cdp.d.ts +42 -0
  14. package/dist/browser/cdp.js +339 -0
  15. package/dist/browser/daemon-client.d.ts +3 -1
  16. package/dist/browser/daemon-client.js +4 -0
  17. package/dist/browser/dom-helpers.d.ts +20 -0
  18. package/dist/browser/dom-helpers.js +109 -0
  19. package/dist/browser/index.d.ts +3 -0
  20. package/dist/browser/index.js +4 -0
  21. package/dist/browser/mcp.d.ts +1 -0
  22. package/dist/browser/mcp.js +10 -5
  23. package/dist/browser/page.d.ts +7 -0
  24. package/dist/browser/page.js +39 -123
  25. package/dist/browser/utils.d.ts +10 -0
  26. package/dist/browser/utils.js +27 -0
  27. package/dist/browser.test.js +49 -1
  28. package/dist/build-manifest.js +3 -1
  29. package/dist/build-manifest.test.js +34 -0
  30. package/dist/capabilityRouting.d.ts +2 -0
  31. package/dist/capabilityRouting.js +30 -0
  32. package/dist/capabilityRouting.test.d.ts +1 -0
  33. package/dist/capabilityRouting.test.js +42 -0
  34. package/dist/chaoxing.d.ts +58 -0
  35. package/dist/chaoxing.js +225 -0
  36. package/dist/chaoxing.test.d.ts +1 -0
  37. package/dist/chaoxing.test.js +45 -0
  38. package/dist/cli-manifest.json +885 -48
  39. package/dist/cli.d.ts +1 -0
  40. package/dist/cli.js +234 -0
  41. package/dist/clis/antigravity/serve.d.ts +14 -0
  42. package/dist/clis/antigravity/serve.js +263 -0
  43. package/dist/clis/bilibili/download.js +4 -14
  44. package/dist/clis/boss/chatlist.d.ts +1 -0
  45. package/dist/clis/boss/chatlist.js +50 -0
  46. package/dist/clis/boss/chatmsg.d.ts +1 -0
  47. package/dist/clis/boss/chatmsg.js +73 -0
  48. package/dist/clis/boss/resume.d.ts +1 -0
  49. package/dist/clis/boss/resume.js +249 -0
  50. package/dist/clis/boss/send.d.ts +1 -0
  51. package/dist/clis/boss/send.js +176 -0
  52. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  53. package/dist/clis/chaoxing/assignments.js +74 -0
  54. package/dist/clis/chaoxing/exams.d.ts +1 -0
  55. package/dist/clis/chaoxing/exams.js +74 -0
  56. package/dist/clis/chatgpt/ask.js +15 -14
  57. package/dist/clis/chatgpt/ax.d.ts +1 -0
  58. package/dist/clis/chatgpt/ax.js +78 -0
  59. package/dist/clis/chatgpt/read.js +5 -6
  60. package/dist/clis/hf/top.d.ts +1 -0
  61. package/dist/clis/hf/top.js +119 -0
  62. package/dist/clis/jike/comment.d.ts +1 -0
  63. package/dist/clis/jike/comment.js +107 -0
  64. package/dist/clis/jike/create.d.ts +1 -0
  65. package/dist/clis/jike/create.js +106 -0
  66. package/dist/clis/jike/feed.d.ts +1 -0
  67. package/dist/clis/jike/feed.js +67 -0
  68. package/dist/clis/jike/like.d.ts +1 -0
  69. package/dist/clis/jike/like.js +61 -0
  70. package/dist/clis/jike/notifications.d.ts +1 -0
  71. package/dist/clis/jike/notifications.js +169 -0
  72. package/dist/clis/jike/post.yaml +58 -0
  73. package/dist/clis/jike/repost.d.ts +1 -0
  74. package/dist/clis/jike/repost.js +103 -0
  75. package/dist/clis/jike/search.d.ts +1 -0
  76. package/dist/clis/jike/search.js +67 -0
  77. package/dist/clis/jike/shared.d.ts +19 -0
  78. package/dist/clis/jike/shared.js +25 -0
  79. package/dist/clis/jike/topic.yaml +52 -0
  80. package/dist/clis/jike/user.yaml +51 -0
  81. package/dist/clis/smzdm/search.js +28 -39
  82. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  83. package/dist/clis/stackoverflow/hot.yaml +28 -0
  84. package/dist/clis/stackoverflow/search.yaml +32 -0
  85. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  86. package/dist/clis/twitter/download.js +6 -16
  87. package/dist/clis/twitter/post.js +9 -2
  88. package/dist/clis/twitter/search.js +14 -33
  89. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  90. package/dist/clis/xiaohongshu/download.js +4 -4
  91. package/dist/clis/zhihu/download.js +3 -3
  92. package/dist/doctor.d.ts +7 -0
  93. package/dist/doctor.js +16 -0
  94. package/dist/download/index.d.ts +12 -8
  95. package/dist/download/index.js +11 -3
  96. package/dist/download/index.test.d.ts +1 -0
  97. package/dist/download/index.test.js +14 -0
  98. package/dist/engine.js +25 -14
  99. package/dist/explore.d.ts +1 -0
  100. package/dist/explore.js +48 -103
  101. package/dist/generate.js +1 -0
  102. package/dist/interceptor.js +3 -2
  103. package/dist/main.js +4 -193
  104. package/dist/output.d.ts +2 -1
  105. package/dist/output.js +3 -1
  106. package/dist/pipeline/executor.test.js +1 -0
  107. package/dist/pipeline/steps/download.js +14 -18
  108. package/dist/registry.d.ts +4 -3
  109. package/dist/registry.js +5 -2
  110. package/dist/runtime.d.ts +4 -1
  111. package/dist/runtime.js +2 -2
  112. package/dist/scripts/framework.d.ts +4 -0
  113. package/dist/scripts/framework.js +21 -0
  114. package/dist/scripts/interact.d.ts +4 -0
  115. package/dist/scripts/interact.js +20 -0
  116. package/dist/scripts/store.d.ts +9 -0
  117. package/dist/scripts/store.js +44 -0
  118. package/dist/synthesize.js +1 -1
  119. package/dist/types.d.ts +12 -0
  120. package/dist/verify.d.ts +6 -1
  121. package/dist/verify.js +54 -2
  122. package/docs/.vitepress/config.mts +193 -0
  123. package/docs/adapters/browser/apple-podcasts.md +28 -0
  124. package/docs/adapters/browser/bbc.md +26 -0
  125. package/docs/adapters/browser/bilibili.md +38 -0
  126. package/docs/adapters/browser/boss.md +28 -0
  127. package/docs/adapters/browser/coupang.md +28 -0
  128. package/docs/adapters/browser/ctrip.md +27 -0
  129. package/docs/adapters/browser/github.md +26 -0
  130. package/docs/adapters/browser/hackernews.md +26 -0
  131. package/docs/adapters/browser/linkedin.md +27 -0
  132. package/docs/adapters/browser/reddit.md +41 -0
  133. package/docs/adapters/browser/reuters.md +27 -0
  134. package/docs/adapters/browser/smzdm.md +27 -0
  135. package/docs/adapters/browser/twitter.md +47 -0
  136. package/docs/adapters/browser/v2ex.md +32 -0
  137. package/docs/adapters/browser/weibo.md +27 -0
  138. package/docs/adapters/browser/xiaohongshu.md +32 -0
  139. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  140. package/docs/adapters/browser/xueqiu.md +32 -0
  141. package/docs/adapters/browser/yahoo-finance.md +26 -0
  142. package/docs/adapters/browser/youtube.md +29 -0
  143. package/docs/adapters/browser/zhihu.md +30 -0
  144. package/docs/adapters/desktop/antigravity.md +46 -0
  145. package/docs/adapters/desktop/chatgpt.md +43 -0
  146. package/docs/adapters/desktop/chatwise.md +38 -0
  147. package/docs/adapters/desktop/codex.md +32 -0
  148. package/docs/adapters/desktop/cursor.md +33 -0
  149. package/docs/adapters/desktop/discord.md +28 -0
  150. package/docs/adapters/desktop/feishu.md +20 -0
  151. package/docs/adapters/desktop/neteasemusic.md +31 -0
  152. package/docs/adapters/desktop/notion.md +29 -0
  153. package/docs/adapters/desktop/wechat.md +28 -0
  154. package/docs/adapters/index.md +49 -0
  155. package/docs/advanced/cdp.md +103 -0
  156. package/docs/advanced/download.md +63 -0
  157. package/docs/advanced/electron.md +125 -0
  158. package/docs/advanced/remote-chrome.md +72 -0
  159. package/docs/developer/ai-workflow.md +66 -0
  160. package/docs/developer/architecture.md +90 -0
  161. package/docs/developer/contributing.md +136 -0
  162. package/docs/developer/testing.md +237 -0
  163. package/docs/developer/ts-adapter.md +87 -0
  164. package/docs/developer/yaml-adapter.md +108 -0
  165. package/docs/guide/browser-bridge.md +38 -0
  166. package/docs/guide/getting-started.md +56 -0
  167. package/docs/guide/installation.md +37 -0
  168. package/docs/guide/troubleshooting.md +56 -0
  169. package/docs/index.md +35 -0
  170. package/docs/zh/adapters/index.md +5 -0
  171. package/docs/zh/advanced/cdp.md +3 -0
  172. package/docs/zh/developer/contributing.md +24 -0
  173. package/docs/zh/guide/browser-bridge.md +25 -0
  174. package/docs/zh/guide/getting-started.md +40 -0
  175. package/docs/zh/guide/installation.md +37 -0
  176. package/docs/zh/index.md +29 -0
  177. package/extension/dist/background.js +386 -438
  178. package/extension/manifest.json +2 -2
  179. package/extension/package-lock.json +1156 -0
  180. package/extension/src/background.test.ts +151 -0
  181. package/extension/src/background.ts +124 -53
  182. package/extension/src/protocol.ts +3 -1
  183. package/package.json +7 -3
  184. package/src/browser/cdp.ts +367 -0
  185. package/src/browser/daemon-client.ts +7 -1
  186. package/src/browser/dom-helpers.ts +116 -0
  187. package/src/browser/index.ts +4 -0
  188. package/src/browser/mcp.ts +14 -6
  189. package/src/browser/page.ts +47 -124
  190. package/src/browser/utils.ts +27 -0
  191. package/src/browser.test.ts +56 -0
  192. package/src/build-manifest.test.ts +36 -0
  193. package/src/build-manifest.ts +2 -1
  194. package/src/capabilityRouting.test.ts +47 -0
  195. package/src/capabilityRouting.ts +28 -0
  196. package/src/chaoxing.test.ts +53 -0
  197. package/src/chaoxing.ts +268 -0
  198. package/src/cli.ts +205 -0
  199. package/src/clis/antigravity/SKILL.md +5 -0
  200. package/src/clis/antigravity/serve.ts +329 -0
  201. package/src/clis/bilibili/download.ts +4 -15
  202. package/src/clis/boss/chatlist.ts +50 -0
  203. package/src/clis/boss/chatmsg.ts +70 -0
  204. package/src/clis/boss/resume.ts +262 -0
  205. package/src/clis/boss/send.ts +193 -0
  206. package/src/clis/chaoxing/README.md +36 -0
  207. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  208. package/src/clis/chaoxing/assignments.ts +88 -0
  209. package/src/clis/chaoxing/exams.ts +88 -0
  210. package/src/clis/chatgpt/ask.ts +14 -15
  211. package/src/clis/chatgpt/ax.ts +81 -0
  212. package/src/clis/chatgpt/read.ts +5 -7
  213. package/src/clis/hf/top.ts +141 -0
  214. package/src/clis/jike/comment.ts +113 -0
  215. package/src/clis/jike/create.ts +113 -0
  216. package/src/clis/jike/feed.ts +74 -0
  217. package/src/clis/jike/like.ts +65 -0
  218. package/src/clis/jike/notifications.ts +185 -0
  219. package/src/clis/jike/post.yaml +58 -0
  220. package/src/clis/jike/repost.ts +114 -0
  221. package/src/clis/jike/search.ts +74 -0
  222. package/src/clis/jike/shared.ts +36 -0
  223. package/src/clis/jike/topic.yaml +52 -0
  224. package/src/clis/jike/user.yaml +51 -0
  225. package/src/clis/smzdm/search.ts +30 -39
  226. package/src/clis/stackoverflow/bounties.yaml +29 -0
  227. package/src/clis/stackoverflow/hot.yaml +28 -0
  228. package/src/clis/stackoverflow/search.yaml +32 -0
  229. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  230. package/src/clis/twitter/download.ts +6 -17
  231. package/src/clis/twitter/post.ts +9 -2
  232. package/src/clis/twitter/search.ts +15 -33
  233. package/src/clis/xiaohongshu/download.ts +4 -4
  234. package/src/clis/zhihu/download.ts +3 -3
  235. package/src/doctor.ts +18 -2
  236. package/src/download/index.test.ts +16 -0
  237. package/src/download/index.ts +22 -4
  238. package/src/engine.ts +20 -13
  239. package/src/explore.ts +54 -103
  240. package/src/generate.ts +1 -0
  241. package/src/interceptor.ts +3 -2
  242. package/src/main.ts +4 -180
  243. package/src/output.ts +15 -13
  244. package/src/pipeline/executor.test.ts +1 -0
  245. package/src/pipeline/steps/download.ts +14 -17
  246. package/src/registry.ts +9 -5
  247. package/src/runtime.ts +3 -2
  248. package/src/scripts/framework.ts +20 -0
  249. package/src/scripts/interact.ts +22 -0
  250. package/src/scripts/store.ts +40 -0
  251. package/src/synthesize.ts +1 -1
  252. package/src/types.ts +9 -0
  253. package/src/verify.ts +64 -3
@@ -0,0 +1,88 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import {
3
+ getCourses, initSession, enterCourse, getTabIframeUrl,
4
+ parseExamsFromDom, sleep,
5
+ type ExamRow,
6
+ } from '../../chaoxing.js';
7
+
8
+ cli({
9
+ site: 'chaoxing',
10
+ name: 'exams',
11
+ description: '学习通考试列表',
12
+ domain: 'mooc2-ans.chaoxing.com',
13
+ strategy: Strategy.COOKIE,
14
+ timeoutSeconds: 90,
15
+ args: [
16
+ { name: 'course', type: 'string', help: '按课程名过滤(模糊匹配)' },
17
+ {
18
+ name: 'status',
19
+ type: 'string',
20
+ default: 'all',
21
+ choices: ['all', 'upcoming', 'ongoing', 'finished'],
22
+ help: '按状态过滤',
23
+ },
24
+ { name: 'limit', type: 'int', default: 20, help: '最大返回数量' },
25
+ ],
26
+ columns: ['rank', 'course', 'title', 'start', 'end', 'status', 'score'],
27
+
28
+ func: async (page, kwargs) => {
29
+ const { course: courseFilter, status: statusFilter = 'all', limit = 20 } = kwargs;
30
+
31
+ // 1. Establish session
32
+ await initSession(page);
33
+
34
+ // 2. Get courses
35
+ const courses = await getCourses(page);
36
+ if (!courses.length) throw new Error('未获取到课程列表,请确认已登录学习通');
37
+
38
+ const filtered = courseFilter
39
+ ? courses.filter(c => c.title.includes(courseFilter))
40
+ : courses;
41
+ if (courseFilter && !filtered.length) {
42
+ throw new Error(`未找到匹配「${courseFilter}」的课程`);
43
+ }
44
+
45
+ // 3. Per-course: enter → click 考试 tab → navigate to iframe → parse
46
+ const allRows: ExamRow[] = [];
47
+
48
+ for (const c of filtered) {
49
+ try {
50
+ await enterCourse(page, c);
51
+ const iframeUrl = await getTabIframeUrl(page, '考试');
52
+ if (!iframeUrl) continue;
53
+
54
+ await page.goto(iframeUrl);
55
+ await page.wait(2);
56
+
57
+ const rows = await parseExamsFromDom(page, c.title);
58
+ allRows.push(...rows);
59
+ } catch {
60
+ // Single course failure: skip, continue
61
+ }
62
+ if (filtered.length > 1) await sleep(600);
63
+ }
64
+
65
+ // 4. Sort: upcoming first
66
+ allRows.sort((a, b) => {
67
+ const order = (s: string) =>
68
+ s === '未开始' ? 0 : s === '进行中' ? 1 : s === '已结束' ? 2 : s === '已完成' ? 3 : 4;
69
+ return order(a.status) - order(b.status);
70
+ });
71
+
72
+ // 5. Filter by status
73
+ const statusMap: Record<string, string[]> = {
74
+ upcoming: ['未开始'],
75
+ ongoing: ['进行中'],
76
+ finished: ['已结束', '已完成'],
77
+ };
78
+ const finalRows =
79
+ statusFilter === 'all'
80
+ ? allRows
81
+ : allRows.filter(r => statusMap[statusFilter]?.includes(r.status));
82
+
83
+ return finalRows.slice(0, Number(limit)).map((item, i) => ({
84
+ rank: i + 1,
85
+ ...item,
86
+ }));
87
+ },
88
+ });
@@ -1,6 +1,7 @@
1
1
  import { execSync, spawnSync } from 'node:child_process';
2
2
  import { cli, Strategy } from '../../registry.js';
3
3
  import type { IPage } from '../../types.js';
4
+ import { getVisibleChatMessages } from './ax.js';
4
5
 
5
6
  export const askCommand = cli({
6
7
  site: 'chatgpt',
@@ -21,6 +22,7 @@ export const askCommand = cli({
21
22
  // Backup clipboard
22
23
  let clipBackup = '';
23
24
  try { clipBackup = execSync('pbpaste', { encoding: 'utf-8' }); } catch {}
25
+ const messagesBefore = getVisibleChatMessages();
24
26
 
25
27
  // Send the message
26
28
  spawnSync('pbcopy', { input: text });
@@ -35,33 +37,30 @@ export const askCommand = cli({
35
37
  "-e 'end tell'";
36
38
  execSync(cmd);
37
39
 
38
- // Clear clipboard marker
39
- spawnSync('pbcopy', { input: '__OPENCLI_WAITING__' });
40
+ // Restore clipboard after the prompt is sent.
41
+ if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
40
42
 
41
- // Wait for response, then read it
42
- const pollInterval = 3;
43
+ // Wait for response, then read the latest visible assistant message from the AX tree.
44
+ const pollInterval = 1;
43
45
  const maxPolls = Math.ceil(timeout / pollInterval);
44
46
  let response = '';
45
47
 
46
48
  for (let i = 0; i < maxPolls; i++) {
47
- // Wait
48
49
  execSync(`sleep ${pollInterval}`);
49
-
50
- // Try Cmd+Shift+C to copy the latest response
51
50
  execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
52
- execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
53
- execSync("osascript -e 'delay 0.3'");
51
+ execSync("osascript -e 'delay 0.2'");
52
+
53
+ const messagesNow = getVisibleChatMessages();
54
+ if (messagesNow.length <= messagesBefore.length) continue;
54
55
 
55
- const copied = execSync('pbpaste', { encoding: 'utf-8' }).trim();
56
- if (copied && copied !== '__OPENCLI_WAITING__' && copied !== text) {
57
- response = copied;
56
+ const newMessages = messagesNow.slice(messagesBefore.length);
57
+ const candidate = [...newMessages].reverse().find((message) => message !== text);
58
+ if (candidate) {
59
+ response = candidate;
58
60
  break;
59
61
  }
60
62
  }
61
63
 
62
- // Restore clipboard
63
- if (clipBackup) spawnSync('pbcopy', { input: clipBackup });
64
-
65
64
  if (!response) {
66
65
  return [
67
66
  { Role: 'User', Text: text },
@@ -0,0 +1,81 @@
1
+ import { execFileSync } from 'node:child_process';
2
+
3
+ const AX_READ_SCRIPT = `
4
+ import Cocoa
5
+ import ApplicationServices
6
+
7
+ func attr(_ el: AXUIElement, _ name: String) -> AnyObject? {
8
+ var value: CFTypeRef?
9
+ guard AXUIElementCopyAttributeValue(el, name as CFString, &value) == .success else { return nil }
10
+ return value as AnyObject?
11
+ }
12
+
13
+ func s(_ el: AXUIElement, _ name: String) -> String? {
14
+ if let v = attr(el, name) as? String, !v.isEmpty { return v }
15
+ return nil
16
+ }
17
+
18
+ func children(_ el: AXUIElement) -> [AXUIElement] {
19
+ (attr(el, kAXChildrenAttribute as String) as? [AnyObject] ?? []).map { $0 as! AXUIElement }
20
+ }
21
+
22
+ func collectLists(_ el: AXUIElement, into out: inout [AXUIElement]) {
23
+ let role = s(el, kAXRoleAttribute as String) ?? ""
24
+ if role == kAXListRole as String { out.append(el) }
25
+ for c in children(el) { collectLists(c, into: &out) }
26
+ }
27
+
28
+ func collectTexts(_ el: AXUIElement, into out: inout [String]) {
29
+ let role = s(el, kAXRoleAttribute as String) ?? ""
30
+ if role == kAXStaticTextRole as String {
31
+ if let text = s(el, kAXDescriptionAttribute as String), !text.isEmpty {
32
+ out.append(text)
33
+ }
34
+ }
35
+ for c in children(el) { collectTexts(c, into: &out) }
36
+ }
37
+
38
+ guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.openai.chat").first else {
39
+ fputs("ChatGPT not running\\n", stderr)
40
+ exit(1)
41
+ }
42
+
43
+ let axApp = AXUIElementCreateApplication(app.processIdentifier)
44
+ guard let win = attr(axApp, kAXFocusedWindowAttribute as String) as! AXUIElement? else {
45
+ fputs("No focused ChatGPT window\\n", stderr)
46
+ exit(1)
47
+ }
48
+
49
+ var lists: [AXUIElement] = []
50
+ collectLists(win, into: &lists)
51
+
52
+ var best: [String] = []
53
+ for list in lists {
54
+ var texts: [String] = []
55
+ collectTexts(list, into: &texts)
56
+ if texts.count > best.count {
57
+ best = texts
58
+ }
59
+ }
60
+
61
+ let data = try! JSONSerialization.data(withJSONObject: best, options: [])
62
+ print(String(data: data, encoding: .utf8)!)
63
+ `;
64
+
65
+ export function getVisibleChatMessages(): string[] {
66
+ const output = execFileSync('swift', ['-'], {
67
+ input: AX_READ_SCRIPT,
68
+ encoding: 'utf-8',
69
+ maxBuffer: 10 * 1024 * 1024,
70
+ }).trim();
71
+
72
+ if (!output) return [];
73
+
74
+ const parsed = JSON.parse(output);
75
+ if (!Array.isArray(parsed)) return [];
76
+
77
+ return parsed
78
+ .filter((item): item is string => typeof item === 'string')
79
+ .map((item) => item.replace(/[\uFFFC\u200B-\u200D\uFEFF]/g, '').trim())
80
+ .filter((item) => item.length > 0);
81
+ }
@@ -1,6 +1,7 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { cli, Strategy } from '../../registry.js';
3
3
  import type { IPage } from '../../types.js';
4
+ import { getVisibleChatMessages } from './ax.js';
4
5
 
5
6
  export const readCommand = cli({
6
7
  site: 'chatgpt',
@@ -14,17 +15,14 @@ export const readCommand = cli({
14
15
  func: async (page: IPage | null) => {
15
16
  try {
16
17
  execSync("osascript -e 'tell application \"ChatGPT\" to activate'");
17
- execSync("osascript -e 'delay 0.5'");
18
- execSync("osascript -e 'tell application \"System Events\" to keystroke \"c\" using {command down, shift down}'");
19
18
  execSync("osascript -e 'delay 0.3'");
19
+ const messages = getVisibleChatMessages();
20
20
 
21
- const result = execSync('pbpaste', { encoding: 'utf-8' }).trim();
22
-
23
- if (!result) {
24
- return [{ Role: 'System', Text: 'No text was copied. Is there a response in the chat?' }];
21
+ if (!messages.length) {
22
+ return [{ Role: 'System', Text: 'No visible chat messages were found in the current ChatGPT window.' }];
25
23
  }
26
24
 
27
- return [{ Role: 'Assistant', Text: result }];
25
+ return [{ Role: 'Assistant', Text: messages[messages.length - 1] }];
28
26
  } catch (err: any) {
29
27
  throw new Error("Failed to read from ChatGPT: " + err.message);
30
28
  }
@@ -0,0 +1,141 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+
4
+ interface PaperAuthor {
5
+ name: string;
6
+ }
7
+
8
+ interface DailyPaper {
9
+ paper: {
10
+ id: string;
11
+ upvotes: number;
12
+ authors: PaperAuthor[];
13
+ };
14
+ title: string;
15
+ numComments: number;
16
+ }
17
+
18
+ interface PeriodPaper {
19
+ id: string;
20
+ title: string;
21
+ upvotes: number;
22
+ publishedAt: string;
23
+ authors: PaperAuthor[];
24
+ }
25
+
26
+ function truncate(str: string, max = 60): string {
27
+ return str.length > max ? str.slice(0, max - 3) + '...' : str;
28
+ }
29
+
30
+ function formatAuthors(authors: PaperAuthor[], max = 3): string {
31
+ const names = authors.map((a) => a.name);
32
+ if (names.length <= max) return names.join(', ');
33
+ return names.slice(0, max).join(', ') + ' et al.';
34
+ }
35
+
36
+ const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
37
+
38
+ function getMonthRange(): string {
39
+ const now = new Date();
40
+ return `${MONTH_ABBR[now.getUTCMonth()]} ${now.getUTCFullYear()}`;
41
+ }
42
+
43
+ function getWeekRange(): string {
44
+ const now = new Date();
45
+ const day = now.getUTCDay(); // 0=Sun, 6=Sat
46
+ const daysToSat = day === 6 ? 0 : 6 - day;
47
+ const end = new Date(now);
48
+ end.setUTCDate(now.getUTCDate() + daysToSat);
49
+ const start = new Date(end);
50
+ start.setUTCDate(end.getUTCDate() - 6);
51
+
52
+ const sm = MONTH_ABBR[start.getUTCMonth()];
53
+ const em = MONTH_ABBR[end.getUTCMonth()];
54
+ const sd = start.getUTCDate();
55
+ const ed = end.getUTCDate();
56
+ return sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
57
+ }
58
+
59
+ cli({
60
+ site: 'hf',
61
+ name: 'top',
62
+ description: 'Top upvoted Hugging Face papers',
63
+ domain: 'huggingface.co',
64
+ strategy: Strategy.PUBLIC,
65
+ browser: false,
66
+ args: [
67
+ { name: 'limit', type: 'int', default: 20, help: 'Number of papers' },
68
+ { name: 'all', type: 'bool', default: false, help: 'Return all papers (ignore limit)' },
69
+ { name: 'date', type: 'str', required: false, help: 'Date (YYYY-MM-DD), defaults to most recent' },
70
+ { name: 'period', type: 'str', default: 'daily', choices: ['daily', 'weekly', 'monthly'], help: 'Time period: daily, weekly, or monthly' },
71
+ ],
72
+ footerExtra: (kwargs) => {
73
+ if (kwargs._footerDate) return kwargs._footerDate;
74
+ if (kwargs.period === 'monthly') return getMonthRange();
75
+ if (kwargs.period === 'weekly') return getWeekRange();
76
+ return kwargs.date ?? new Date().toISOString().slice(0, 10);
77
+ },
78
+ func: async (_page, kwargs) => {
79
+ const period = String(kwargs.period ?? 'daily');
80
+ const all = Boolean(kwargs.all);
81
+ const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co';
82
+
83
+ if (period === 'weekly' || period === 'monthly') {
84
+ if (kwargs.date) {
85
+ throw new CliError('INVALID_ARG', `--date is not supported for ${period} period`, `Omit --date when using --period ${period}`);
86
+ }
87
+ const url = `${endpoint}/api/papers?period=${period}`;
88
+ const res = await fetch(url);
89
+ if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
90
+ const body = await res.json();
91
+ if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check endpoint');
92
+ const data: PeriodPaper[] = body;
93
+ const dates = data.map((d) => d.publishedAt).filter(Boolean).sort();
94
+ if (dates.length > 0) {
95
+ if (period === 'monthly') {
96
+ const d = new Date(dates[0]);
97
+ kwargs._footerDate = `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
98
+ } else {
99
+ const start = new Date(dates[0]);
100
+ const end = new Date(dates[dates.length - 1]);
101
+ const sm = MONTH_ABBR[start.getUTCMonth()];
102
+ const em = MONTH_ABBR[end.getUTCMonth()];
103
+ const sd = start.getUTCDate();
104
+ const ed = end.getUTCDate();
105
+ kwargs._footerDate = sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
106
+ }
107
+ }
108
+ const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0));
109
+ const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
110
+ return items.map((item, i) => ({
111
+ rank: i + 1,
112
+ id: item.id ?? '',
113
+ title: truncate(item.title ?? ''),
114
+ upvotes: item.upvotes ?? 0,
115
+ authors: formatAuthors(item.authors ?? []),
116
+ }));
117
+ }
118
+
119
+ // daily
120
+ if (kwargs.date && !/^\d{4}-\d{2}-\d{2}$/.test(String(kwargs.date))) {
121
+ throw new CliError('INVALID_ARG', `Invalid date format: ${kwargs.date}`, 'Use YYYY-MM-DD');
122
+ }
123
+ const url = kwargs.date
124
+ ? `${endpoint}/api/daily_papers?date=${kwargs.date}`
125
+ : `${endpoint}/api/daily_papers`;
126
+ const res = await fetch(url);
127
+ if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
128
+ const body = await res.json();
129
+ if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check date format or endpoint');
130
+ const data: DailyPaper[] = body;
131
+ const sorted = [...data].sort((a, b) => (b.paper?.upvotes ?? 0) - (a.paper?.upvotes ?? 0));
132
+ const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
133
+ return items.map((item, i) => ({
134
+ rank: i + 1,
135
+ id: item.paper?.id ?? '',
136
+ title: truncate(item.title ?? ''),
137
+ upvotes: item.paper?.upvotes ?? 0,
138
+ authors: formatAuthors(item.paper?.authors ?? []),
139
+ }));
140
+ },
141
+ });
@@ -0,0 +1,113 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ /**
4
+ * 评论即刻帖子
5
+ *
6
+ * 帖子详情页有评论输入框(contenteditable 或 textarea),
7
+ * 填入文本后点击"回复"或"发布"按钮提交。
8
+ */
9
+
10
+ cli({
11
+ site: 'jike',
12
+ name: 'comment',
13
+ description: '评论即刻帖子',
14
+ domain: 'web.okjike.com',
15
+ strategy: Strategy.UI,
16
+ browser: true,
17
+ args: [
18
+ { name: 'id', type: 'string', required: true, help: '帖子 ID' },
19
+ { name: 'text', type: 'string', required: true, help: '评论内容' },
20
+ ],
21
+ columns: ['status', 'message'],
22
+ func: async (page, kwargs) => {
23
+ await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`);
24
+ await page.wait(5);
25
+
26
+ // 1. 找到评论输入框并填入文本
27
+ const inputResult = await page.evaluate(`(async () => {
28
+ try {
29
+ const textToInsert = ${JSON.stringify(kwargs.text)};
30
+
31
+ // 优先在评论区容器内找 contenteditable,避免误选页面其他编辑器;
32
+ // 若评论区 class 名变更则回退到全页查找
33
+ const editor =
34
+ document.querySelector('[class*="_comment_"] [contenteditable="true"]') ||
35
+ document.querySelector('[contenteditable="true"]');
36
+ if (editor) {
37
+ editor.focus();
38
+ const dt = new DataTransfer();
39
+ dt.setData('text/plain', textToInsert);
40
+ editor.dispatchEvent(new ClipboardEvent('paste', {
41
+ clipboardData: dt, bubbles: true, cancelable: true,
42
+ }));
43
+ await new Promise(r => setTimeout(r, 800));
44
+ if (editor.textContent?.length > 0) {
45
+ return { ok: true, message: 'contenteditable' };
46
+ }
47
+ }
48
+
49
+ // 回退:textarea(带评论相关 placeholder)
50
+ const textareas = document.querySelectorAll('textarea');
51
+ for (const ta of textareas) {
52
+ const ph = ta.getAttribute('placeholder') || '';
53
+ if (ph.includes('评论') || ph.includes('回复') || ph.includes('说点什么')) {
54
+ ta.focus();
55
+ const setter = Object.getOwnPropertyDescriptor(
56
+ HTMLTextAreaElement.prototype, 'value'
57
+ )?.set;
58
+ setter?.call(ta, textToInsert);
59
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
60
+ await new Promise(r => setTimeout(r, 500));
61
+ return { ok: true, message: 'textarea' };
62
+ }
63
+ }
64
+
65
+ // 兜底:任意 textarea
66
+ if (textareas.length > 0) {
67
+ const ta = textareas[0];
68
+ ta.focus();
69
+ const setter = Object.getOwnPropertyDescriptor(
70
+ HTMLTextAreaElement.prototype, 'value'
71
+ )?.set;
72
+ setter?.call(ta, textToInsert);
73
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
74
+ await new Promise(r => setTimeout(r, 500));
75
+ return { ok: true, message: 'textarea-fallback' };
76
+ }
77
+
78
+ return { ok: false, message: '未找到评论输入框' };
79
+ } catch (e) {
80
+ return { ok: false, message: e.toString() };
81
+ }
82
+ })()`);
83
+
84
+ if (!inputResult.ok) {
85
+ return [{ status: 'failed', message: inputResult.message }];
86
+ }
87
+
88
+ // 2. 点击"回复"或"发布"按钮
89
+ const submitResult = await page.evaluate(`(async () => {
90
+ try {
91
+ await new Promise(r => setTimeout(r, 500));
92
+ const btns = Array.from(document.querySelectorAll('button')).filter(btn => {
93
+ const text = btn.textContent?.trim() || '';
94
+ return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled;
95
+ });
96
+ if (btns.length === 0) {
97
+ return { ok: false, message: '未找到可用的回复按钮(可能因内容为空而禁用)' };
98
+ }
99
+ btns[0].click();
100
+ return { ok: true, message: '评论发布成功' };
101
+ } catch (e) {
102
+ return { ok: false, message: e.toString() };
103
+ }
104
+ })()`);
105
+
106
+ if (submitResult.ok) await page.wait(3);
107
+
108
+ return [{
109
+ status: submitResult.ok ? 'success' : 'failed',
110
+ message: submitResult.message,
111
+ }];
112
+ },
113
+ });
@@ -0,0 +1,113 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ /**
4
+ * 发布即刻动态
5
+ *
6
+ * 即刻首页 /following 顶部有内联发帖框("分享你的想法..."),
7
+ * 直接在其中输入文本,点击"发送"按钮即可发布。
8
+ */
9
+
10
+ cli({
11
+ site: 'jike',
12
+ name: 'create',
13
+ description: '发布即刻动态',
14
+ domain: 'web.okjike.com',
15
+ strategy: Strategy.UI,
16
+ browser: true,
17
+ args: [
18
+ { name: 'text', type: 'string', required: true, help: '动态正文内容' },
19
+ ],
20
+ columns: ['status', 'message'],
21
+ func: async (page, kwargs) => {
22
+ // 1. 导航到首页(有内联发帖框)
23
+ await page.goto('https://web.okjike.com');
24
+ await page.wait(5);
25
+
26
+ // 2. 在发帖框中输入文本
27
+ const textResult = await page.evaluate(`(async () => {
28
+ try {
29
+ const textToInsert = ${JSON.stringify(kwargs.text)};
30
+
31
+ // 首页发帖框在 _postForm_ 容器内,查找其中的 contenteditable
32
+ const form = document.querySelector('[class*="_postForm_"]');
33
+ const editor = form
34
+ ? form.querySelector('[contenteditable="true"]')
35
+ : document.querySelector('[contenteditable="true"]');
36
+
37
+ if (editor) {
38
+ editor.focus();
39
+ // 用 ClipboardEvent paste 触发 React 状态更新
40
+ const dt = new DataTransfer();
41
+ dt.setData('text/plain', textToInsert);
42
+ editor.dispatchEvent(new ClipboardEvent('paste', {
43
+ clipboardData: dt, bubbles: true, cancelable: true,
44
+ }));
45
+ await new Promise(r => setTimeout(r, 800));
46
+
47
+ // 检查是否成功插入
48
+ const inserted = editor.textContent || '';
49
+ if (inserted.length > 0) {
50
+ return { ok: true, message: 'contenteditable' };
51
+ }
52
+ }
53
+
54
+ // 回退:textarea
55
+ const textarea = form
56
+ ? form.querySelector('textarea')
57
+ : document.querySelector('textarea');
58
+
59
+ if (textarea) {
60
+ textarea.focus();
61
+ const setter = Object.getOwnPropertyDescriptor(
62
+ HTMLTextAreaElement.prototype, 'value'
63
+ )?.set;
64
+ setter?.call(textarea, textToInsert);
65
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
66
+ await new Promise(r => setTimeout(r, 500));
67
+ return { ok: true, message: 'textarea' };
68
+ }
69
+
70
+ return { ok: false, message: '未找到发帖输入框' };
71
+ } catch (e) {
72
+ return { ok: false, message: e.toString() };
73
+ }
74
+ })()`);
75
+
76
+ if (!textResult.ok) {
77
+ return [{ status: 'failed', message: textResult.message }];
78
+ }
79
+
80
+ // 3. 点击"发送"按钮
81
+ const submitResult = await page.evaluate(`(async () => {
82
+ try {
83
+ await new Promise(r => setTimeout(r, 500));
84
+
85
+ // 即刻首页发帖框的按钮文字为"发送"
86
+ const candidates = [
87
+ ...Array.from(document.querySelectorAll('button')).filter(btn => {
88
+ const text = btn.textContent?.trim() || '';
89
+ return text === '发送' || text === '发布';
90
+ }),
91
+ ].filter(el => el && !el.disabled);
92
+
93
+ if (candidates.length === 0) {
94
+ return { ok: false, message: '未找到可用的发送按钮(按钮可能因内容为空而禁用)' };
95
+ }
96
+
97
+ candidates[0].click();
98
+ return { ok: true, message: '动态发布成功' };
99
+ } catch (e) {
100
+ return { ok: false, message: e.toString() };
101
+ }
102
+ })()`);
103
+
104
+ if (submitResult.ok) {
105
+ await page.wait(3);
106
+ }
107
+
108
+ return [{
109
+ status: submitResult.ok ? 'success' : 'failed',
110
+ message: submitResult.message,
111
+ }];
112
+ },
113
+ });