@jackwener/opencli 1.5.4 → 1.5.6

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 (256) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1284 -67
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/band/bands.d.ts +1 -0
  10. package/dist/clis/band/bands.js +72 -0
  11. package/dist/clis/band/mentions.d.ts +1 -0
  12. package/dist/clis/band/mentions.js +127 -0
  13. package/dist/clis/band/post.d.ts +1 -0
  14. package/dist/clis/band/post.js +175 -0
  15. package/dist/clis/band/posts.d.ts +1 -0
  16. package/dist/clis/band/posts.js +94 -0
  17. package/dist/clis/doubao/detail.d.ts +1 -0
  18. package/dist/clis/doubao/detail.js +33 -0
  19. package/dist/clis/doubao/detail.test.d.ts +1 -0
  20. package/dist/clis/doubao/detail.test.js +42 -0
  21. package/dist/clis/doubao/history.d.ts +1 -0
  22. package/dist/clis/doubao/history.js +28 -0
  23. package/dist/clis/doubao/history.test.d.ts +1 -0
  24. package/dist/clis/doubao/history.test.js +37 -0
  25. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-summary.js +39 -0
  27. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  28. package/dist/clis/doubao/meeting-transcript.js +36 -0
  29. package/dist/clis/doubao/utils.d.ts +27 -0
  30. package/dist/clis/doubao/utils.js +317 -0
  31. package/dist/clis/doubao/utils.test.d.ts +1 -0
  32. package/dist/clis/doubao/utils.test.js +24 -0
  33. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  34. package/dist/clis/douyin/_shared/public-api.js +29 -0
  35. package/dist/clis/douyin/user-videos.d.ts +5 -0
  36. package/dist/clis/douyin/user-videos.js +74 -0
  37. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  38. package/dist/clis/douyin/user-videos.test.js +108 -0
  39. package/dist/clis/ones/common.d.ts +32 -0
  40. package/dist/clis/ones/common.js +144 -0
  41. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  42. package/dist/clis/ones/enrich-tasks.js +37 -0
  43. package/dist/clis/ones/login.d.ts +1 -0
  44. package/dist/clis/ones/login.js +80 -0
  45. package/dist/clis/ones/logout.d.ts +1 -0
  46. package/dist/clis/ones/logout.js +17 -0
  47. package/dist/clis/ones/me.d.ts +1 -0
  48. package/dist/clis/ones/me.js +30 -0
  49. package/dist/clis/ones/my-tasks.d.ts +1 -0
  50. package/dist/clis/ones/my-tasks.js +120 -0
  51. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  52. package/dist/clis/ones/resolve-labels.js +64 -0
  53. package/dist/clis/ones/task-helpers.d.ts +29 -0
  54. package/dist/clis/ones/task-helpers.js +212 -0
  55. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  56. package/dist/clis/ones/task-helpers.test.js +12 -0
  57. package/dist/clis/ones/task.d.ts +1 -0
  58. package/dist/clis/ones/task.js +66 -0
  59. package/dist/clis/ones/tasks.d.ts +1 -0
  60. package/dist/clis/ones/tasks.js +79 -0
  61. package/dist/clis/ones/token-info.d.ts +1 -0
  62. package/dist/clis/ones/token-info.js +42 -0
  63. package/dist/clis/ones/worklog.d.ts +11 -0
  64. package/dist/clis/ones/worklog.js +267 -0
  65. package/dist/clis/ones/worklog.test.d.ts +1 -0
  66. package/dist/clis/ones/worklog.test.js +20 -0
  67. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  68. package/dist/clis/sinafinance/rolling-news.js +40 -0
  69. package/dist/clis/sinafinance/stock.d.ts +8 -0
  70. package/dist/clis/sinafinance/stock.js +117 -0
  71. package/dist/clis/spotify/spotify.d.ts +1 -0
  72. package/dist/clis/spotify/spotify.js +316 -0
  73. package/dist/clis/spotify/utils.d.ts +21 -0
  74. package/dist/clis/spotify/utils.js +66 -0
  75. package/dist/clis/spotify/utils.test.d.ts +1 -0
  76. package/dist/clis/spotify/utils.test.js +67 -0
  77. package/dist/clis/tieba/commands.test.d.ts +4 -0
  78. package/dist/clis/tieba/commands.test.js +79 -0
  79. package/dist/clis/tieba/hot.d.ts +1 -0
  80. package/dist/clis/tieba/hot.js +48 -0
  81. package/dist/clis/tieba/posts.d.ts +1 -0
  82. package/dist/clis/tieba/posts.js +85 -0
  83. package/dist/clis/tieba/read.d.ts +1 -0
  84. package/dist/clis/tieba/read.js +140 -0
  85. package/dist/clis/tieba/search.d.ts +1 -0
  86. package/dist/clis/tieba/search.js +108 -0
  87. package/dist/clis/tieba/utils.d.ts +101 -0
  88. package/dist/clis/tieba/utils.js +240 -0
  89. package/dist/clis/tieba/utils.test.d.ts +1 -0
  90. package/dist/clis/tieba/utils.test.js +290 -0
  91. package/dist/clis/weread/book.js +100 -13
  92. package/dist/clis/weread/commands.test.js +221 -0
  93. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  94. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  95. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  96. package/dist/clis/weread/search-regression.test.js +407 -0
  97. package/dist/clis/weread/search.js +143 -7
  98. package/dist/clis/weread/shelf.js +13 -95
  99. package/dist/clis/weread/utils.d.ts +46 -0
  100. package/dist/clis/weread/utils.js +214 -7
  101. package/dist/clis/weread/utils.test.js +71 -1
  102. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  103. package/dist/clis/xiaohongshu/publish.js +78 -31
  104. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  105. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  106. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  107. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  108. package/dist/clis/xueqiu/comments.d.ts +118 -0
  109. package/dist/clis/xueqiu/comments.js +354 -0
  110. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  111. package/dist/clis/xueqiu/comments.test.js +696 -0
  112. package/dist/clis/youtube/transcript.js +2 -4
  113. package/dist/clis/youtube/utils.d.ts +9 -0
  114. package/dist/clis/youtube/utils.js +67 -3
  115. package/dist/clis/youtube/utils.test.d.ts +1 -0
  116. package/dist/clis/youtube/utils.test.js +37 -0
  117. package/dist/clis/youtube/video.js +16 -15
  118. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  119. package/dist/clis/zsxq/dynamics.js +47 -0
  120. package/dist/clis/zsxq/groups.d.ts +1 -0
  121. package/dist/clis/zsxq/groups.js +32 -0
  122. package/dist/clis/zsxq/search.d.ts +1 -0
  123. package/dist/clis/zsxq/search.js +43 -0
  124. package/dist/clis/zsxq/search.test.d.ts +1 -0
  125. package/dist/clis/zsxq/search.test.js +24 -0
  126. package/dist/clis/zsxq/topic.d.ts +1 -0
  127. package/dist/clis/zsxq/topic.js +47 -0
  128. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  129. package/dist/clis/zsxq/topic.test.js +29 -0
  130. package/dist/clis/zsxq/topics.d.ts +1 -0
  131. package/dist/clis/zsxq/topics.js +25 -0
  132. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  133. package/dist/clis/zsxq/topics.test.js +24 -0
  134. package/dist/clis/zsxq/utils.d.ts +97 -0
  135. package/dist/clis/zsxq/utils.js +230 -0
  136. package/dist/commanderAdapter.js +27 -4
  137. package/dist/commanderAdapter.test.js +39 -0
  138. package/dist/daemon.js +5 -4
  139. package/dist/errors.d.ts +29 -1
  140. package/dist/errors.js +49 -11
  141. package/dist/external-clis.yaml +17 -0
  142. package/dist/external.js +3 -3
  143. package/dist/main.js +2 -1
  144. package/dist/tui.js +2 -1
  145. package/dist/types.d.ts +5 -0
  146. package/docs/.vitepress/config.mts +3 -0
  147. package/docs/adapters/browser/band.md +63 -0
  148. package/docs/adapters/browser/ones.md +59 -0
  149. package/docs/adapters/browser/sinafinance.md +56 -6
  150. package/docs/adapters/browser/spotify.md +62 -0
  151. package/docs/adapters/browser/tieba.md +45 -0
  152. package/docs/adapters/browser/xueqiu.md +5 -0
  153. package/docs/adapters/browser/zsxq.md +49 -0
  154. package/docs/adapters/index.md +5 -2
  155. package/docs/adapters-doc/ones.md +32 -0
  156. package/extension/dist/background.js +1 -2
  157. package/extension/manifest.json +1 -1
  158. package/extension/package.json +1 -1
  159. package/extension/src/background.ts +17 -1
  160. package/extension/src/cdp.ts +42 -0
  161. package/extension/src/protocol.ts +5 -1
  162. package/package.json +1 -1
  163. package/scripts/postinstall.js +16 -0
  164. package/src/browser/daemon-client.ts +5 -1
  165. package/src/browser/page.ts +16 -0
  166. package/src/cli.ts +14 -14
  167. package/src/clis/antigravity/serve.ts +2 -2
  168. package/src/clis/band/bands.ts +76 -0
  169. package/src/clis/band/mentions.ts +134 -0
  170. package/src/clis/band/post.ts +187 -0
  171. package/src/clis/band/posts.ts +106 -0
  172. package/src/clis/doubao/detail.test.ts +53 -0
  173. package/src/clis/doubao/detail.ts +41 -0
  174. package/src/clis/doubao/history.test.ts +45 -0
  175. package/src/clis/doubao/history.ts +32 -0
  176. package/src/clis/doubao/meeting-summary.ts +53 -0
  177. package/src/clis/doubao/meeting-transcript.ts +48 -0
  178. package/src/clis/doubao/utils.test.ts +45 -0
  179. package/src/clis/doubao/utils.ts +371 -0
  180. package/src/clis/douyin/_shared/public-api.ts +84 -0
  181. package/src/clis/douyin/user-videos.test.ts +122 -0
  182. package/src/clis/douyin/user-videos.ts +101 -0
  183. package/src/clis/ones/common.ts +187 -0
  184. package/src/clis/ones/enrich-tasks.ts +47 -0
  185. package/src/clis/ones/login.ts +103 -0
  186. package/src/clis/ones/logout.ts +19 -0
  187. package/src/clis/ones/me.ts +34 -0
  188. package/src/clis/ones/my-tasks.ts +148 -0
  189. package/src/clis/ones/resolve-labels.ts +80 -0
  190. package/src/clis/ones/task-helpers.test.ts +14 -0
  191. package/src/clis/ones/task-helpers.ts +214 -0
  192. package/src/clis/ones/task.ts +79 -0
  193. package/src/clis/ones/tasks.ts +92 -0
  194. package/src/clis/ones/token-info.ts +46 -0
  195. package/src/clis/ones/worklog.test.ts +24 -0
  196. package/src/clis/ones/worklog.ts +306 -0
  197. package/src/clis/sinafinance/rolling-news.ts +42 -0
  198. package/src/clis/sinafinance/stock.ts +127 -0
  199. package/src/clis/spotify/spotify.ts +328 -0
  200. package/src/clis/spotify/utils.test.ts +87 -0
  201. package/src/clis/spotify/utils.ts +92 -0
  202. package/src/clis/tieba/commands.test.ts +86 -0
  203. package/src/clis/tieba/hot.ts +52 -0
  204. package/src/clis/tieba/posts.ts +108 -0
  205. package/src/clis/tieba/read.ts +158 -0
  206. package/src/clis/tieba/search.ts +119 -0
  207. package/src/clis/tieba/utils.test.ts +322 -0
  208. package/src/clis/tieba/utils.ts +348 -0
  209. package/src/clis/weread/book.ts +116 -13
  210. package/src/clis/weread/commands.test.ts +249 -0
  211. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  212. package/src/clis/weread/search-regression.test.ts +440 -0
  213. package/src/clis/weread/search.ts +189 -9
  214. package/src/clis/weread/shelf.ts +20 -122
  215. package/src/clis/weread/utils.test.ts +81 -1
  216. package/src/clis/weread/utils.ts +264 -7
  217. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  218. package/src/clis/xiaohongshu/publish.ts +84 -30
  219. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  220. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  221. package/src/clis/xueqiu/comments.test.ts +823 -0
  222. package/src/clis/xueqiu/comments.ts +461 -0
  223. package/src/clis/youtube/transcript.ts +2 -4
  224. package/src/clis/youtube/utils.test.ts +43 -0
  225. package/src/clis/youtube/utils.ts +69 -0
  226. package/src/clis/youtube/video.ts +16 -15
  227. package/src/clis/zsxq/dynamics.ts +60 -0
  228. package/src/clis/zsxq/groups.ts +41 -0
  229. package/src/clis/zsxq/search.test.ts +29 -0
  230. package/src/clis/zsxq/search.ts +54 -0
  231. package/src/clis/zsxq/topic.test.ts +34 -0
  232. package/src/clis/zsxq/topic.ts +68 -0
  233. package/src/clis/zsxq/topics.test.ts +29 -0
  234. package/src/clis/zsxq/topics.ts +36 -0
  235. package/src/clis/zsxq/utils.ts +351 -0
  236. package/src/commanderAdapter.test.ts +47 -0
  237. package/src/commanderAdapter.ts +26 -3
  238. package/src/daemon.ts +5 -4
  239. package/src/errors.ts +71 -10
  240. package/src/external-clis.yaml +17 -0
  241. package/src/external.ts +3 -3
  242. package/src/main.ts +2 -1
  243. package/src/tui.ts +2 -1
  244. package/src/types.ts +5 -0
  245. package/tests/e2e/band-auth.test.ts +20 -0
  246. package/tests/e2e/browser-auth-helpers.ts +18 -0
  247. package/tests/e2e/browser-auth.test.ts +35 -47
  248. package/tests/e2e/browser-public.test.ts +288 -0
  249. package/tests/e2e/management.test.ts +1 -1
  250. package/tests/e2e/plugin-management.test.ts +1 -1
  251. package/vitest.config.ts +1 -0
  252. package/SKILL.md +0 -879
  253. package/dist/weread-private-api-regression.test.d.ts +0 -1
  254. package/dist/weread-search-regression.test.d.ts +0 -1
  255. package/dist/weread-search-regression.test.js +0 -39
  256. package/src/weread-search-regression.test.ts +0 -44
@@ -0,0 +1,60 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { ZsxqTopic } from './utils.js';
3
+ import {
4
+ ensureZsxqAuth,
5
+ ensureZsxqPage,
6
+ fetchFirstJson,
7
+ toTopicRow,
8
+ getTopicText,
9
+ getTopicAuthor,
10
+ getTopicUrl,
11
+ } from './utils.js';
12
+
13
+ cli({
14
+ site: 'zsxq',
15
+ name: 'dynamics',
16
+ description: '获取所有星球的最新动态',
17
+ domain: 'wx.zsxq.com',
18
+ strategy: Strategy.COOKIE,
19
+ browser: true,
20
+ args: [
21
+ { name: 'limit', type: 'int', default: 20, help: 'Number of dynamics to return' },
22
+ ],
23
+ columns: ['time', 'group', 'author', 'title', 'comments', 'likes', 'url'],
24
+ func: async (page, kwargs) => {
25
+ await ensureZsxqPage(page);
26
+ await ensureZsxqAuth(page);
27
+
28
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
29
+
30
+ const { data } = await fetchFirstJson(page, [
31
+ `https://api.zsxq.com/v2/dynamics?scope=general&count=${limit}`,
32
+ ]);
33
+
34
+ const respData = (data as any)?.resp_data || data;
35
+ const dynamics = respData?.dynamics || [];
36
+ return dynamics.slice(0, limit).map((d: any) => {
37
+ const topic = d.topic as ZsxqTopic | undefined;
38
+ if (!topic) {
39
+ return {
40
+ time: d.create_time || '',
41
+ group: '',
42
+ author: '',
43
+ title: `[${d.action || 'unknown'}]`,
44
+ comments: 0,
45
+ likes: 0,
46
+ url: '',
47
+ };
48
+ }
49
+ return {
50
+ time: d.create_time || topic.create_time || '',
51
+ group: topic.group?.name || '',
52
+ author: getTopicAuthor(topic),
53
+ title: getTopicText(topic).slice(0, 120),
54
+ comments: topic.comments_count ?? 0,
55
+ likes: topic.likes_count ?? 0,
56
+ url: getTopicUrl(topic.topic_id),
57
+ };
58
+ });
59
+ },
60
+ });
@@ -0,0 +1,41 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import {
3
+ ensureZsxqAuth,
4
+ ensureZsxqPage,
5
+ fetchFirstJson,
6
+ getGroupsFromResponse,
7
+ } from './utils.js';
8
+
9
+ cli({
10
+ site: 'zsxq',
11
+ name: 'groups',
12
+ description: '列出当前账号加入的星球',
13
+ domain: 'wx.zsxq.com',
14
+ strategy: Strategy.COOKIE,
15
+ browser: true,
16
+ args: [
17
+ { name: 'limit', type: 'int', default: 50, help: 'Number of groups to return' },
18
+ ],
19
+ columns: ['group_id', 'name', 'category', 'members', 'topics', 'joined_at', 'url'],
20
+ func: async (page, kwargs) => {
21
+ await ensureZsxqPage(page);
22
+ await ensureZsxqAuth(page);
23
+
24
+ const limit = Math.max(1, Number(kwargs.limit) || 50);
25
+
26
+ const { data } = await fetchFirstJson(page, [
27
+ `https://api.zsxq.com/v2/groups`,
28
+ ]);
29
+
30
+ return getGroupsFromResponse(data).slice(0, limit).map((group) => ({
31
+ group_id: group.group_id ?? '',
32
+ name: group.name || '',
33
+ category: group.category?.title || '',
34
+ members: group.statistics?.subscriptions_count ?? 0,
35
+ topics: group.statistics?.topics_count ?? 0,
36
+ joined_at: group.user_specific?.join_time || '',
37
+ valid_until: group.user_specific?.validity?.end_time || '',
38
+ url: group.group_id ? `https://wx.zsxq.com/group/${group.group_id}` : 'https://wx.zsxq.com',
39
+ }));
40
+ },
41
+ });
@@ -0,0 +1,29 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './search.js';
4
+
5
+ describe('zsxq search command', () => {
6
+ beforeEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+
10
+ it('requires an explicit group_id when there is no active group context', async () => {
11
+ const command = getRegistry().get('zsxq/search');
12
+ expect(command?.func).toBeTypeOf('function');
13
+
14
+ const mockPage = {
15
+ goto: vi.fn().mockResolvedValue(undefined),
16
+ evaluate: vi.fn()
17
+ .mockResolvedValueOnce(true)
18
+ .mockResolvedValueOnce(null),
19
+ } as any;
20
+
21
+ await expect(command!.func!(mockPage, { keyword: 'opencli', limit: 20 })).rejects.toMatchObject({
22
+ code: 'ARGUMENT',
23
+ message: 'Cannot determine active group_id',
24
+ });
25
+
26
+ expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
27
+ expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
28
+ });
29
+ });
@@ -0,0 +1,54 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import {
3
+ getActiveGroupId,
4
+ ensureZsxqAuth,
5
+ ensureZsxqPage,
6
+ fetchFirstJson,
7
+ getGroupsFromResponse,
8
+ getTopicsFromResponse,
9
+ toTopicRow,
10
+ } from './utils.js';
11
+
12
+ cli({
13
+ site: 'zsxq',
14
+ name: 'search',
15
+ description: '搜索星球内容',
16
+ domain: 'wx.zsxq.com',
17
+ strategy: Strategy.COOKIE,
18
+ browser: true,
19
+ args: [
20
+ { name: 'keyword', required: true, positional: true, help: 'Search keyword' },
21
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results to return' },
22
+ { name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' },
23
+ ],
24
+ columns: ['topic_id', 'group', 'author', 'title', 'comments', 'likes', 'time', 'url'],
25
+ func: async (page, kwargs) => {
26
+ await ensureZsxqPage(page);
27
+ await ensureZsxqAuth(page);
28
+
29
+ const keyword = String(kwargs.keyword || '').trim();
30
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
31
+ const groupId = String(kwargs.group_id || await getActiveGroupId(page));
32
+ const query = encodeURIComponent(keyword);
33
+
34
+ // Resolve group name from groups API
35
+ let groupName = groupId;
36
+ try {
37
+ const { data: groupsData } = await fetchFirstJson(page, [
38
+ `https://api.zsxq.com/v2/groups`,
39
+ ]);
40
+ const groups = getGroupsFromResponse(groupsData);
41
+ const found = groups.find(g => String(g.group_id) === groupId);
42
+ if (found?.name) groupName = found.name;
43
+ } catch { /* ignore */ }
44
+
45
+ const { data } = await fetchFirstJson(page, [
46
+ `https://api.zsxq.com/v2/search/groups/${groupId}/topics?keyword=${query}&count=${limit}`,
47
+ ]);
48
+
49
+ return getTopicsFromResponse(data).slice(0, limit).map((topic) => ({
50
+ ...toTopicRow(topic),
51
+ group: groupName,
52
+ }));
53
+ },
54
+ });
@@ -0,0 +1,34 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './topic.js';
4
+
5
+ describe('zsxq topic command', () => {
6
+ beforeEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+
10
+ it('maps topic detail 404 responses to NOT_FOUND before fetching comments', async () => {
11
+ const command = getRegistry().get('zsxq/topic');
12
+ expect(command?.func).toBeTypeOf('function');
13
+
14
+ const mockPage = {
15
+ goto: vi.fn().mockResolvedValue(undefined),
16
+ evaluate: vi.fn()
17
+ .mockResolvedValueOnce(true)
18
+ .mockResolvedValueOnce({
19
+ ok: true,
20
+ status: 404,
21
+ url: 'https://api.zsxq.com/v2/topics/404',
22
+ data: null,
23
+ }),
24
+ } as any;
25
+
26
+ await expect(command!.func!(mockPage, { id: '404', comment_limit: 20 })).rejects.toMatchObject({
27
+ code: 'NOT_FOUND',
28
+ message: 'Topic 404 not found',
29
+ });
30
+
31
+ expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
32
+ expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
33
+ });
34
+ });
@@ -0,0 +1,68 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import {
4
+ browserJsonRequest,
5
+ ensureZsxqAuth,
6
+ ensureZsxqPage,
7
+ fetchFirstJson,
8
+ getCommentsFromResponse,
9
+ getTopicFromResponse,
10
+ getTopicUrl,
11
+ summarizeComments,
12
+ toTopicRow,
13
+ } from './utils.js';
14
+
15
+ cli({
16
+ site: 'zsxq',
17
+ name: 'topic',
18
+ description: '获取单个话题详情和评论',
19
+ domain: 'wx.zsxq.com',
20
+ strategy: Strategy.COOKIE,
21
+ browser: true,
22
+ args: [
23
+ { name: 'id', required: true, positional: true, help: 'Topic ID' },
24
+ { name: 'comment_limit', type: 'int', default: 20, help: 'Number of comments to fetch' },
25
+ ],
26
+ columns: ['topic_id', 'type', 'author', 'title', 'comments', 'likes', 'comment_preview', 'url'],
27
+ func: async (page, kwargs) => {
28
+ await ensureZsxqPage(page);
29
+ await ensureZsxqAuth(page);
30
+
31
+ const topicId = String(kwargs.id);
32
+ const commentLimit = Math.max(1, Number(kwargs.comment_limit) || 20);
33
+
34
+ const detailUrl = `https://api.zsxq.com/v2/topics/${topicId}`;
35
+ const detailResp = await browserJsonRequest(page, detailUrl);
36
+
37
+ if (detailResp.status === 404) {
38
+ throw new CliError('NOT_FOUND', `Topic ${topicId} not found`);
39
+ }
40
+ if (!detailResp.ok) {
41
+ throw new CliError(
42
+ 'FETCH_ERROR',
43
+ detailResp.error || `Failed to fetch topic ${topicId}`,
44
+ `Checked endpoint: ${detailUrl}`,
45
+ );
46
+ }
47
+
48
+ const commentsResp = await fetchFirstJson(page, [
49
+ `https://api.zsxq.com/v2/topics/${topicId}/comments?sort=asc&count=${commentLimit}`,
50
+ ]);
51
+
52
+ const topic = getTopicFromResponse(detailResp.data);
53
+ if (!topic) throw new CliError('NOT_FOUND', `Topic ${topicId} not found`);
54
+
55
+ const comments = getCommentsFromResponse(commentsResp.data);
56
+ const row = toTopicRow({
57
+ ...topic,
58
+ comments,
59
+ comments_count: topic.comments_count ?? comments.length,
60
+ });
61
+
62
+ return [{
63
+ ...row,
64
+ comment_preview: summarizeComments(comments, 5),
65
+ url: getTopicUrl(topic.topic_id ?? topicId),
66
+ }];
67
+ },
68
+ });
@@ -0,0 +1,29 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './topics.js';
4
+
5
+ describe('zsxq topics command', () => {
6
+ beforeEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+
10
+ it('requires an explicit group_id when there is no active group context', async () => {
11
+ const command = getRegistry().get('zsxq/topics');
12
+ expect(command?.func).toBeTypeOf('function');
13
+
14
+ const mockPage = {
15
+ goto: vi.fn().mockResolvedValue(undefined),
16
+ evaluate: vi.fn()
17
+ .mockResolvedValueOnce(true)
18
+ .mockResolvedValueOnce(null),
19
+ } as any;
20
+
21
+ await expect(command!.func!(mockPage, { limit: 20 })).rejects.toMatchObject({
22
+ code: 'ARGUMENT',
23
+ message: 'Cannot determine active group_id',
24
+ });
25
+
26
+ expect(mockPage.goto).toHaveBeenCalledWith('https://wx.zsxq.com');
27
+ expect(mockPage.evaluate).toHaveBeenCalledTimes(2);
28
+ });
29
+ });
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import {
3
+ getActiveGroupId,
4
+ ensureZsxqAuth,
5
+ ensureZsxqPage,
6
+ fetchFirstJson,
7
+ getTopicsFromResponse,
8
+ toTopicRow,
9
+ } from './utils.js';
10
+
11
+ cli({
12
+ site: 'zsxq',
13
+ name: 'topics',
14
+ description: '获取当前星球的话题列表',
15
+ domain: 'wx.zsxq.com',
16
+ strategy: Strategy.COOKIE,
17
+ browser: true,
18
+ args: [
19
+ { name: 'limit', type: 'int', default: 20, help: 'Number of topics to return' },
20
+ { name: 'group_id', help: 'Optional group id; defaults to the active group in Chrome' },
21
+ ],
22
+ columns: ['topic_id', 'type', 'author', 'title', 'comments', 'likes', 'time', 'url'],
23
+ func: async (page, kwargs) => {
24
+ await ensureZsxqPage(page);
25
+ await ensureZsxqAuth(page);
26
+
27
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
28
+ const groupId = String(kwargs.group_id || await getActiveGroupId(page));
29
+
30
+ const { data } = await fetchFirstJson(page, [
31
+ `https://api.zsxq.com/v2/groups/${groupId}/topics?scope=all&count=${limit}`,
32
+ ]);
33
+
34
+ return getTopicsFromResponse(data).slice(0, limit).map(toTopicRow);
35
+ },
36
+ });