@jackwener/opencli 1.7.4 → 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 (126) hide show
  1. package/README.md +71 -49
  2. package/README.zh-CN.md +73 -60
  3. package/cli-manifest.json +3261 -1758
  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/deepseek/ask.js +74 -0
  8. package/clis/deepseek/history.js +25 -0
  9. package/clis/deepseek/new.js +20 -0
  10. package/clis/deepseek/read.js +22 -0
  11. package/clis/deepseek/status.js +24 -0
  12. package/clis/deepseek/utils.js +208 -0
  13. package/clis/eastmoney/_secid.js +78 -0
  14. package/clis/eastmoney/announcement.js +52 -0
  15. package/clis/eastmoney/convertible.js +73 -0
  16. package/clis/eastmoney/etf.js +65 -0
  17. package/clis/eastmoney/holders.js +78 -0
  18. package/clis/eastmoney/index-board.js +96 -0
  19. package/clis/eastmoney/kline.js +87 -0
  20. package/clis/eastmoney/kuaixun.js +54 -0
  21. package/clis/eastmoney/longhu.js +67 -0
  22. package/clis/eastmoney/money-flow.js +78 -0
  23. package/clis/eastmoney/northbound.js +57 -0
  24. package/clis/eastmoney/quote.js +107 -0
  25. package/clis/eastmoney/rank.js +94 -0
  26. package/clis/eastmoney/sectors.js +76 -0
  27. package/clis/google-scholar/search.js +58 -0
  28. package/clis/google-scholar/search.test.js +23 -0
  29. package/clis/gov-law/commands.test.js +39 -0
  30. package/clis/gov-law/recent.js +22 -0
  31. package/clis/gov-law/search.js +41 -0
  32. package/clis/gov-law/shared.js +51 -0
  33. package/clis/gov-policy/commands.test.js +27 -0
  34. package/clis/gov-policy/recent.js +47 -0
  35. package/clis/gov-policy/search.js +48 -0
  36. package/clis/nowcoder/companies.js +23 -0
  37. package/clis/nowcoder/creators.js +27 -0
  38. package/clis/nowcoder/detail.js +61 -0
  39. package/clis/nowcoder/experience.js +36 -0
  40. package/clis/nowcoder/hot.js +24 -0
  41. package/clis/nowcoder/jobs.js +21 -0
  42. package/clis/nowcoder/notifications.js +29 -0
  43. package/clis/nowcoder/papers.js +40 -0
  44. package/clis/nowcoder/practice.js +37 -0
  45. package/clis/nowcoder/recommend.js +30 -0
  46. package/clis/nowcoder/referral.js +39 -0
  47. package/clis/nowcoder/salary.js +40 -0
  48. package/clis/nowcoder/search.js +49 -0
  49. package/clis/nowcoder/suggest.js +33 -0
  50. package/clis/nowcoder/topics.js +27 -0
  51. package/clis/nowcoder/trending.js +25 -0
  52. package/clis/twitter/list-add.js +337 -0
  53. package/clis/twitter/list-add.test.js +15 -0
  54. package/clis/twitter/list-remove.js +297 -0
  55. package/clis/twitter/list-remove.test.js +14 -0
  56. package/clis/twitter/list-tweets.js +185 -0
  57. package/clis/twitter/list-tweets.test.js +108 -0
  58. package/clis/twitter/lists.js +134 -47
  59. package/clis/twitter/lists.test.js +105 -38
  60. package/clis/wanfang/search.js +66 -0
  61. package/clis/wanfang/search.test.js +23 -0
  62. package/clis/web/read.js +1 -1
  63. package/clis/weixin/download.js +3 -2
  64. package/clis/xiaohongshu/publish.js +149 -28
  65. package/clis/xiaohongshu/publish.test.js +319 -6
  66. package/clis/xiaoyuzhou/download.js +8 -4
  67. package/clis/xiaoyuzhou/download.test.js +23 -13
  68. package/clis/xiaoyuzhou/episode.js +9 -4
  69. package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
  70. package/clis/xiaoyuzhou/podcast.js +9 -4
  71. package/clis/xiaoyuzhou/utils.js +0 -40
  72. package/clis/xiaoyuzhou/utils.test.js +15 -75
  73. package/clis/zsxq/dynamics.js +1 -1
  74. package/clis/zsxq/utils.js +6 -3
  75. package/clis/zsxq/utils.test.js +31 -0
  76. package/dist/src/browser/base-page.d.ts +1 -1
  77. package/dist/src/browser/bridge.d.ts +1 -0
  78. package/dist/src/browser/bridge.js +1 -1
  79. package/dist/src/browser/cdp.js +1 -1
  80. package/dist/src/browser/daemon-client.d.ts +6 -4
  81. package/dist/src/browser/daemon-client.js +6 -1
  82. package/dist/src/browser/daemon-client.test.js +40 -1
  83. package/dist/src/browser/dom-snapshot.js +7 -2
  84. package/dist/src/browser/page.d.ts +14 -4
  85. package/dist/src/browser/page.js +48 -7
  86. package/dist/src/browser/page.test.js +97 -0
  87. package/dist/src/cli.js +227 -150
  88. package/dist/src/cli.test.js +167 -90
  89. package/dist/src/commanderAdapter.d.ts +0 -1
  90. package/dist/src/commanderAdapter.js +2 -16
  91. package/dist/src/commanderAdapter.test.js +1 -1
  92. package/dist/src/completion-shared.js +2 -5
  93. package/dist/src/daemon.js +8 -0
  94. package/dist/src/download/article-download.d.ts +1 -0
  95. package/dist/src/download/article-download.js +3 -0
  96. package/dist/src/download/article-download.test.js +39 -0
  97. package/dist/src/plugin.d.ts +1 -8
  98. package/dist/src/plugin.js +1 -27
  99. package/dist/src/plugin.test.js +1 -59
  100. package/dist/src/registry.d.ts +1 -0
  101. package/dist/src/registry.js +3 -2
  102. package/dist/src/registry.test.js +22 -0
  103. package/dist/src/types.d.ts +14 -5
  104. package/package.json +1 -1
  105. package/clis/twitter/lists-parser.js +0 -77
  106. package/clis/twitter/lists.d.ts +0 -5
  107. package/dist/src/cascade.d.ts +0 -46
  108. package/dist/src/cascade.js +0 -135
  109. package/dist/src/explore.d.ts +0 -99
  110. package/dist/src/explore.js +0 -402
  111. package/dist/src/generate-verified.d.ts +0 -105
  112. package/dist/src/generate-verified.js +0 -696
  113. package/dist/src/generate-verified.test.js +0 -925
  114. package/dist/src/generate.d.ts +0 -46
  115. package/dist/src/generate.js +0 -117
  116. package/dist/src/record.d.ts +0 -96
  117. package/dist/src/record.js +0 -657
  118. package/dist/src/record.test.d.ts +0 -1
  119. package/dist/src/record.test.js +0 -293
  120. package/dist/src/skill-generate.d.ts +0 -30
  121. package/dist/src/skill-generate.js +0 -75
  122. package/dist/src/skill-generate.test.d.ts +0 -1
  123. package/dist/src/skill-generate.test.js +0 -173
  124. package/dist/src/synthesize.d.ts +0 -97
  125. package/dist/src/synthesize.js +0 -208
  126. /package/dist/src/{generate-verified.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
+ });
@@ -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
+ });
@@ -0,0 +1,208 @@
1
+ export const DEEPSEEK_DOMAIN = 'chat.deepseek.com';
2
+ export const DEEPSEEK_URL = 'https://chat.deepseek.com/';
3
+ export const TEXTAREA_SELECTOR = 'textarea[placeholder*="DeepSeek"]';
4
+ export const MESSAGE_SELECTOR = '.ds-message';
5
+
6
+ export async function isOnDeepSeek(page) {
7
+ const url = await page.evaluate('window.location.href').catch(() => '');
8
+ if (typeof url !== 'string' || !url) return false;
9
+ try {
10
+ const h = new URL(url).hostname;
11
+ return h === 'deepseek.com' || h.endsWith('.deepseek.com');
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export async function ensureOnDeepSeek(page) {
18
+ if (!(await isOnDeepSeek(page))) {
19
+ await page.goto(DEEPSEEK_URL);
20
+ await page.wait(3);
21
+ }
22
+ }
23
+
24
+ export async function getPageState(page) {
25
+ return page.evaluate(`(() => {
26
+ const url = window.location.href;
27
+ const title = document.title;
28
+ const textarea = document.querySelector('${TEXTAREA_SELECTOR}');
29
+ const avatar = document.querySelector('img[src*="user-avatar"]');
30
+ return {
31
+ url,
32
+ title,
33
+ hasTextarea: !!textarea,
34
+ isLoggedIn: !!avatar,
35
+ };
36
+ })()`);
37
+ }
38
+
39
+ export async function selectModel(page, modelName) {
40
+ return page.evaluate(`(() => {
41
+ const radios = document.querySelectorAll('div[role="radio"]');
42
+ for (const radio of radios) {
43
+ const span = radio.querySelector('span');
44
+ if (span && span.textContent.trim().toLowerCase() === '${modelName}'.toLowerCase()) {
45
+ const alreadySelected = radio.getAttribute('aria-checked') === 'true';
46
+ if (!alreadySelected) radio.click();
47
+ return { ok: true, toggled: !alreadySelected };
48
+ }
49
+ }
50
+ return { ok: false };
51
+ })()`);
52
+ }
53
+
54
+ export async function setFeature(page, featureName, enabled) {
55
+ return page.evaluate(`(() => {
56
+ const btns = document.querySelectorAll('div[role="button"]');
57
+ for (const btn of btns) {
58
+ const span = btn.querySelector('span');
59
+ if (span && span.textContent.trim() === '${featureName}') {
60
+ const isActive = btn.classList.contains('ds-toggle-button--selected');
61
+ if (${enabled} !== isActive) btn.click();
62
+ return { ok: true, toggled: ${enabled} !== isActive };
63
+ }
64
+ }
65
+ return { ok: false };
66
+ })()`);
67
+ }
68
+
69
+ export async function sendMessage(page, prompt) {
70
+ const promptJson = JSON.stringify(prompt);
71
+ return page.evaluate(`(async () => {
72
+ const box = document.querySelector('${TEXTAREA_SELECTOR}');
73
+ if (!box) return { ok: false, reason: 'textarea not found' };
74
+
75
+ box.focus();
76
+ box.value = '';
77
+ document.execCommand('selectAll');
78
+ document.execCommand('insertText', false, ${promptJson});
79
+ await new Promise(r => setTimeout(r, 800));
80
+
81
+ const btns = document.querySelectorAll('div[role="button"]');
82
+ for (const btn of btns) {
83
+ if (btn.getAttribute('aria-disabled') === 'false') {
84
+ const svgs = btn.querySelectorAll('svg');
85
+ if (svgs.length > 0 && btn.closest('div')?.querySelector('textarea')) {
86
+ btn.click();
87
+ return { ok: true };
88
+ }
89
+ }
90
+ }
91
+
92
+ box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
93
+ return { ok: true, method: 'enter' };
94
+ })()`);
95
+ }
96
+
97
+ export async function getBubbleCount(page) {
98
+ const count = await page.evaluate(`(() => {
99
+ return document.querySelectorAll('${MESSAGE_SELECTOR}').length;
100
+ })()`);
101
+ return count || 0;
102
+ }
103
+
104
+ export async function waitForResponse(page, baselineCount, prompt, timeoutMs) {
105
+ const startTime = Date.now();
106
+ let lastText = '';
107
+ let stableCount = 0;
108
+
109
+ while (Date.now() - startTime < timeoutMs) {
110
+ await page.wait(3);
111
+
112
+ let result;
113
+ try {
114
+ result = await page.evaluate(`(() => {
115
+ const bubbles = document.querySelectorAll('${MESSAGE_SELECTOR}');
116
+ const texts = Array.from(bubbles).map(b => (b.innerText || '').trim()).filter(Boolean);
117
+ return { count: texts.length, last: texts[texts.length - 1] || '' };
118
+ })()`);
119
+ } catch {
120
+ continue;
121
+ }
122
+
123
+ if (!result) continue;
124
+
125
+ const candidate = result.last;
126
+ if (candidate && result.count > baselineCount && candidate !== prompt.trim()) {
127
+ if (candidate === lastText) {
128
+ stableCount++;
129
+ if (stableCount >= 3) return candidate;
130
+ } else {
131
+ stableCount = 0;
132
+ }
133
+ lastText = candidate;
134
+ }
135
+ }
136
+
137
+ return lastText || null;
138
+ }
139
+
140
+ export async function getVisibleMessages(page) {
141
+ const result = await page.evaluate(`(() => {
142
+ const msgs = document.querySelectorAll('${MESSAGE_SELECTOR}');
143
+ return Array.from(msgs).map(m => {
144
+ // User messages carry an extra hash-class alongside ds-message
145
+ const isUser = m.className.split(/\\s+/).length > 2;
146
+ return {
147
+ Role: isUser ? 'user' : 'assistant',
148
+ Text: (m.innerText || '').trim(),
149
+ };
150
+ }).filter(m => m.Text);
151
+ })()`);
152
+ return Array.isArray(result) ? result : [];
153
+ }
154
+
155
+ export async function getConversationList(page) {
156
+ await ensureOnDeepSeek(page);
157
+ // Expand sidebar if collapsed
158
+ await page.evaluate(`(() => {
159
+ if (document.querySelectorAll('a[href*="/a/chat/s/"]').length === 0) {
160
+ const btn = document.querySelector('div[tabindex="0"][role="button"]');
161
+ if (btn) btn.click();
162
+ }
163
+ })()`);
164
+ // Poll for sidebar history links to render
165
+ for (let attempt = 0; attempt < 5; attempt++) {
166
+ await page.wait(2);
167
+ const items = await page.evaluate(`(() => {
168
+ const items = [];
169
+ const links = document.querySelectorAll('a[href*="/a/chat/s/"]');
170
+ links.forEach((link, i) => {
171
+ const titleEl = link.querySelector('div');
172
+ const title = titleEl ? titleEl.textContent.trim() : '';
173
+ const href = link.getAttribute('href') || '';
174
+ const idMatch = href.match(/\\/s\\/([a-f0-9-]+)/);
175
+ items.push({
176
+ Index: i + 1,
177
+ Id: idMatch ? idMatch[1] : href,
178
+ Title: title || '(untitled)',
179
+ Url: 'https://chat.deepseek.com' + href,
180
+ });
181
+ });
182
+ return items;
183
+ })()`);
184
+ if (Array.isArray(items) && items.length > 0) return items;
185
+ }
186
+ return [];
187
+ }
188
+
189
+ // Retries on CDP "Promise was collected" errors caused by DeepSeek's SPA router transitions.
190
+ export async function withRetry(fn, retries = 2) {
191
+ for (let i = 0; i <= retries; i++) {
192
+ try {
193
+ return await fn();
194
+ } catch (err) {
195
+ const msg = String(err?.message || err);
196
+ if (i < retries && msg.includes('Promise was collected')) {
197
+ await new Promise(r => setTimeout(r, 2000));
198
+ continue;
199
+ }
200
+ throw err;
201
+ }
202
+ }
203
+ }
204
+
205
+ export function parseBoolFlag(value) {
206
+ if (typeof value === 'boolean') return value;
207
+ return String(value ?? '').trim().toLowerCase() === 'true';
208
+ }