@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,73 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ cli({
3
+ site: 'boss',
4
+ name: 'chatmsg',
5
+ description: 'BOSS直聘查看与候选人的聊天消息',
6
+ domain: 'www.zhipin.com',
7
+ strategy: Strategy.COOKIE,
8
+ browser: true,
9
+ args: [
10
+ { name: 'uid', required: true, help: 'Encrypted UID (from chatlist)' },
11
+ { name: 'page', type: 'int', default: 1, help: 'Page number' },
12
+ ],
13
+ columns: ['from', 'type', 'text', 'time'],
14
+ func: async (page, kwargs) => {
15
+ if (!page)
16
+ throw new Error('Browser page required');
17
+ await page.goto('https://www.zhipin.com/web/chat/index');
18
+ await page.wait({ time: 2 });
19
+ const uid = kwargs.uid;
20
+ const friendData = await page.evaluate(`
21
+ async () => {
22
+ return new Promise((resolve, reject) => {
23
+ const xhr = new XMLHttpRequest();
24
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
25
+ xhr.withCredentials = true;
26
+ xhr.timeout = 15000;
27
+ xhr.setRequestHeader('Accept', 'application/json');
28
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
29
+ xhr.onerror = () => reject(new Error('Network Error'));
30
+ xhr.send();
31
+ });
32
+ }
33
+ `);
34
+ if (friendData.code !== 0)
35
+ throw new Error('获取好友列表失败');
36
+ const friend = (friendData.zpData?.friendList || []).find((f) => f.encryptUid === uid);
37
+ if (!friend)
38
+ throw new Error('未找到该候选人');
39
+ const gid = friend.uid;
40
+ const securityId = encodeURIComponent(friend.securityId);
41
+ const msgUrl = `https://www.zhipin.com/wapi/zpchat/boss/historyMsg?gid=${gid}&securityId=${securityId}&page=${kwargs.page}&c=20&src=0`;
42
+ const msgData = await page.evaluate(`
43
+ async () => {
44
+ return new Promise((resolve, reject) => {
45
+ const xhr = new XMLHttpRequest();
46
+ xhr.open('GET', '${msgUrl}', true);
47
+ xhr.withCredentials = true;
48
+ xhr.timeout = 15000;
49
+ xhr.setRequestHeader('Accept', 'application/json');
50
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { resolve({raw: xhr.responseText.substring(0,500)}); } };
51
+ xhr.onerror = () => reject(new Error('Network Error'));
52
+ xhr.send();
53
+ });
54
+ }
55
+ `);
56
+ if (msgData.raw)
57
+ throw new Error('Non-JSON: ' + msgData.raw);
58
+ if (msgData.code !== 0)
59
+ throw new Error('API error: ' + (msgData.message || msgData.code));
60
+ const TYPE_MAP = { 1: '文本', 2: '图片', 3: '招呼', 4: '简历', 5: '系统', 6: '名片', 7: '语音', 8: '视频', 9: '表情' };
61
+ const messages = msgData.zpData?.messages || msgData.zpData?.historyMsgList || [];
62
+ return messages.map((m) => {
63
+ const fromObj = m.from || {};
64
+ const isSelf = typeof fromObj === 'object' ? fromObj.uid !== friend.uid : false;
65
+ return {
66
+ from: isSelf ? '我' : (typeof fromObj === 'object' ? fromObj.name : friend.name),
67
+ type: TYPE_MAP[m.type] || '其他(' + m.type + ')',
68
+ text: m.text || m.body?.text || '',
69
+ time: m.time ? new Date(m.time).toLocaleString('zh-CN') : '',
70
+ };
71
+ });
72
+ },
73
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,249 @@
1
+ /**
2
+ * BOSS直聘 resume — view candidate resume/profile via chat page UI scraping (boss side).
3
+ *
4
+ * Flow: navigate to chat page → click on candidate → scrape the right panel info.
5
+ * The chat page loads candidate basic info, work experience, and education
6
+ * in the right panel when a candidate is selected.
7
+ *
8
+ * HTML structure (right panel):
9
+ * .base-info-single-detial → name, gender, age, experience, degree
10
+ * .experience-content.time-list → time ranges (icon-base-info-work / icon-base-info-edu)
11
+ * .experience-content.detail-list → details (company·position / school·major·degree)
12
+ * .position-content → job being discussed + expectation
13
+ */
14
+ import { cli, Strategy } from '../../registry.js';
15
+ cli({
16
+ site: 'boss',
17
+ name: 'resume',
18
+ description: 'BOSS直聘查看候选人简历(招聘端)',
19
+ domain: 'www.zhipin.com',
20
+ strategy: Strategy.COOKIE,
21
+ browser: true,
22
+ args: [
23
+ { name: 'uid', required: true, help: 'Encrypted UID of the candidate (from chatlist)' },
24
+ ],
25
+ columns: [
26
+ 'name', 'gender', 'age', 'experience', 'degree', 'active_time',
27
+ 'work_history', 'education',
28
+ 'job_chatting', 'expect',
29
+ ],
30
+ func: async (page, kwargs) => {
31
+ if (!page)
32
+ throw new Error('Browser page required');
33
+ const uid = kwargs.uid;
34
+ // Step 1: Navigate to chat page
35
+ await page.goto('https://www.zhipin.com/web/chat/index');
36
+ await page.wait({ time: 3 });
37
+ // Step 2: Get friend list to find candidate's numeric uid
38
+ const friendData = await page.evaluate(`
39
+ async () => {
40
+ return new Promise((resolve, reject) => {
41
+ const xhr = new XMLHttpRequest();
42
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
43
+ xhr.withCredentials = true;
44
+ xhr.timeout = 15000;
45
+ xhr.setRequestHeader('Accept', 'application/json');
46
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
47
+ xhr.onerror = () => reject(new Error('Network Error'));
48
+ xhr.send();
49
+ });
50
+ }
51
+ `);
52
+ if (friendData.code !== 0) {
53
+ if (friendData.code === 7 || friendData.code === 37) {
54
+ throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
55
+ }
56
+ throw new Error('获取好友列表失败: ' + (friendData.message || friendData.code));
57
+ }
58
+ let friend = null;
59
+ const allFriends = friendData.zpData?.friendList || [];
60
+ friend = allFriends.find((f) => f.encryptUid === uid);
61
+ if (!friend) {
62
+ for (let p = 2; p <= 5; p++) {
63
+ const moreUrl = `https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=${p}&status=0&jobId=0`;
64
+ const moreData = await page.evaluate(`
65
+ async () => {
66
+ return new Promise((resolve, reject) => {
67
+ const xhr = new XMLHttpRequest();
68
+ xhr.open('GET', '${moreUrl}', true);
69
+ xhr.withCredentials = true;
70
+ xhr.timeout = 15000;
71
+ xhr.setRequestHeader('Accept', 'application/json');
72
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
73
+ xhr.onerror = () => reject(new Error('Network Error'));
74
+ xhr.send();
75
+ });
76
+ }
77
+ `);
78
+ if (moreData.code === 0) {
79
+ const list = moreData.zpData?.friendList || [];
80
+ friend = list.find((f) => f.encryptUid === uid);
81
+ if (friend)
82
+ break;
83
+ if (list.length === 0)
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ if (!friend)
89
+ throw new Error('未找到该候选人,请确认 uid 是否正确');
90
+ const numericUid = friend.uid;
91
+ // Step 3: Click on candidate in chat list
92
+ const clicked = await page.evaluate(`
93
+ async () => {
94
+ const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
95
+ if (item) {
96
+ item.click();
97
+ return { clicked: true };
98
+ }
99
+ const items = document.querySelectorAll('.geek-item');
100
+ for (const el of items) {
101
+ if (el.id && el.id.startsWith('_${numericUid}')) {
102
+ el.click();
103
+ return { clicked: true };
104
+ }
105
+ }
106
+ return { clicked: false };
107
+ }
108
+ `);
109
+ if (!clicked.clicked) {
110
+ throw new Error('无法在聊天列表中找到该用户,请确认聊天列表中有此人');
111
+ }
112
+ // Step 4: Wait for right panel to load
113
+ await page.wait({ time: 2 });
114
+ // Step 5: Scrape the right panel
115
+ const resumeInfo = await page.evaluate(`
116
+ (() => {
117
+ const container = document.querySelector('.base-info-single-container') || document.querySelector('.base-info-content');
118
+ if (!container) return { error: 'no container found' };
119
+
120
+ // === Basic Info ===
121
+ const nameEl = container.querySelector('.base-name');
122
+ const name = nameEl ? nameEl.textContent.trim() : '';
123
+
124
+ // Gender
125
+ let gender = '';
126
+ const detailDiv = container.querySelector('.base-info-single-detial');
127
+ if (detailDiv) {
128
+ const uses = detailDiv.querySelectorAll('use');
129
+ for (const u of uses) {
130
+ const href = u.getAttribute('xlink:href') || u.getAttribute('href') || '';
131
+ if (href.includes('icon-men')) { gender = '男'; break; }
132
+ if (href.includes('icon-women')) { gender = '女'; break; }
133
+ }
134
+ }
135
+
136
+ // Active time
137
+ const activeEl = container.querySelector('.active-time');
138
+ const activeTime = activeEl ? activeEl.textContent.trim() : '';
139
+
140
+ // Age, experience, degree — direct child divs of .base-info-single-detial
141
+ let age = '', experience = '', degree = '';
142
+ if (detailDiv) {
143
+ for (const el of detailDiv.children) {
144
+ if (el.classList.contains('name-contet') || el.classList.contains('high-light-orange') ||
145
+ el.classList.contains('resume-btn-content') || el.classList.contains('label-remark-content') ||
146
+ el.classList.contains('base-info-item')) continue;
147
+ const text = el.textContent.trim();
148
+ if (!text) continue;
149
+ if (text.match(/\\d+岁/)) age = text;
150
+ else if (text.match(/年|经验|应届/)) experience = text;
151
+ else if (['博士', '硕士', '本科', '大专', '高中', '中专', '中技', '初中'].some(d => text.includes(d))) degree = text;
152
+ }
153
+ }
154
+
155
+ // === Work & Education ===
156
+ // Structure: two .experience-content divs
157
+ // 1. .time-list → <li> items with icon (work/edu) and time span
158
+ // 2. .detail-list → <li> items with icon (work/edu) and detail text
159
+ // Each <li> has a <use> with xlink:href "#icon-base-info-work" or "#icon-base-info-edu"
160
+
161
+ const workTimes = [];
162
+ const eduTimes = [];
163
+ const workDetails = [];
164
+ const eduDetails = [];
165
+
166
+ const timeList = container.querySelector('.experience-content.time-list');
167
+ if (timeList) {
168
+ const lis = timeList.querySelectorAll('li');
169
+ for (const li of lis) {
170
+ const useEl = li.querySelector('use');
171
+ const href = useEl ? (useEl.getAttribute('xlink:href') || useEl.getAttribute('href') || '') : '';
172
+ const timeSpan = li.querySelector('.time');
173
+ const timeText = timeSpan ? timeSpan.textContent.trim() : li.textContent.trim();
174
+ if (href.includes('base-info-edu')) {
175
+ eduTimes.push(timeText);
176
+ } else {
177
+ workTimes.push(timeText);
178
+ }
179
+ }
180
+ }
181
+
182
+ const detailList = container.querySelector('.experience-content.detail-list');
183
+ if (detailList) {
184
+ const lis = detailList.querySelectorAll('li');
185
+ for (const li of lis) {
186
+ const useEl = li.querySelector('use');
187
+ const href = useEl ? (useEl.getAttribute('xlink:href') || useEl.getAttribute('href') || '') : '';
188
+ const valueSpan = li.querySelector('.value');
189
+ const valueText = valueSpan ? valueSpan.textContent.trim() : li.textContent.trim();
190
+ if (href.includes('base-info-edu')) {
191
+ eduDetails.push(valueText);
192
+ } else {
193
+ workDetails.push(valueText);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Combine times and details
199
+ const workHistory = [];
200
+ for (let i = 0; i < Math.max(workTimes.length, workDetails.length); i++) {
201
+ const parts = [];
202
+ if (workTimes[i]) parts.push(workTimes[i]);
203
+ if (workDetails[i]) parts.push(workDetails[i]);
204
+ if (parts.length) workHistory.push(parts.join(' '));
205
+ }
206
+
207
+ const education = [];
208
+ for (let i = 0; i < Math.max(eduTimes.length, eduDetails.length); i++) {
209
+ const parts = [];
210
+ if (eduTimes[i]) parts.push(eduTimes[i]);
211
+ if (eduDetails[i]) parts.push(eduDetails[i]);
212
+ if (parts.length) education.push(parts.join(' '));
213
+ }
214
+
215
+ // === Job Chatting & Expect ===
216
+ const positionContent = container.querySelector('.position-content');
217
+ let jobChatting = '', expect = '';
218
+ if (positionContent) {
219
+ const posNameEl = positionContent.querySelector('.position-name');
220
+ if (posNameEl) jobChatting = posNameEl.textContent.trim();
221
+
222
+ const expectEl = positionContent.querySelector('.position-item.expect .value');
223
+ if (expectEl) expect = expectEl.textContent.trim();
224
+ }
225
+
226
+ return {
227
+ name, gender, age, experience, degree, activeTime,
228
+ workHistory, education,
229
+ jobChatting, expect,
230
+ };
231
+ })()
232
+ `);
233
+ if (resumeInfo.error) {
234
+ throw new Error('无法获取简历面板: ' + resumeInfo.error);
235
+ }
236
+ return [{
237
+ name: resumeInfo.name || friend.name || '',
238
+ gender: resumeInfo.gender || '',
239
+ age: resumeInfo.age || '',
240
+ experience: resumeInfo.experience || '',
241
+ degree: resumeInfo.degree || '',
242
+ active_time: resumeInfo.activeTime || '',
243
+ work_history: (resumeInfo.workHistory || []).join('\\n') || '(未获取到)',
244
+ education: (resumeInfo.education || []).join('\\n') || '(未获取到)',
245
+ job_chatting: resumeInfo.jobChatting || '',
246
+ expect: resumeInfo.expect || '',
247
+ }];
248
+ },
249
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,176 @@
1
+ /**
2
+ * BOSS直聘 send message — via UI automation on chat page.
3
+ *
4
+ * Flow: navigate to chat → click on user in list → type in editor → send.
5
+ * BOSS chat uses MQTT (not HTTP) for messaging, so we must go through the UI.
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ cli({
9
+ site: 'boss',
10
+ name: 'send',
11
+ description: 'BOSS直聘发送聊天消息',
12
+ domain: 'www.zhipin.com',
13
+ strategy: Strategy.COOKIE,
14
+ browser: true,
15
+ args: [
16
+ { name: 'uid', required: true, help: 'Encrypted UID of the candidate (from chatlist)' },
17
+ { name: 'text', required: true, help: 'Message text to send' },
18
+ ],
19
+ columns: ['status', 'detail'],
20
+ func: async (page, kwargs) => {
21
+ if (!page)
22
+ throw new Error('Browser page required');
23
+ const uid = kwargs.uid;
24
+ const text = kwargs.text;
25
+ // Step 1: Navigate to chat page
26
+ await page.goto('https://www.zhipin.com/web/chat/index');
27
+ await page.wait({ time: 3 });
28
+ // Step 2: Find friend in list to get their numeric uid, then click
29
+ const friendData = await page.evaluate(`
30
+ async () => {
31
+ return new Promise((resolve, reject) => {
32
+ const xhr = new XMLHttpRequest();
33
+ xhr.open('GET', 'https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=1&status=0&jobId=0', true);
34
+ xhr.withCredentials = true;
35
+ xhr.timeout = 15000;
36
+ xhr.setRequestHeader('Accept', 'application/json');
37
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
38
+ xhr.onerror = () => reject(new Error('Network Error'));
39
+ xhr.send();
40
+ });
41
+ }
42
+ `);
43
+ if (friendData.code !== 0) {
44
+ if (friendData.code === 7 || friendData.code === 37) {
45
+ throw new Error('Cookie 已过期!请在当前 Chrome 浏览器中重新登录 BOSS 直聘。');
46
+ }
47
+ throw new Error('获取好友列表失败: ' + (friendData.message || friendData.code));
48
+ }
49
+ let target = null;
50
+ const allFriends = friendData.zpData?.friendList || [];
51
+ target = allFriends.find((f) => f.encryptUid === uid);
52
+ if (!target) {
53
+ for (let p = 2; p <= 5; p++) {
54
+ const moreUrl = `https://www.zhipin.com/wapi/zprelation/friend/getBossFriendListV2.json?page=${p}&status=0&jobId=0`;
55
+ const moreData = await page.evaluate(`
56
+ async () => {
57
+ return new Promise((resolve, reject) => {
58
+ const xhr = new XMLHttpRequest();
59
+ xhr.open('GET', '${moreUrl}', true);
60
+ xhr.withCredentials = true;
61
+ xhr.timeout = 15000;
62
+ xhr.setRequestHeader('Accept', 'application/json');
63
+ xhr.onload = () => { try { resolve(JSON.parse(xhr.responseText)); } catch(e) { reject(e); } };
64
+ xhr.onerror = () => reject(new Error('Network Error'));
65
+ xhr.send();
66
+ });
67
+ }
68
+ `);
69
+ if (moreData.code === 0) {
70
+ const list = moreData.zpData?.friendList || [];
71
+ target = list.find((f) => f.encryptUid === uid);
72
+ if (target)
73
+ break;
74
+ if (list.length === 0)
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ if (!target)
80
+ throw new Error('未找到该候选人,请确认 uid 是否正确');
81
+ const numericUid = target.uid;
82
+ const friendName = target.name || '候选人';
83
+ // Step 3: Click on the user in the chat list to open conversation
84
+ const clicked = await page.evaluate(`
85
+ async () => {
86
+ // The geek-item has id like _748787762-0
87
+ const item = document.querySelector('#_${numericUid}-0') || document.querySelector('[id^="_${numericUid}"]');
88
+ if (item) {
89
+ item.click();
90
+ return { clicked: true, id: item.id };
91
+ }
92
+ // Fallback: try clicking by iterating geek items
93
+ const items = document.querySelectorAll('.geek-item');
94
+ for (const el of items) {
95
+ if (el.id && el.id.startsWith('_${numericUid}')) {
96
+ el.click();
97
+ return { clicked: true, id: el.id };
98
+ }
99
+ }
100
+ return { clicked: false };
101
+ }
102
+ `);
103
+ if (!clicked.clicked) {
104
+ throw new Error('无法在聊天列表中找到该用户,请确认聊天列表中有此人');
105
+ }
106
+ // Step 4: Wait for the conversation to load and input area to appear
107
+ await page.wait({ time: 2 });
108
+ // Step 5: Find the message editor and type
109
+ const typed = await page.evaluate(`
110
+ async () => {
111
+ // Look for the chat editor - BOSS uses contenteditable div or textarea
112
+ const selectors = [
113
+ '.chat-editor [contenteditable="true"]',
114
+ '.chat-input [contenteditable="true"]',
115
+ '.message-editor [contenteditable="true"]',
116
+ '.chat-conversation [contenteditable="true"]',
117
+ '[contenteditable="true"]',
118
+ '.chat-editor textarea',
119
+ '.chat-input textarea',
120
+ 'textarea',
121
+ ];
122
+
123
+ for (const sel of selectors) {
124
+ const el = document.querySelector(sel);
125
+ if (el && el.offsetParent !== null) {
126
+ el.focus();
127
+
128
+ if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
129
+ el.value = ${JSON.stringify(text)};
130
+ el.dispatchEvent(new Event('input', { bubbles: true }));
131
+ el.dispatchEvent(new Event('change', { bubbles: true }));
132
+ } else {
133
+ // contenteditable
134
+ el.textContent = '';
135
+ el.focus();
136
+ document.execCommand('insertText', false, ${JSON.stringify(text)});
137
+ el.dispatchEvent(new Event('input', { bubbles: true }));
138
+ }
139
+
140
+ return { found: true, selector: sel, tag: el.tagName };
141
+ }
142
+ }
143
+
144
+ // Debug: list all visible elements in chat-conversation
145
+ const conv = document.querySelector('.chat-conversation');
146
+ const allEls = conv ? Array.from(conv.querySelectorAll('*')).filter(e => e.offsetParent !== null).map(e => e.tagName + '.' + (e.className?.substring?.(0, 50) || '')).slice(0, 30) : [];
147
+
148
+ return { found: false, visibleElements: allEls };
149
+ }
150
+ `);
151
+ if (!typed.found) {
152
+ throw new Error('找不到消息输入框。可能的元素: ' + JSON.stringify(typed.visibleElements || []));
153
+ }
154
+ await page.wait({ time: 0.5 });
155
+ // Step 6: Click the send button (Enter key doesn't trigger send on BOSS)
156
+ const sent = await page.evaluate(`
157
+ async () => {
158
+ // The send button is .submit inside .submit-content
159
+ const btn = document.querySelector('.conversation-editor .submit')
160
+ || document.querySelector('.submit-content .submit')
161
+ || document.querySelector('.conversation-operate .submit');
162
+ if (btn) {
163
+ btn.click();
164
+ return { clicked: true };
165
+ }
166
+ return { clicked: false };
167
+ }
168
+ `);
169
+ if (!sent.clicked) {
170
+ // Fallback: try Enter key
171
+ await page.pressKey('Enter');
172
+ }
173
+ await page.wait({ time: 1 });
174
+ return [{ status: '✅ 发送成功', detail: `已向 ${friendName} 发送: ${text}` }];
175
+ },
176
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { getCourses, initSession, enterCourse, getTabIframeUrl, parseAssignmentsFromDom, sleep, } from '../../chaoxing.js';
3
+ cli({
4
+ site: 'chaoxing',
5
+ name: 'assignments',
6
+ description: '学习通作业列表',
7
+ domain: 'mooc2-ans.chaoxing.com',
8
+ strategy: Strategy.COOKIE,
9
+ timeoutSeconds: 90,
10
+ args: [
11
+ { name: 'course', type: 'string', help: '按课程名过滤(模糊匹配)' },
12
+ {
13
+ name: 'status',
14
+ type: 'string',
15
+ default: 'all',
16
+ choices: ['all', 'pending', 'submitted', 'graded'],
17
+ help: '按状态过滤',
18
+ },
19
+ { name: 'limit', type: 'int', default: 20, help: '最大返回数量' },
20
+ ],
21
+ columns: ['rank', 'course', 'title', 'deadline', 'status', 'score'],
22
+ func: async (page, kwargs) => {
23
+ const { course: courseFilter, status: statusFilter = 'all', limit = 20 } = kwargs;
24
+ // 1. Establish session
25
+ await initSession(page);
26
+ // 2. Get courses
27
+ const courses = await getCourses(page);
28
+ if (!courses.length)
29
+ throw new Error('未获取到课程列表,请确认已登录学习通');
30
+ const filtered = courseFilter
31
+ ? courses.filter(c => c.title.includes(courseFilter))
32
+ : courses;
33
+ if (courseFilter && !filtered.length) {
34
+ throw new Error(`未找到匹配「${courseFilter}」的课程`);
35
+ }
36
+ // 3. Per-course: enter → click 作业 tab → navigate to iframe → parse
37
+ const allRows = [];
38
+ for (const c of filtered) {
39
+ try {
40
+ await enterCourse(page, c);
41
+ const iframeUrl = await getTabIframeUrl(page, '作业');
42
+ if (!iframeUrl)
43
+ continue;
44
+ await page.goto(iframeUrl);
45
+ await page.wait(2);
46
+ const rows = await parseAssignmentsFromDom(page, c.title);
47
+ allRows.push(...rows);
48
+ }
49
+ catch {
50
+ // Single course failure: skip, continue
51
+ }
52
+ if (filtered.length > 1)
53
+ await sleep(600);
54
+ }
55
+ // 4. Sort: pending first, then by deadline
56
+ allRows.sort((a, b) => {
57
+ const order = (s) => s === '未交' ? 0 : s === '待批阅' ? 1 : s === '已完成' ? 2 : s === '已批阅' ? 3 : 4;
58
+ return order(a.status) - order(b.status);
59
+ });
60
+ // 5. Filter by status
61
+ const statusMap = {
62
+ pending: ['未交'],
63
+ submitted: ['待批阅', '已完成'],
64
+ graded: ['已批阅'],
65
+ };
66
+ const finalRows = statusFilter === 'all'
67
+ ? allRows
68
+ : allRows.filter(r => statusMap[statusFilter]?.includes(r.status));
69
+ return finalRows.slice(0, Number(limit)).map((item, i) => ({
70
+ rank: i + 1,
71
+ ...item,
72
+ }));
73
+ },
74
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { getCourses, initSession, enterCourse, getTabIframeUrl, parseExamsFromDom, sleep, } from '../../chaoxing.js';
3
+ cli({
4
+ site: 'chaoxing',
5
+ name: 'exams',
6
+ description: '学习通考试列表',
7
+ domain: 'mooc2-ans.chaoxing.com',
8
+ strategy: Strategy.COOKIE,
9
+ timeoutSeconds: 90,
10
+ args: [
11
+ { name: 'course', type: 'string', help: '按课程名过滤(模糊匹配)' },
12
+ {
13
+ name: 'status',
14
+ type: 'string',
15
+ default: 'all',
16
+ choices: ['all', 'upcoming', 'ongoing', 'finished'],
17
+ help: '按状态过滤',
18
+ },
19
+ { name: 'limit', type: 'int', default: 20, help: '最大返回数量' },
20
+ ],
21
+ columns: ['rank', 'course', 'title', 'start', 'end', 'status', 'score'],
22
+ func: async (page, kwargs) => {
23
+ const { course: courseFilter, status: statusFilter = 'all', limit = 20 } = kwargs;
24
+ // 1. Establish session
25
+ await initSession(page);
26
+ // 2. Get courses
27
+ const courses = await getCourses(page);
28
+ if (!courses.length)
29
+ throw new Error('未获取到课程列表,请确认已登录学习通');
30
+ const filtered = courseFilter
31
+ ? courses.filter(c => c.title.includes(courseFilter))
32
+ : courses;
33
+ if (courseFilter && !filtered.length) {
34
+ throw new Error(`未找到匹配「${courseFilter}」的课程`);
35
+ }
36
+ // 3. Per-course: enter → click 考试 tab → navigate to iframe → parse
37
+ const allRows = [];
38
+ for (const c of filtered) {
39
+ try {
40
+ await enterCourse(page, c);
41
+ const iframeUrl = await getTabIframeUrl(page, '考试');
42
+ if (!iframeUrl)
43
+ continue;
44
+ await page.goto(iframeUrl);
45
+ await page.wait(2);
46
+ const rows = await parseExamsFromDom(page, c.title);
47
+ allRows.push(...rows);
48
+ }
49
+ catch {
50
+ // Single course failure: skip, continue
51
+ }
52
+ if (filtered.length > 1)
53
+ await sleep(600);
54
+ }
55
+ // 4. Sort: upcoming first
56
+ allRows.sort((a, b) => {
57
+ const order = (s) => s === '未开始' ? 0 : s === '进行中' ? 1 : s === '已结束' ? 2 : s === '已完成' ? 3 : 4;
58
+ return order(a.status) - order(b.status);
59
+ });
60
+ // 5. Filter by status
61
+ const statusMap = {
62
+ upcoming: ['未开始'],
63
+ ongoing: ['进行中'],
64
+ finished: ['已结束', '已完成'],
65
+ };
66
+ const finalRows = statusFilter === 'all'
67
+ ? allRows
68
+ : allRows.filter(r => statusMap[statusFilter]?.includes(r.status));
69
+ return finalRows.slice(0, Number(limit)).map((item, i) => ({
70
+ rank: i + 1,
71
+ ...item,
72
+ }));
73
+ },
74
+ });