@jackwener/opencli 1.7.3 → 1.7.5

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 (197) hide show
  1. package/README.md +81 -59
  2. package/README.zh-CN.md +93 -67
  3. package/cli-manifest.json +5015 -2975
  4. package/clis/antigravity/serve.js +71 -25
  5. package/clis/baidu-scholar/search.js +87 -0
  6. package/clis/baidu-scholar/search.test.js +23 -0
  7. package/clis/bilibili/favorite.js +18 -13
  8. package/clis/binance/depth.js +3 -4
  9. package/clis/boss/utils.js +2 -3
  10. package/clis/chatgpt-app/ax.js +6 -3
  11. package/clis/deepseek/ask.js +74 -0
  12. package/clis/deepseek/history.js +25 -0
  13. package/clis/deepseek/new.js +20 -0
  14. package/clis/deepseek/read.js +22 -0
  15. package/clis/deepseek/status.js +24 -0
  16. package/clis/deepseek/utils.js +208 -0
  17. package/clis/douban/search.js +1 -0
  18. package/clis/douban/search.test.js +11 -0
  19. package/clis/douban/subject.js +20 -93
  20. package/clis/douban/subject.test.js +11 -0
  21. package/clis/douban/utils.js +250 -8
  22. package/clis/douban/utils.test.js +179 -4
  23. package/clis/doubao/utils.js +319 -130
  24. package/clis/doubao/utils.test.js +241 -2
  25. package/clis/eastmoney/_secid.js +78 -0
  26. package/clis/eastmoney/announcement.js +52 -0
  27. package/clis/eastmoney/convertible.js +73 -0
  28. package/clis/eastmoney/etf.js +65 -0
  29. package/clis/eastmoney/holders.js +78 -0
  30. package/clis/eastmoney/hot-rank.js +50 -0
  31. package/clis/eastmoney/hot-rank.test.js +59 -0
  32. package/clis/eastmoney/index-board.js +96 -0
  33. package/clis/eastmoney/kline.js +87 -0
  34. package/clis/eastmoney/kuaixun.js +54 -0
  35. package/clis/eastmoney/longhu.js +67 -0
  36. package/clis/eastmoney/money-flow.js +78 -0
  37. package/clis/eastmoney/northbound.js +57 -0
  38. package/clis/eastmoney/quote.js +107 -0
  39. package/clis/eastmoney/rank.js +94 -0
  40. package/clis/eastmoney/sectors.js +76 -0
  41. package/clis/google-scholar/search.js +58 -0
  42. package/clis/google-scholar/search.test.js +23 -0
  43. package/clis/gov-law/commands.test.js +39 -0
  44. package/clis/gov-law/recent.js +22 -0
  45. package/clis/gov-law/search.js +41 -0
  46. package/clis/gov-law/shared.js +51 -0
  47. package/clis/gov-policy/commands.test.js +27 -0
  48. package/clis/gov-policy/recent.js +47 -0
  49. package/clis/gov-policy/search.js +48 -0
  50. package/clis/grok/image.test.ts +107 -0
  51. package/clis/grok/image.ts +356 -0
  52. package/clis/nowcoder/companies.js +23 -0
  53. package/clis/nowcoder/creators.js +27 -0
  54. package/clis/nowcoder/detail.js +61 -0
  55. package/clis/nowcoder/experience.js +36 -0
  56. package/clis/nowcoder/hot.js +24 -0
  57. package/clis/nowcoder/jobs.js +21 -0
  58. package/clis/nowcoder/notifications.js +29 -0
  59. package/clis/nowcoder/papers.js +40 -0
  60. package/clis/nowcoder/practice.js +37 -0
  61. package/clis/nowcoder/recommend.js +30 -0
  62. package/clis/nowcoder/referral.js +39 -0
  63. package/clis/nowcoder/salary.js +40 -0
  64. package/clis/nowcoder/search.js +49 -0
  65. package/clis/nowcoder/suggest.js +33 -0
  66. package/clis/nowcoder/topics.js +27 -0
  67. package/clis/nowcoder/trending.js +25 -0
  68. package/clis/tdx/hot-rank.js +47 -0
  69. package/clis/tdx/hot-rank.test.js +59 -0
  70. package/clis/ths/hot-rank.js +49 -0
  71. package/clis/ths/hot-rank.test.js +64 -0
  72. package/clis/twitter/bookmarks.js +2 -1
  73. package/clis/twitter/list-add.js +337 -0
  74. package/clis/twitter/list-add.test.js +15 -0
  75. package/clis/twitter/list-remove.js +297 -0
  76. package/clis/twitter/list-remove.test.js +14 -0
  77. package/clis/twitter/list-tweets.js +185 -0
  78. package/clis/twitter/list-tweets.test.js +108 -0
  79. package/clis/twitter/lists.js +134 -47
  80. package/clis/twitter/lists.test.js +105 -38
  81. package/clis/uiverse/_shared.js +368 -0
  82. package/clis/uiverse/_shared.test.js +55 -0
  83. package/clis/uiverse/code.js +47 -0
  84. package/clis/uiverse/preview.js +71 -0
  85. package/clis/wanfang/search.js +66 -0
  86. package/clis/wanfang/search.test.js +23 -0
  87. package/clis/web/read.js +1 -1
  88. package/clis/weixin/download.js +3 -2
  89. package/clis/xiaohongshu/comments.js +2 -2
  90. package/clis/xiaohongshu/comments.test.js +46 -25
  91. package/clis/xiaohongshu/download.js +6 -7
  92. package/clis/xiaohongshu/download.test.js +17 -5
  93. package/clis/xiaohongshu/note-helpers.js +46 -12
  94. package/clis/xiaohongshu/note.js +3 -5
  95. package/clis/xiaohongshu/note.test.js +52 -25
  96. package/clis/xiaohongshu/publish.js +149 -28
  97. package/clis/xiaohongshu/publish.test.js +319 -6
  98. package/clis/xiaoyuzhou/auth.js +303 -0
  99. package/clis/xiaoyuzhou/auth.test.js +124 -0
  100. package/clis/xiaoyuzhou/download.js +53 -0
  101. package/clis/xiaoyuzhou/download.test.js +135 -0
  102. package/clis/xiaoyuzhou/episode.js +9 -4
  103. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  104. package/clis/xiaoyuzhou/podcast.js +9 -4
  105. package/clis/xiaoyuzhou/transcript.js +76 -0
  106. package/clis/xiaoyuzhou/transcript.test.js +195 -0
  107. package/clis/xiaoyuzhou/utils.js +0 -40
  108. package/clis/xiaoyuzhou/utils.test.js +15 -75
  109. package/clis/youtube/feed.js +120 -0
  110. package/clis/youtube/history.js +118 -0
  111. package/clis/youtube/like.js +62 -0
  112. package/clis/youtube/playlist.js +97 -0
  113. package/clis/youtube/subscribe.js +71 -0
  114. package/clis/youtube/subscriptions.js +57 -0
  115. package/clis/youtube/unlike.js +62 -0
  116. package/clis/youtube/unsubscribe.js +71 -0
  117. package/clis/youtube/utils.js +122 -0
  118. package/clis/youtube/utils.test.js +32 -1
  119. package/clis/youtube/watch-later.js +76 -0
  120. package/clis/zsxq/dynamics.js +1 -1
  121. package/clis/zsxq/utils.js +6 -3
  122. package/clis/zsxq/utils.test.js +31 -0
  123. package/dist/src/browser/base-page.d.ts +1 -1
  124. package/dist/src/browser/base-page.js +25 -5
  125. package/dist/src/browser/bridge.d.ts +3 -0
  126. package/dist/src/browser/bridge.js +52 -15
  127. package/dist/src/browser/cdp.js +2 -1
  128. package/dist/src/browser/daemon-client.d.ts +7 -4
  129. package/dist/src/browser/daemon-client.js +6 -1
  130. package/dist/src/browser/daemon-client.test.js +40 -1
  131. package/dist/src/browser/dom-snapshot.js +20 -3
  132. package/dist/src/browser/page.d.ts +18 -5
  133. package/dist/src/browser/page.js +96 -15
  134. package/dist/src/browser/page.test.js +158 -1
  135. package/dist/src/browser/target-errors.d.ts +23 -0
  136. package/dist/src/browser/target-errors.js +29 -0
  137. package/dist/src/browser/target-errors.test.js +61 -0
  138. package/dist/src/browser/target-resolver.d.ts +57 -0
  139. package/dist/src/browser/target-resolver.js +298 -0
  140. package/dist/src/browser/target-resolver.test.js +43 -0
  141. package/dist/src/browser.test.js +38 -1
  142. package/dist/src/cli.js +272 -187
  143. package/dist/src/cli.test.js +167 -90
  144. package/dist/src/commanderAdapter.d.ts +0 -1
  145. package/dist/src/commanderAdapter.js +2 -16
  146. package/dist/src/commanderAdapter.test.js +1 -1
  147. package/dist/src/commands/daemon.d.ts +4 -2
  148. package/dist/src/commands/daemon.js +22 -2
  149. package/dist/src/commands/daemon.test.js +65 -2
  150. package/dist/src/completion-shared.js +2 -5
  151. package/dist/src/daemon.js +10 -0
  152. package/dist/src/doctor.d.ts +1 -0
  153. package/dist/src/doctor.js +32 -9
  154. package/dist/src/doctor.test.js +28 -12
  155. package/dist/src/download/article-download.d.ts +1 -0
  156. package/dist/src/download/article-download.js +3 -0
  157. package/dist/src/download/article-download.test.js +39 -0
  158. package/dist/src/external-clis.yaml +2 -2
  159. package/dist/src/logger.d.ts +2 -2
  160. package/dist/src/logger.js +3 -3
  161. package/dist/src/output.js +1 -5
  162. package/dist/src/output.test.js +0 -21
  163. package/dist/src/pipeline/steps/transform.js +1 -1
  164. package/dist/src/pipeline/template.d.ts +1 -0
  165. package/dist/src/pipeline/template.js +11 -3
  166. package/dist/src/pipeline/template.test.js +3 -0
  167. package/dist/src/pipeline/transform.test.js +14 -0
  168. package/dist/src/plugin.d.ts +8 -9
  169. package/dist/src/plugin.js +24 -28
  170. package/dist/src/plugin.test.js +16 -60
  171. package/dist/src/registry.d.ts +1 -0
  172. package/dist/src/registry.js +3 -2
  173. package/dist/src/registry.test.js +22 -0
  174. package/dist/src/types.d.ts +15 -6
  175. package/package.json +1 -1
  176. package/clis/twitter/lists-parser.js +0 -77
  177. package/clis/twitter/lists.d.ts +0 -5
  178. package/dist/src/cascade.d.ts +0 -46
  179. package/dist/src/cascade.js +0 -135
  180. package/dist/src/explore.d.ts +0 -99
  181. package/dist/src/explore.js +0 -402
  182. package/dist/src/generate-verified.d.ts +0 -105
  183. package/dist/src/generate-verified.js +0 -696
  184. package/dist/src/generate-verified.test.js +0 -925
  185. package/dist/src/generate.d.ts +0 -46
  186. package/dist/src/generate.js +0 -117
  187. package/dist/src/record.d.ts +0 -96
  188. package/dist/src/record.js +0 -657
  189. package/dist/src/record.test.js +0 -293
  190. package/dist/src/skill-generate.d.ts +0 -30
  191. package/dist/src/skill-generate.js +0 -75
  192. package/dist/src/skill-generate.test.js +0 -173
  193. package/dist/src/synthesize.d.ts +0 -97
  194. package/dist/src/synthesize.js +0 -208
  195. /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
  196. /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
  197. /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
@@ -54,6 +54,20 @@ function jsonResponse(res, status, data) {
54
54
  function sleep(ms) {
55
55
  return new Promise(resolve => setTimeout(resolve, ms));
56
56
  }
57
+ function parseTimeoutValue(val, label, fallback) {
58
+ if (val === undefined) {
59
+ return fallback;
60
+ }
61
+ const parsed = typeof val === 'number' ? val : parseInt(String(val), 10);
62
+ if (Number.isNaN(parsed) || parsed <= 0) {
63
+ console.error(`[serve] Invalid ${label}="${val}", using default ${fallback}s`);
64
+ return fallback;
65
+ }
66
+ return parsed;
67
+ }
68
+ function parseEnvTimeout(envVar, fallback) {
69
+ return parseTimeoutValue(process.env[envVar], envVar, fallback);
70
+ }
57
71
  // ─── DOM helpers ─────────────────────────────────────────────────────
58
72
  /**
59
73
  * Click the 'New Conversation' button to reset context.
@@ -267,41 +281,65 @@ async function waitForReply(page, beforeText, opts = {}) {
267
281
  let lastText = beforeText;
268
282
  let stableCount = 0;
269
283
  const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback
284
+ let reconnectCount = 0;
270
285
  while (Date.now() < deadline) {
271
- const generating = await isGenerating(page);
272
- const currentText = await getConversationText(page);
273
- const textChanged = currentText !== beforeText && currentText.length > 0;
274
- if (generating) {
275
- hasStartedGenerating = true;
276
- stableCount = 0; // Reset stability while generating
277
- }
278
- else {
279
- if (hasStartedGenerating) {
280
- // It actively generated and now it stopped -> DONE
281
- // Provide a small buffer to let React render the final message fully
282
- await sleep(500);
283
- return;
286
+ try {
287
+ const generating = await isGenerating(page);
288
+ const currentText = await getConversationText(page);
289
+ const textChanged = currentText !== beforeText && currentText.length > 0;
290
+ if (generating) {
291
+ hasStartedGenerating = true;
292
+ stableCount = 0; // Reset stability while generating
284
293
  }
285
- // Fallback: If it never showed "Generating/Cancel", but text changed and is stable
286
- if (textChanged) {
287
- if (currentText === lastText) {
288
- stableCount++;
289
- if (stableCount >= stableThreshold) {
290
- return; // Text has been stable for 2 seconds -> DONE
294
+ else {
295
+ if (hasStartedGenerating) {
296
+ // It actively generated and now it stopped -> DONE
297
+ // Provide a small buffer to let React render the final message fully
298
+ await sleep(500);
299
+ return page;
300
+ }
301
+ // Fallback: If it never showed "Generating/Cancel", but text changed and is stable
302
+ if (textChanged) {
303
+ if (currentText === lastText) {
304
+ stableCount++;
305
+ if (stableCount >= stableThreshold) {
306
+ return page; // Text has been stable for 2 seconds -> DONE
307
+ }
308
+ }
309
+ else {
310
+ stableCount = 0;
311
+ lastText = currentText;
291
312
  }
292
313
  }
293
- else {
314
+ }
315
+ }
316
+ catch (err) {
317
+ const msg = err.message || String(err);
318
+ const isSessionLoss = /closed|lost|not open|websocket/i.test(msg);
319
+ if (opts.reconnect && isSessionLoss && reconnectCount < 2) {
320
+ reconnectCount++;
321
+ console.error(`[serve] CDP session loss detected (${msg}), attempting to reconnect (${reconnectCount}/2)...`);
322
+ try {
323
+ page = await opts.reconnect();
324
+ // Reset stability tracking after reconnect
294
325
  stableCount = 0;
295
- lastText = currentText;
326
+ lastText = beforeText;
327
+ continue;
328
+ }
329
+ catch (reconnectErr) {
330
+ console.error(`[serve] Reconnection failed: ${reconnectErr.message}`);
331
+ throw err; // Throw original error if reconnection itself fails
296
332
  }
297
333
  }
334
+ throw err;
298
335
  }
299
336
  await sleep(pollInterval);
300
337
  }
301
- throw new Error('Timeout waiting for Antigravity reply');
338
+ throw new Error(`Timeout waiting for Antigravity reply after ${timeout / 1000}s`);
302
339
  }
303
340
  // ─── Request Handlers ────────────────────────────────────────────────
304
- async function handleMessages(body, page, bridge) {
341
+ async function handleMessages(body, page, opts = {}) {
342
+ const { bridge, timeout, reconnect } = opts;
305
343
  // Extract the last user message
306
344
  const userMessages = body.messages.filter(m => m.role === 'user');
307
345
  if (userMessages.length === 0) {
@@ -328,7 +366,7 @@ async function handleMessages(body, page, bridge) {
328
366
  await sendMessage(page, userText, bridge);
329
367
  // Poll for reply (change detection)
330
368
  console.error('[serve] Waiting for reply...');
331
- await waitForReply(page, beforeText);
369
+ page = await waitForReply(page, beforeText, { timeout, reconnect });
332
370
  // Extract the actual reply text precisely from the DOM
333
371
  const replyText = await getLastAssistantReply(page, userText);
334
372
  console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
@@ -349,6 +387,10 @@ async function handleMessages(body, page, bridge) {
349
387
  // ─── Server ──────────────────────────────────────────────────────────
350
388
  export async function startServe(opts = {}) {
351
389
  const port = opts.port ?? 8082;
390
+ const envTimeoutSeconds = parseEnvTimeout('OPENCLI_ANTIGRAVITY_TIMEOUT', 120);
391
+ const effectiveTimeoutSeconds = parseTimeoutValue(opts.timeout, '--timeout', envTimeoutSeconds);
392
+ const effectiveTimeout = effectiveTimeoutSeconds * 1000;
393
+ console.error(`[serve] Starting Antigravity API proxy on port ${port} (timeout: ${effectiveTimeout / 1000}s)`);
352
394
  // Lazy CDP connection — connect when first request comes in
353
395
  let cdp = null;
354
396
  let page = null;
@@ -462,7 +504,11 @@ export async function startServe(opts = {}) {
462
504
  }
463
505
  // Lazy connect on first request
464
506
  const activePage = await ensureConnected();
465
- const response = await handleMessages(body, activePage, cdp ?? undefined);
507
+ const response = await handleMessages(body, activePage, {
508
+ bridge: cdp,
509
+ timeout: effectiveTimeout,
510
+ reconnect: ensureConnected,
511
+ });
466
512
  jsonResponse(res, 200, response);
467
513
  }
468
514
  finally {
@@ -0,0 +1,87 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
3
+
4
+ cli({
5
+ site: 'baidu-scholar',
6
+ name: 'search',
7
+ description: '百度学术搜索',
8
+ domain: 'xueshu.baidu.com',
9
+ strategy: Strategy.PUBLIC,
10
+ browser: true,
11
+ args: [
12
+ { name: 'query', positional: true, required: true, help: '搜索关键词' },
13
+ { name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
14
+ ],
15
+ columns: ['rank', 'title', 'authors', 'journal', 'year', 'cited', 'url'],
16
+ navigateBefore: false,
17
+ func: async (page, kwargs) => {
18
+ const limit = clampInt(kwargs.limit, 10, 1, 20);
19
+ const query = requireNonEmptyQuery(kwargs.query);
20
+ await page.goto(`https://xueshu.baidu.com/s?wd=${encodeURIComponent(query)}&pn=0&tn=SE_baiduxueshu_c1gjeupa`);
21
+ await page.wait(5);
22
+ const data = await page.evaluate(`
23
+ (async () => {
24
+ const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
25
+ for (let i = 0; i < 20; i++) {
26
+ if (document.querySelectorAll('.result').length > 0) break;
27
+ await new Promise(r => setTimeout(r, 500));
28
+ }
29
+ const results = [];
30
+ for (const el of document.querySelectorAll('.result')) {
31
+ const titleEl = el.querySelector('h3 a, .paper-title a, .t a');
32
+ const title = normalize(titleEl?.textContent);
33
+ if (!title) continue;
34
+
35
+ let url = titleEl?.getAttribute('href') || '';
36
+ if (url && !url.startsWith('http')) url = 'https://xueshu.baidu.com' + url;
37
+
38
+ const infoEl = el.querySelector('.paper-info');
39
+ const infoText = normalize(infoEl?.textContent);
40
+ const spans = infoEl ? Array.from(infoEl.querySelectorAll('span')) : [];
41
+
42
+ let journal = '';
43
+ let year = '';
44
+ let cited = '0';
45
+ const authorParts = [];
46
+
47
+ for (const span of spans) {
48
+ const text = normalize(span.textContent);
49
+ if (!text || text === ',' || text === ',') continue;
50
+ if (text.startsWith('《') || text.startsWith('〈')) {
51
+ journal = text.replace(/[《》〈〉]/g, '');
52
+ continue;
53
+ }
54
+ if (/^被引量[::]/.test(text)) {
55
+ cited = text.match(/(\\d+)/)?.[1] || '0';
56
+ continue;
57
+ }
58
+ if (/^-\\s*(\\d{4})/.test(text) || /^\\d{4}年?$/.test(text)) {
59
+ year = text.match(/(\\d{4})/)?.[1] || '';
60
+ continue;
61
+ }
62
+ if (!journal && !/^被引/.test(text) && !text.startsWith('-')) {
63
+ authorParts.push(text);
64
+ }
65
+ }
66
+
67
+ if (!year) year = infoText.match(/(19|20)\\d{2}/)?.[0] || '';
68
+ if (!cited || cited === '0') cited = infoText.match(/被引量[::]\\s*(\\d+)/)?.[1] || '0';
69
+
70
+ results.push({
71
+ rank: results.length + 1,
72
+ title,
73
+ authors: authorParts.join(', ').slice(0, 80),
74
+ journal,
75
+ year,
76
+ cited,
77
+ url,
78
+ });
79
+
80
+ if (results.length >= ${limit}) break;
81
+ }
82
+ return results;
83
+ })()
84
+ `);
85
+ return Array.isArray(data) ? data : [];
86
+ },
87
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '@jackwener/opencli/registry';
3
+ import './search.js';
4
+
5
+ describe('baidu-scholar search command', () => {
6
+ const command = getRegistry().get('baidu-scholar/search');
7
+
8
+ it('registers as a public browser command', () => {
9
+ expect(command).toBeDefined();
10
+ expect(command.site).toBe('baidu-scholar');
11
+ expect(command.strategy).toBe('public');
12
+ expect(command.browser).toBe(true);
13
+ });
14
+
15
+ it('rejects empty queries before browser navigation', async () => {
16
+ const page = { goto: vi.fn() };
17
+ await expect(command.func(page, { query: ' ' })).rejects.toMatchObject({
18
+ name: 'ArgumentError',
19
+ code: 'ARGUMENT',
20
+ });
21
+ expect(page.goto).not.toHaveBeenCalled();
22
+ });
23
+ });
@@ -3,27 +3,32 @@ import { apiGet, payloadData, getSelfUid } from './utils.js';
3
3
  cli({
4
4
  site: 'bilibili',
5
5
  name: 'favorite',
6
- description: '我的默认收藏夹',
6
+ description: '我的收藏夹',
7
7
  domain: 'www.bilibili.com',
8
8
  strategy: Strategy.COOKIE,
9
9
  args: [
10
+ { name: 'fid', type: 'int', required: false, help: 'Favorite folder ID (defaults to first folder)' },
10
11
  { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
11
12
  { name: 'page', type: 'int', default: 1, help: 'Page number' },
12
13
  ],
13
14
  columns: ['rank', 'title', 'author', 'plays', 'url'],
14
15
  func: async (page, kwargs) => {
15
- const { limit = 20, page: pageNum = 1 } = kwargs;
16
- // Get current user's UID
17
- const uid = await getSelfUid(page);
18
- // Get default favorite folder ID
19
- const foldersPayload = await apiGet(page, '/x/v3/fav/folder/created/list-all', {
20
- params: { up_mid: uid },
21
- signed: true,
22
- });
23
- const folders = payloadData(foldersPayload)?.list ?? [];
24
- if (!folders.length)
25
- return [];
26
- const fid = folders[0].id;
16
+ const { fid: favoriteId, limit = 20, page: pageNum = 1 } = kwargs;
17
+ let fid;
18
+ if (favoriteId) {
19
+ fid = Number(favoriteId);
20
+ } else {
21
+ // Fall back to the default (first) favorite folder
22
+ const uid = await getSelfUid(page);
23
+ const foldersPayload = await apiGet(page, '/x/v3/fav/folder/created/list-all', {
24
+ params: { up_mid: uid },
25
+ signed: true,
26
+ });
27
+ const folders = payloadData(foldersPayload)?.list ?? [];
28
+ if (!folders.length)
29
+ return [];
30
+ fid = folders[0].id;
31
+ }
27
32
  // Fetch favorite items
28
33
  const payload = await apiGet(page, '/x/v3/fav/resource/list', {
29
34
  params: { media_id: fid, pn: pageNum, ps: Math.min(Number(limit), 40) },
@@ -3,7 +3,7 @@ import { cli, Strategy } from '@jackwener/opencli/registry';
3
3
  cli({
4
4
  site: 'binance',
5
5
  name: 'depth',
6
- description: 'Order book bid prices for a trading pair',
6
+ description: 'Order book bid and ask prices for a trading pair',
7
7
  domain: 'data-api.binance.vision',
8
8
  strategy: Strategy.PUBLIC,
9
9
  browser: false,
@@ -11,11 +11,10 @@ cli({
11
11
  { name: 'symbol', type: 'str', required: true, positional: true, help: 'Trading pair symbol (e.g. BTCUSDT, ETHUSDT)' },
12
12
  { name: 'limit', type: 'int', default: 10, help: 'Number of price levels (5, 10, 20, 50, 100)' },
13
13
  ],
14
- columns: ['rank', 'bid_price', 'bid_qty'],
14
+ columns: ['rank', 'bid_price', 'bid_qty', 'ask_price', 'ask_qty'],
15
15
  pipeline: [
16
16
  { fetch: { url: 'https://data-api.binance.vision/api/v3/depth?symbol=${{ args.symbol }}&limit=${{ args.limit }}' } },
17
- { select: 'bids' },
18
- { map: { rank: '${{ index + 1 }}', bid_price: '${{ item.0 }}', bid_qty: '${{ item.1 }}' } },
17
+ { map: { select: 'bids', rank: '${{ index + 1 }}', bid_price: '${{ item[0] }}', bid_qty: '${{ item[1] }}', ask_price: '${{ root.asks[index]?.[0] ?? "" }}', ask_qty: '${{ root.asks[index]?.[1] ?? "" }}' } },
19
18
  { limit: '${{ args.limit }}' },
20
19
  ],
21
20
  });
@@ -214,11 +214,10 @@ export async function typeAndSendMessage(page, text) {
214
214
  return true;
215
215
  }
216
216
  /**
217
- * Verbose log helper — prints when OPENCLI_VERBOSE is set, with DEBUG=opencli
218
- * kept as a compatibility fallback.
217
+ * Verbose log helper — prints when OPENCLI_VERBOSE is set.
219
218
  */
220
219
  export function verbose(msg) {
221
- if (process.env.OPENCLI_VERBOSE || process.env.DEBUG?.includes('opencli')) {
220
+ if (process.env.OPENCLI_VERBOSE) {
222
221
  console.error(`[opencli:boss] ${msg}`);
223
222
  }
224
223
  }
@@ -121,11 +121,14 @@ let args = CommandLine.arguments
121
121
  let target = args.count > 1 ? args[1] : ""
122
122
  let needsLegacy = args.count > 2 && args[2] == "legacy"
123
123
 
124
- // Step 1: Click the "Options" button to open the popover
125
- guard let optionsBtn = findByDesc(win, "Options") else {
124
+ // Step 1: Click the "Options" button to open the popover (support both English and Chinese UI)
125
+ var optionsBtn: AXUIElement? = nil
126
+ if let btn = findByDesc(win, "Options") { optionsBtn = btn }
127
+ else if let btn = findByDesc(win, "选项") { optionsBtn = btn }
128
+ guard let options = optionsBtn else {
126
129
  fputs("Could not find Options button\\n", stderr); exit(1)
127
130
  }
128
- press(optionsBtn)
131
+ press(options)
129
132
  Thread.sleep(forTimeInterval: 0.8)
130
133
 
131
134
  // Step 2: Find the popover that appeared, search ONLY within it
@@ -0,0 +1,74 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import {
4
+ DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
5
+ sendMessage, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
6
+ } from './utils.js';
7
+
8
+ export const askCommand = cli({
9
+ site: 'deepseek',
10
+ name: 'ask',
11
+ description: 'Send a prompt to DeepSeek and get the response',
12
+ domain: DEEPSEEK_DOMAIN,
13
+ strategy: Strategy.COOKIE,
14
+ browser: true,
15
+ navigateBefore: false,
16
+ timeoutSeconds: 180,
17
+ args: [
18
+ { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
19
+ { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
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' },
22
+ { name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' },
23
+ { name: 'search', type: 'boolean', default: false, help: 'Enable web search' },
24
+ ],
25
+ columns: ['response'],
26
+
27
+ func: async (page, kwargs) => {
28
+ const prompt = kwargs.prompt;
29
+ const timeoutMs = (kwargs.timeout || 120) * 1000;
30
+ const wantThink = parseBoolFlag(kwargs.think);
31
+ const wantSearch = parseBoolFlag(kwargs.search);
32
+
33
+ if (parseBoolFlag(kwargs.new)) {
34
+ await page.goto(DEEPSEEK_URL);
35
+ await page.wait(3);
36
+ } else {
37
+ await ensureOnDeepSeek(page);
38
+ }
39
+
40
+ await page.wait(2);
41
+
42
+ const wantModel = kwargs.model || 'instant';
43
+ const modelResult = await withRetry(() => selectModel(page, wantModel));
44
+ if (!modelResult?.ok) {
45
+ throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
46
+ }
47
+ if (modelResult.toggled) await page.wait(0.5);
48
+
49
+ const thinkResult = await withRetry(() => setFeature(page, 'DeepThink', wantThink));
50
+ if (!thinkResult?.ok) {
51
+ throw new CommandExecutionError('Could not toggle DeepThink');
52
+ }
53
+
54
+ const searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch));
55
+ if (!searchResult?.ok) {
56
+ throw new CommandExecutionError('Could not toggle Search');
57
+ }
58
+
59
+ if (thinkResult.toggled || searchResult.toggled) await page.wait(0.5);
60
+
61
+ const baseline = await withRetry(() => getBubbleCount(page));
62
+ const sendResult = await withRetry(() => sendMessage(page, prompt));
63
+ if (!sendResult?.ok) {
64
+ throw new CommandExecutionError(sendResult?.reason || 'Failed to send message');
65
+ }
66
+
67
+ const response = await waitForResponse(page, baseline, prompt, timeoutMs);
68
+ if (!response) {
69
+ return [{ response: `[NO RESPONSE] No reply within ${kwargs.timeout}s.` }];
70
+ }
71
+
72
+ return [{ response }];
73
+ },
74
+ });
@@ -0,0 +1,25 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { DEEPSEEK_DOMAIN, getConversationList } from './utils.js';
3
+
4
+ export const historyCommand = cli({
5
+ site: 'deepseek',
6
+ name: 'history',
7
+ description: 'List conversation history from DeepSeek sidebar',
8
+ domain: DEEPSEEK_DOMAIN,
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ navigateBefore: false,
12
+ args: [
13
+ { name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
14
+ ],
15
+ columns: ['Index', 'Title', 'Url'],
16
+
17
+ func: async (page, kwargs) => {
18
+ const limit = Math.max(1, kwargs.limit || 20);
19
+ const conversations = await getConversationList(page);
20
+ if (conversations.length === 0) {
21
+ return [{ Index: 0, Title: 'No conversation history found.', Url: '' }];
22
+ }
23
+ return conversations.slice(0, limit);
24
+ },
25
+ });
@@ -0,0 +1,20 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { DEEPSEEK_DOMAIN, DEEPSEEK_URL } from './utils.js';
3
+
4
+ export const newCommand = cli({
5
+ site: 'deepseek',
6
+ name: 'new',
7
+ description: 'Start a new conversation in DeepSeek',
8
+ domain: DEEPSEEK_DOMAIN,
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ navigateBefore: false,
12
+ args: [],
13
+ columns: ['Status'],
14
+
15
+ func: async (page) => {
16
+ await page.goto(DEEPSEEK_URL);
17
+ await page.wait(2);
18
+ return [{ Status: 'New chat started' }];
19
+ },
20
+ });
@@ -0,0 +1,22 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { DEEPSEEK_DOMAIN, ensureOnDeepSeek, getVisibleMessages } from './utils.js';
3
+
4
+ export const readCommand = cli({
5
+ site: 'deepseek',
6
+ name: 'read',
7
+ description: 'Read the current DeepSeek conversation',
8
+ domain: DEEPSEEK_DOMAIN,
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ navigateBefore: false,
12
+ args: [],
13
+ columns: ['Role', 'Text'],
14
+
15
+ func: async (page) => {
16
+ await ensureOnDeepSeek(page);
17
+ await page.wait(5);
18
+ const messages = await getVisibleMessages(page);
19
+ if (messages.length > 0) return messages;
20
+ return [{ Role: 'system', Text: 'No visible messages found.' }];
21
+ },
22
+ });
@@ -0,0 +1,24 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { DEEPSEEK_DOMAIN, ensureOnDeepSeek, getPageState } from './utils.js';
3
+
4
+ export const statusCommand = cli({
5
+ site: 'deepseek',
6
+ name: 'status',
7
+ description: 'Check DeepSeek page availability and login state',
8
+ domain: DEEPSEEK_DOMAIN,
9
+ strategy: Strategy.COOKIE,
10
+ browser: true,
11
+ navigateBefore: false,
12
+ args: [],
13
+ columns: ['Status', 'Login', 'Url'],
14
+
15
+ func: async (page) => {
16
+ await ensureOnDeepSeek(page);
17
+ const state = await getPageState(page);
18
+ return [{
19
+ Status: state.hasTextarea ? 'Connected' : 'Page not ready',
20
+ Login: state.isLoggedIn ? 'Yes' : 'No',
21
+ Url: state.url,
22
+ }];
23
+ },
24
+ });