@jackwener/opencli 1.7.8 → 1.7.10

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 (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { SelectorError } from '@jackwener/opencli/errors';
2
+ import { selectorError } from '@jackwener/opencli/errors';
3
3
  export const composerCommand = cli({
4
4
  site: 'cursor',
5
5
  name: 'composer',
@@ -27,7 +27,7 @@ export const composerCommand = cli({
27
27
  return true;
28
28
  })(${JSON.stringify(textToInsert)})`);
29
29
  if (!typed) {
30
- throw new SelectorError('Cursor Composer input element', 'Could not find Cursor Composer input element after pressing Cmd+I.');
30
+ throw selectorError('Cursor Composer input element', 'Could not find Cursor Composer input element after pressing Cmd+I.');
31
31
  }
32
32
  await page.wait(0.5);
33
33
  await page.pressKey('Enter');
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { SelectorError } from '@jackwener/opencli/errors';
2
+ import { selectorError } from '@jackwener/opencli/errors';
3
3
  export const sendCommand = cli({
4
4
  site: 'cursor',
5
5
  name: 'send',
@@ -24,7 +24,7 @@ export const sendCommand = cli({
24
24
  return true;
25
25
  })(${JSON.stringify(textToInsert)})`);
26
26
  if (!injected) {
27
- throw new SelectorError('Cursor Composer input element');
27
+ throw selectorError('Cursor Composer input element');
28
28
  }
29
29
  // Submit the command. In Cursor, Enter usually submits the chat.
30
30
  await page.wait(0.5);
@@ -18,7 +18,7 @@ export const askCommand = cli({
18
18
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
19
19
  { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
20
20
  { name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
21
- { name: 'model', default: 'instant', choices: ['instant', 'expert'], help: 'Model to use: instant or expert' },
21
+ { name: 'model', default: 'instant', choices: ['instant', 'expert', 'vision'], help: 'Model to use: instant, expert, or vision' },
22
22
  { name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' },
23
23
  { name: 'search', type: 'boolean', default: false, help: 'Enable web search' },
24
24
  { name: 'file', help: 'Attach a file (PDF, image, text) with the prompt' },
@@ -78,9 +78,22 @@ export const askCommand = cli({
78
78
  throw new CommandExecutionError('Could not enable DeepThink');
79
79
  }
80
80
 
81
- const searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
82
- if (!searchResult?.ok && wantSearch) {
83
- throw new CommandExecutionError('Could not enable Search');
81
+ if (wantModel === 'vision' && wantSearch) {
82
+ throw new CliError(
83
+ 'ARGUMENT',
84
+ 'DeepSeek vision mode does not support --search.',
85
+ 'Run without --search, or use --model instant/expert for web search.',
86
+ EXIT_CODES.USAGE_ERROR,
87
+ );
88
+ }
89
+
90
+ // Vision mode does not have the search toggle.
91
+ let searchResult;
92
+ if (wantModel !== 'vision') {
93
+ searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
94
+ if (!searchResult?.ok && wantSearch) {
95
+ throw new CommandExecutionError('Could not enable Search');
96
+ }
84
97
  }
85
98
 
86
99
  if (thinkResult?.toggled || searchResult?.toggled) await page.wait(0.5);
@@ -263,4 +263,50 @@ describe('deepseek ask conversation resume', () => {
263
263
  expect(rows).toEqual([{ response: 'follow-up reply' }]);
264
264
  expect(mockSelectModel).toHaveBeenCalled();
265
265
  });
266
+
267
+ it('skips search toggle in vision mode when search is not requested', async () => {
268
+ mockEnsureOnDeepSeek.mockResolvedValue(false);
269
+ mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
270
+ mockSetFeature.mockResolvedValue({ ok: true, toggled: false });
271
+ mockSendMessage.mockResolvedValue({ ok: true });
272
+ mockGetBubbleCount.mockResolvedValue(0);
273
+ mockWaitForResponse.mockResolvedValue('vision reply');
274
+ page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
275
+
276
+ const rows = await askCommand.func(page, {
277
+ prompt: 'describe',
278
+ timeout: 120,
279
+ new: false,
280
+ model: 'vision',
281
+ think: false,
282
+ search: false,
283
+ });
284
+
285
+ expect(rows).toEqual([{ response: 'vision reply' }]);
286
+ expect(mockSetFeature).toHaveBeenCalledTimes(1);
287
+ expect(mockSetFeature).toHaveBeenCalledWith(expect.anything(), 'DeepThink', false);
288
+ });
289
+
290
+ it('fails fast instead of silently ignoring --search in vision mode', async () => {
291
+ mockEnsureOnDeepSeek.mockResolvedValue(false);
292
+ mockSelectModel.mockResolvedValue({ ok: true, toggled: false });
293
+ page.evaluate.mockResolvedValue('https://chat.deepseek.com/');
294
+
295
+ await expect(askCommand.func(page, {
296
+ prompt: 'describe',
297
+ timeout: 120,
298
+ new: false,
299
+ model: 'vision',
300
+ think: false,
301
+ search: true,
302
+ })).rejects.toMatchObject(new CliError(
303
+ 'ARGUMENT',
304
+ 'DeepSeek vision mode does not support --search.',
305
+ 'Run without --search, or use --model instant/expert for web search.',
306
+ EXIT_CODES.USAGE_ERROR,
307
+ ));
308
+
309
+ expect(mockSendMessage).not.toHaveBeenCalled();
310
+ expect(mockSendWithFile).not.toHaveBeenCalled();
311
+ });
266
312
  });
@@ -40,9 +40,10 @@ export async function selectModel(page, modelName) {
40
40
  return page.evaluate(`(() => {
41
41
  var radios = document.querySelectorAll('div[role="radio"]');
42
42
  if (radios.length === 0) return { ok: false };
43
- var isFirst = '${modelName}'.toLowerCase() === 'instant';
44
- if (!isFirst && radios.length < 2) return { ok: false };
45
- var target = isFirst ? radios[0] : radios[radios.length - 1];
43
+ var name = '${modelName}'.toLowerCase();
44
+ var index = name === 'instant' ? 0 : name === 'expert' ? 1 : name === 'vision' ? 2 : -1;
45
+ if (index < 0 || index >= radios.length) return { ok: false };
46
+ var target = radios[index];
46
47
  var alreadySelected = target.getAttribute('aria-checked') === 'true';
47
48
  if (!alreadySelected) target.click();
48
49
  return { ok: true, toggled: !alreadySelected };
@@ -74,14 +75,18 @@ export async function sendMessage(page, prompt) {
74
75
  document.execCommand('insertText', false, ${promptJson});
75
76
  await new Promise(r => setTimeout(r, 800));
76
77
 
77
- const btns = document.querySelectorAll('div[role="button"]');
78
- for (const btn of btns) {
79
- if (btn.getAttribute('aria-disabled') === 'false') {
80
- const svgs = btn.querySelectorAll('svg');
81
- if (svgs.length > 0 && btn.closest('div')?.querySelector('textarea')) {
82
- btn.click();
83
- return { ok: true };
84
- }
78
+ // Find the send button: last non-toggle button in the textarea's container
79
+ var container = box.parentElement;
80
+ while (container && !container.querySelector('div[role="button"]')) {
81
+ container = container.parentElement;
82
+ }
83
+ if (container) {
84
+ var btns = container.querySelectorAll('div[role="button"]:not(.ds-toggle-button)');
85
+ var sendBtn = btns[btns.length - 1];
86
+ if (sendBtn && sendBtn.getAttribute('aria-disabled') === 'false'
87
+ && sendBtn.querySelectorAll('svg').length > 0) {
88
+ sendBtn.click();
89
+ return { ok: true };
85
90
  }
86
91
  }
87
92
 
@@ -273,9 +278,18 @@ async function waitForFilePreview(page, fileName) {
273
278
  for (let attempt = 0; attempt < 8; attempt++) {
274
279
  await page.wait(2);
275
280
  const ready = await page.evaluate(`(() => {
276
- const name = ${JSON.stringify(fileName)};
277
- return Array.from(document.querySelectorAll('div'))
278
- .some((el) => el.children.length === 0 && (el.textContent || '').trim() === name);
281
+ var name = ${JSON.stringify(fileName)};
282
+ var hasFileName = Array.from(document.querySelectorAll('div'))
283
+ .some(function(el) { return el.children.length === 0 && (el.textContent || '').trim() === name; });
284
+ if (hasFileName) return true;
285
+ // Vision mode shows an image thumbnail, not filename text. Require
286
+ // a preview-like node here; send-button readiness is checked later.
287
+ var box = document.querySelector('${TEXTAREA_SELECTOR}');
288
+ if (!box) return false;
289
+ var c = box.parentElement;
290
+ while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
291
+ if (!c) return false;
292
+ return !!c.querySelector('img[src], canvas, video, [style*="background-image"], [class*="preview"], [class*="upload"]');
279
293
  })()`);
280
294
  if (ready) return true;
281
295
  }
@@ -314,7 +328,7 @@ export async function sendWithFile(page, filePath, prompt) {
314
328
  uploaded = true;
315
329
  } catch (err) {
316
330
  const msg = String(err?.message || err);
317
- if (!msg.includes('Unknown action') && !msg.includes('not supported')) {
331
+ if (!msg.includes('Unknown action') && !msg.includes('not supported') && !msg.includes('Not allowed')) {
318
332
  throw err;
319
333
  }
320
334
  }
@@ -341,7 +355,8 @@ export async function sendWithFile(page, filePath, prompt) {
341
355
  }
342
356
 
343
357
  inp.files = dt.files;
344
- inp[propsKey].onChange({ target: { files: dt.files } });
358
+ // Use inp.files, not dt.files; assignment transfers ownership
359
+ inp[propsKey].onChange({ target: { files: inp.files } });
345
360
  return { ok: true };
346
361
  })()`);
347
362
  if (fallbackResult && !fallbackResult.ok) return fallbackResult;
@@ -350,6 +365,30 @@ export async function sendWithFile(page, filePath, prompt) {
350
365
  const ready = await waitForFilePreview(page, fileName);
351
366
  if (!ready) return { ok: false, reason: 'file preview did not appear' };
352
367
 
368
+ // File preview appears immediately but send button stays disabled until
369
+ // the server upload finishes. Wait for it.
370
+ let sendEnabled = false;
371
+ for (let tick = 0; tick < 15; tick++) {
372
+ const enabled = await page.evaluate(`(() => {
373
+ var box = document.querySelector('${TEXTAREA_SELECTOR}');
374
+ if (!box) return false;
375
+ var c = box.parentElement;
376
+ while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
377
+ if (!c) return false;
378
+ var btns = c.querySelectorAll('div[role="button"]:not(.ds-toggle-button)');
379
+ var last = btns[btns.length - 1];
380
+ return !!(last && last.getAttribute('aria-disabled') === 'false');
381
+ })()`);
382
+ if (enabled) {
383
+ sendEnabled = true;
384
+ break;
385
+ }
386
+ await page.wait(1);
387
+ }
388
+ if (!sendEnabled) {
389
+ return { ok: false, reason: 'send button did not enable after upload' };
390
+ }
391
+
353
392
  return sendMessage(page, prompt);
354
393
  }
355
394
 
@@ -107,15 +107,36 @@ describe('deepseek sendWithFile', () => {
107
107
  setFileInput: vi.fn().mockResolvedValue(undefined),
108
108
  wait: vi.fn().mockResolvedValue(undefined),
109
109
  evaluate: vi.fn()
110
- .mockResolvedValueOnce(undefined)
111
- .mockResolvedValueOnce(true)
112
- .mockResolvedValueOnce({ ok: true }),
110
+ .mockResolvedValueOnce(undefined) // sidebar collapse
111
+ .mockResolvedValueOnce(true) // waitForFilePreview
112
+ .mockResolvedValueOnce(true) // send button enabled check
113
+ .mockResolvedValueOnce({ ok: true }), // sendMessage
113
114
  };
114
115
 
115
116
  const result = await sendWithFile(page, filePath, 'summarize this');
116
117
 
117
- expect(result).toEqual({ ok: true });
118
- expect(page.setFileInput).toHaveBeenCalledWith([filePath], 'input[type="file"]');
118
+ expect(result).toEqual({ ok: true }); expect(page.setFileInput).toHaveBeenCalledWith([filePath], 'input[type="file"]');
119
+ });
120
+
121
+ it('fails closed when upload preview appears but send button never enables', async () => {
122
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
123
+ tempDirs.push(dir);
124
+ const filePath = path.join(dir, 'report.txt');
125
+ fs.writeFileSync(filePath, 'hello');
126
+
127
+ const page = {
128
+ setFileInput: vi.fn().mockResolvedValue(undefined),
129
+ wait: vi.fn().mockResolvedValue(undefined),
130
+ evaluate: vi.fn()
131
+ .mockResolvedValueOnce(undefined) // sidebar collapse
132
+ .mockResolvedValueOnce(true) // waitForFilePreview
133
+ .mockResolvedValue(false), // send button never enables
134
+ };
135
+
136
+ const result = await sendWithFile(page, filePath, 'summarize this');
137
+
138
+ expect(result).toEqual({ ok: false, reason: 'send button did not enable after upload' });
139
+ expect(page.evaluate).toHaveBeenCalledTimes(17);
119
140
  });
120
141
  });
121
142
 
@@ -142,4 +163,102 @@ describe('deepseek selectModel', () => {
142
163
  expect(result).toEqual({ ok: false });
143
164
  expect(instantRadio.click).not.toHaveBeenCalled();
144
165
  });
166
+
167
+ it('selects the correct radio for each model', async () => {
168
+ const radios = [0, 1, 2].map(() => ({
169
+ getAttribute: vi.fn(() => 'false'),
170
+ click: vi.fn(),
171
+ }));
172
+ global.document = {
173
+ querySelectorAll: vi.fn(() => radios),
174
+ };
175
+ const page = {
176
+ evaluate: vi.fn(async (script) => eval(script)),
177
+ };
178
+
179
+ await selectModel(page, 'instant');
180
+ expect(radios[0].click).toHaveBeenCalled();
181
+ expect(radios[1].click).not.toHaveBeenCalled();
182
+ expect(radios[2].click).not.toHaveBeenCalled();
183
+
184
+ radios.forEach(r => r.click.mockClear());
185
+ await selectModel(page, 'expert');
186
+ expect(radios[1].click).toHaveBeenCalled();
187
+
188
+ radios.forEach(r => r.click.mockClear());
189
+ await selectModel(page, 'vision');
190
+ expect(radios[2].click).toHaveBeenCalled();
191
+ });
192
+
193
+ it('rejects unknown model names', async () => {
194
+ const radios = [0, 1, 2].map(() => ({
195
+ getAttribute: vi.fn(() => 'false'),
196
+ click: vi.fn(),
197
+ }));
198
+ global.document = {
199
+ querySelectorAll: vi.fn(() => radios),
200
+ };
201
+ const page = {
202
+ evaluate: vi.fn(async (script) => eval(script)),
203
+ };
204
+
205
+ const result = await selectModel(page, 'turbo');
206
+ expect(result).toEqual({ ok: false });
207
+ });
208
+ });
209
+
210
+ describe('deepseek sendWithFile Not allowed fallback', () => {
211
+ const tempDirs = [];
212
+
213
+ afterEach(() => {
214
+ vi.restoreAllMocks();
215
+ while (tempDirs.length) {
216
+ fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
217
+ }
218
+ });
219
+
220
+ it('falls back to DataTransfer when setFileInput throws Not allowed', async () => {
221
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
222
+ tempDirs.push(dir);
223
+ const filePath = path.join(dir, 'image.png');
224
+ fs.writeFileSync(filePath, 'fake-png');
225
+
226
+ const page = {
227
+ setFileInput: vi.fn().mockRejectedValue(new Error('Not allowed')),
228
+ wait: vi.fn().mockResolvedValue(undefined),
229
+ evaluate: vi.fn()
230
+ .mockResolvedValueOnce(undefined) // sidebar collapse
231
+ .mockResolvedValueOnce({ ok: true }) // DataTransfer fallback
232
+ .mockResolvedValueOnce(true) // waitForFilePreview
233
+ .mockResolvedValueOnce(true) // send button enabled
234
+ .mockResolvedValueOnce({ ok: true }),// sendMessage
235
+ };
236
+
237
+ const result = await sendWithFile(page, filePath, 'describe');
238
+
239
+ expect(page.setFileInput).toHaveBeenCalled();
240
+ expect(page.evaluate).toHaveBeenCalledTimes(5);
241
+ expect(result).toEqual({ ok: true });
242
+ });
243
+
244
+ it('does not treat send-button enablement alone as image upload proof', async () => {
245
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-'));
246
+ tempDirs.push(dir);
247
+ const filePath = path.join(dir, 'image.png');
248
+ fs.writeFileSync(filePath, 'fake-png');
249
+
250
+ const page = {
251
+ setFileInput: vi.fn().mockResolvedValue(undefined),
252
+ wait: vi.fn().mockResolvedValue(undefined),
253
+ evaluate: vi.fn()
254
+ .mockResolvedValueOnce(undefined) // sidebar collapse
255
+ .mockResolvedValue(false), // no filename / thumbnail preview
256
+ };
257
+
258
+ const result = await sendWithFile(page, filePath, 'describe');
259
+
260
+ expect(result).toEqual({ ok: false, reason: 'file preview did not appear' });
261
+ expect(page.evaluate.mock.calls[1][0]).toContain('img[src], canvas, video');
262
+ expect(page.evaluate.mock.calls[1][0]).not.toContain("aria-disabled') === 'false'");
263
+ });
145
264
  });
@@ -63,7 +63,7 @@ function getTranscriptLinesScript() {
63
63
  const stopLines = new Set([
64
64
  '豆包',
65
65
  '新对话',
66
- '内容由豆包 AI 生成',
66
+ '内容由豆包 AI 生成,请仔细甄别',
67
67
  'AI 创作',
68
68
  '云盘',
69
69
  '更多',
@@ -75,6 +75,8 @@ function getTranscriptLinesScript() {
75
75
  'PPT 生成',
76
76
  '图像生成',
77
77
  '帮我写作',
78
+ '请仔细甄别',
79
+ '下载电脑版',
78
80
  ]);
79
81
 
80
82
  const noisyPatterns = [
@@ -88,7 +90,7 @@ function getTranscriptLinesScript() {
88
90
 
89
91
  const transcriptText = clean(root.innerText || root.textContent || '')
90
92
  .replace(/新对话/g, '\\n')
91
- .replace(/内容由豆包 AI 生成/g, '\\n')
93
+ .replace(/内容由豆包 AI 生成,请仔细甄别/g, '\\n')
92
94
  .replace(/在此处拖放文件/g, '\\n')
93
95
  .replace(/文件数量:[^\\n]*/g, '')
94
96
  .replace(/文件类型:[^\\n]*/g, '');
@@ -144,12 +146,20 @@ function getTurnsScript() {
144
146
  if (
145
147
  root.matches('[data-testid="send_message"], [class*="send-message"]')
146
148
  || root.querySelector('[data-testid="send_message"], [class*="send-message"]')
149
+ || root.matches('[class*="bg-g-send-msg-bubble"]')
150
+ ||
151
+ root.querySelector('[class*="bg-g-send-msg-bubble"]')
152
+ || root.querySelector('[data-foundation-type="send-message-action-bar"]')
147
153
  ) {
148
154
  return 'User';
149
155
  }
150
156
  if (
151
157
  root.matches('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]')
152
158
  || root.querySelector('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]')
159
+ || root.matches('[class*="bg-g-receive-msg-bubble"]')
160
+ ||
161
+ root.querySelector('[class*="bg-g-receive-msg-bubble"]')
162
+ || root.querySelector('[data-foundation-type="receive-message-action-bar"]')
153
163
  ) {
154
164
  return 'Assistant';
155
165
  }
@@ -163,6 +173,10 @@ function getTurnsScript() {
163
173
  '[data-testid*="message_content"]',
164
174
  '[class*="message-text"]',
165
175
  '[class*="message-content"]',
176
+ '[class*="bg-g-send-msg-bubble"]',
177
+ '[class*="bg-g-receive-msg-bubble"]',
178
+ '.flow-markdown-body',
179
+ '[class*="bubble"]',
166
180
  ];
167
181
  const messageImageSelector = messageTextSelectors.map((s) => s + ' img').join(', ');
168
182
 
@@ -205,14 +219,30 @@ function getTurnsScript() {
205
219
  return text ? text + '\\n' + imageLines.join('\\n') : imageLines.join('\\n');
206
220
  };
207
221
 
208
- const messageList = document.querySelector('[data-testid="message-list"]');
222
+ const messageList = document.querySelector('[class*="message-list-S2Fv2S"], .container-PvPoAn, .scroll-view-OEiNXD, [data-testid="message-list"]');
209
223
  if (!messageList) return [];
210
224
 
211
- const unionRoots = Array.from(messageList.querySelectorAll('[data-testid="union_message"]'))
212
- .filter((el) => isVisible(el));
213
- const blockRoots = Array.from(messageList.querySelectorAll('[data-testid="message-block-container"]'))
214
- .filter((el) => isVisible(el) && !el.closest('[data-testid="union_message"]'));
215
- const roots = (unionRoots.length > 0 ? unionRoots : blockRoots)
225
+ const itemSelectors = [
226
+ '[class*="item-kDun2N"]',
227
+ '[data-testid="union_message"]',
228
+ '[data-testid="message-block-container"]',
229
+ '[data-message-id]',
230
+ '[class*="bg-g-send-msg-bubble"]',
231
+ '[class*="bg-g-receive-msg-bubble"]',
232
+ ];
233
+
234
+ const allRoots = [];
235
+ const seen = new Set();
236
+ for (const sel of itemSelectors) {
237
+ messageList.querySelectorAll(sel).forEach((el) => {
238
+ if (!seen.has(el)) {
239
+ seen.add(el);
240
+ allRoots.push(el);
241
+ }
242
+ });
243
+ }
244
+ const roots = allRoots
245
+ .filter((el) => isVisible(el) && !el.closest('script, style, noscript'))
216
246
  .filter((el, index, items) => !items.some((other, otherIndex) => otherIndex !== index && other.contains(el)));
217
247
 
218
248
  const turns = roots
@@ -230,11 +260,11 @@ function getTurnsScript() {
230
260
  });
231
261
 
232
262
  const deduped = [];
233
- const seen = new Set();
263
+ const dedupedSeen = new Set();
234
264
  for (const turn of turns) {
235
265
  const key = turn.role + '::' + turn.text;
236
- if (seen.has(key)) continue;
237
- seen.add(key);
266
+ if (dedupedSeen.has(key)) continue;
267
+ dedupedSeen.add(key);
238
268
  deduped.push({ Role: turn.role, Text: turn.text });
239
269
  }
240
270
 
@@ -421,6 +451,16 @@ function clickSendButtonScript() {
421
451
  return `
422
452
  (() => {
423
453
  ${buildDoubaoComposerLocatorScript()}
454
+ const directSendButton = document.querySelector('button#flow-end-msg-send');
455
+ if (directSendButton instanceof HTMLElement && isVisible(directSendButton)) {
456
+ const disabled = directSendButton.getAttribute('disabled') !== null
457
+ || directSendButton.getAttribute('aria-disabled') === 'true';
458
+ if (!disabled) {
459
+ directSendButton.click();
460
+ return true;
461
+ }
462
+ }
463
+
424
464
  const composer = findComposer();
425
465
  if (!(composer instanceof HTMLElement)) return false;
426
466
 
@@ -1076,6 +1116,8 @@ export const __test__ = {
1076
1116
  clickSendButtonScript,
1077
1117
  composerStateScript,
1078
1118
  detectDoubaoVerificationScript,
1119
+ getTurnsScript,
1120
+ getTranscriptLinesScript,
1079
1121
  };
1080
1122
  export async function startNewDoubaoChat(page) {
1081
1123
  await ensureDoubaoChatPage(page);
@@ -144,6 +144,25 @@ describe('doubao send strategy', () => {
144
144
  await expect(sendDoubaoMessage(page, '你好')).rejects.toBeInstanceOf(CommandExecutionError);
145
145
  });
146
146
  });
147
+ describe('doubao receive strategy', () => {
148
+ it('keeps both the new skin selectors and the older structural fallbacks in the turns script', () => {
149
+ const turnsScript = __test__.getTurnsScript();
150
+ expect(turnsScript).toContain('[class*="message-list-S2Fv2S"]');
151
+ expect(turnsScript).toContain('.container-PvPoAn');
152
+ expect(turnsScript).toContain('[data-testid="message-list"]');
153
+ expect(turnsScript).toContain('[class*="bg-g-receive-msg-bubble"]');
154
+ expect(turnsScript).toContain('[data-testid="receive_message"]');
155
+ expect(turnsScript).toContain('[data-foundation-type="receive-message-action-bar"]');
156
+ expect(turnsScript).toContain('[data-testid="union_message"]');
157
+ expect(turnsScript).toContain('[data-testid="message-block-container"]');
158
+ });
159
+
160
+ it('extends transcript-noise cleanup for the current zh-CN chrome copy', () => {
161
+ const transcriptScript = __test__.getTranscriptLinesScript();
162
+ expect(transcriptScript).toContain('请仔细甄别');
163
+ expect(transcriptScript).toContain('下载电脑版');
164
+ });
165
+ });
147
166
  describe('collectDoubaoTranscriptAdditions', () => {
148
167
  it('ignores landing-page capability chips that are not assistant content', () => {
149
168
  const before = ['older'];
@@ -202,9 +221,10 @@ describe('collectDoubaoTranscriptAdditions', () => {
202
221
  expect(collectDoubaoTranscriptAdditions(before, current, '测试一下,只回复OK', (value) => value.replace('测试一下,只回复OK', '').trim())).toBe('');
203
222
  });
204
223
  it('treats only the exact landing-page chip string as UI noise', () => {
205
- expect(__test__.clickSendButtonScript()).not.toContain('document,');
224
+ expect(__test__.clickSendButtonScript()).toContain("button#flow-end-msg-send");
225
+ expect(__test__.clickSendButtonScript()).toContain("getAttribute('disabled') !== null");
226
+ expect(__test__.clickSendButtonScript()).toContain("getAttribute('aria-disabled') === 'true'");
206
227
  expect(__test__.clickSendButtonScript()).toContain('bestScore >= 200');
207
- expect(__test__.clickSendButtonScript()).not.toContain("|| !!button.closest('.chat-input-button')");
208
228
  expect(__test__.clickSendButtonScript()).toContain("button.getAttribute('type') === 'submit') score += 1200");
209
229
  expect(__test__.composerStateScript()).toContain("(composer.innerText || '').trim() || (composer.textContent || '').trim()");
210
230
  expect(__test__.detectDoubaoVerificationScript()).not.toContain('document.body?.innerText');
@@ -18,7 +18,7 @@ cli({
18
18
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
19
19
  ],
20
20
  columns: ['time', 'code', 'name', 'title', 'category', 'url'],
21
- func: async (_page, args) => {
21
+ func: async (args) => {
22
22
  const market = String(args.market ?? 'SHA,SZA,BJA').trim() || 'SHA,SZA,BJA';
23
23
  const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
24
24
 
@@ -28,7 +28,7 @@ cli({
28
28
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
29
29
  ],
30
30
  columns: ['rank', 'bondCode', 'bondName', 'bondPrice', 'bondChangePct', 'stockCode', 'stockName', 'stockPrice', 'stockChangePct', 'convPrice', 'convValue', 'convPremiumPct', 'remainingYears', 'ytm', 'listDate'],
31
- func: async (_page, args) => {
31
+ func: async (args) => {
32
32
  const sortKey = String(args.sort ?? 'turnover').toLowerCase();
33
33
  const sort = SORTS[sortKey];
34
34
  if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
@@ -26,7 +26,7 @@ cli({
26
26
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
27
27
  ],
28
28
  columns: ['rank', 'code', 'name', 'price', 'changePercent', 'change', 'turnover', 'volume', 'turnoverRate'],
29
- func: async (_page, args) => {
29
+ func: async (args) => {
30
30
  const sortKey = String(args.sort ?? 'turnover').toLowerCase();
31
31
  const sort = SORTS[sortKey];
32
32
  if (!sort) throw new CliError('INVALID_ARGUMENT', `Unknown sort "${sortKey}". Valid: ${Object.keys(SORTS).join(', ')}`);
@@ -37,7 +37,7 @@ cli({
37
37
  { name: 'limit', type: 'int', default: 10, help: '返回股东数(默认十大流通股东)' },
38
38
  ],
39
39
  columns: ['rank', 'reportDate', 'name', 'holdNum', 'floatRatio', 'change'],
40
- func: async (_page, args) => {
40
+ func: async (args) => {
41
41
  /** @type {string} */
42
42
  let secucode;
43
43
  try { secucode = toSecucode(args.symbol); }
@@ -47,7 +47,7 @@ cli({
47
47
  },
48
48
  ],
49
49
  columns: ['code', 'name', 'price', 'changePercent', 'change', 'open', 'high', 'low', 'prevClose'],
50
- func: async (_page, args) => {
50
+ func: async (args) => {
51
51
  const group = String(args.group ?? 'main').toLowerCase();
52
52
  /** @type {[string,string][]} */
53
53
  let entries;
@@ -36,7 +36,7 @@ cli({
36
36
  { name: 'limit', type: 'int', default: 30, help: '返回最近 N 根(末尾)' },
37
37
  ],
38
38
  columns: ['date', 'open', 'close', 'high', 'low', 'volume', 'turnover', 'amplitude', 'changePercent', 'change', 'turnoverRate'],
39
- func: async (_page, args) => {
39
+ func: async (args) => {
40
40
  const secid = resolveSecid(args.symbol);
41
41
  const periodKey = String(args.period ?? 'day').toLowerCase();
42
42
  const klt = PERIOD_MAP[periodKey];
@@ -26,7 +26,7 @@ cli({
26
26
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
27
27
  ],
28
28
  columns: ['time', 'title', 'summary', 'stocks'],
29
- func: async (_page, args) => {
29
+ func: async (args) => {
30
30
  const column = String(args.column ?? '102').trim();
31
31
  const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
32
32
 
@@ -26,7 +26,7 @@ cli({
26
26
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
27
27
  ],
28
28
  columns: ['tradeDate', 'code', 'name', 'closePrice', 'changeRate', 'boardAmt', 'buyAmt', 'sellAmt', 'netAmt', 'turnover', 'dealRatio', 'market', 'reason'],
29
- func: async (_page, args) => {
29
+ func: async (args) => {
30
30
  const sinceDate = String(args.date || '').trim() || defaultTradeDate();
31
31
  const limit = Math.max(1, Math.min(Number(args.limit) || 20, 100));
32
32
 
@@ -28,7 +28,7 @@ cli({
28
28
  { name: 'limit', type: 'int', default: 20, help: '返回数量 (max 100)' },
29
29
  ],
30
30
  columns: ['rank', 'code', 'name', 'price', 'changePercent', 'mainNet', 'mainNetRatio', 'superNet', 'bigNet', 'mediumNet', 'smallNet'],
31
- func: async (_page, args) => {
31
+ func: async (args) => {
32
32
  const rangeKey = String(args.range ?? 'today').toLowerCase();
33
33
  const range = RANGES[rangeKey];
34
34
  if (!range) {
@@ -19,7 +19,7 @@ cli({
19
19
  { name: 'limit', type: 'int', default: 10, help: '返回最近 N 分钟' },
20
20
  ],
21
21
  columns: ['time', 'cumulativeNetYi', 'minuteNetYi', 'totalNetYi'],
22
- func: async (_page, args) => {
22
+ func: async (args) => {
23
23
  const dir = String(args.direction ?? 'north').toLowerCase();
24
24
  if (!['north', 'south', 'n', 's'].includes(dir)) {
25
25
  throw new CliError('INVALID_ARGUMENT', `Unknown direction "${dir}". Valid: north / south`);