@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
@@ -1,6 +1,44 @@
1
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
2
+
1
3
  export const DOUBAO_DOMAIN = 'www.doubao.com';
2
4
  export const DOUBAO_CHAT_URL = 'https://www.doubao.com/chat';
3
5
  export const DOUBAO_NEW_CHAT_URL = 'https://www.doubao.com/chat/new-thread/create-by-msg';
6
+ const DOUBAO_COMPOSER_SELECTORS = [
7
+ 'textarea[data-testid="chat_input_input"]',
8
+ '[data-testid="chat_input"] textarea',
9
+ '.chat-input textarea',
10
+ '.chat-input [contenteditable="true"]',
11
+ '.chat-editor textarea',
12
+ '.chat-editor [contenteditable="true"]',
13
+ 'textarea[placeholder*="发消息"]',
14
+ 'textarea[placeholder*="Message"]',
15
+ '[contenteditable="true"][placeholder*="发消息"]',
16
+ '[contenteditable="true"][placeholder*="Message"]',
17
+ '[contenteditable="true"][aria-label*="发消息"]',
18
+ '[contenteditable="true"][aria-label*="Message"]',
19
+ 'textarea',
20
+ '[contenteditable="true"]',
21
+ ];
22
+ function buildDoubaoComposerLocatorScript() {
23
+ return `
24
+ const isVisible = (el) => {
25
+ if (!(el instanceof HTMLElement)) return false;
26
+ const style = window.getComputedStyle(el);
27
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
28
+ const rect = el.getBoundingClientRect();
29
+ return rect.width > 0 && rect.height > 0;
30
+ };
31
+
32
+ const composerSelectors = ${JSON.stringify(DOUBAO_COMPOSER_SELECTORS)};
33
+ const findComposer = () => {
34
+ for (const selector of composerSelectors) {
35
+ const node = Array.from(document.querySelectorAll(selector)).find(isVisible);
36
+ if (node) return node;
37
+ }
38
+ return null;
39
+ };
40
+ `;
41
+ }
4
42
  function getTranscriptLinesScript() {
5
43
  return `
6
44
  (() => {
@@ -205,42 +243,98 @@ function getTurnsScript() {
205
243
  })()
206
244
  `;
207
245
  }
208
- function fillComposerScript(text) {
246
+ function prepareDoubaoComposerScript() {
209
247
  return `
210
- ((inputText) => {
211
- const isVisible = (el) => {
212
- if (!(el instanceof HTMLElement)) return false;
213
- const style = window.getComputedStyle(el);
214
- if (style.display === 'none' || style.visibility === 'hidden') return false;
215
- const rect = el.getBoundingClientRect();
216
- return rect.width > 0 && rect.height > 0;
217
- };
248
+ (() => {
249
+ ${buildDoubaoComposerLocatorScript()}
250
+ const composer = findComposer();
218
251
 
219
- const candidates = [
220
- 'textarea[data-testid="chat_input_input"]',
221
- '.chat-input textarea',
222
- '.chat-input [contenteditable="true"]',
223
- '.chat-editor textarea',
224
- '.chat-editor [contenteditable="true"]',
225
- 'textarea[placeholder*="发消息"]',
226
- 'textarea[placeholder*="Message"]',
227
- '[contenteditable="true"][placeholder*="发消息"]',
228
- '[contenteditable="true"][placeholder*="Message"]',
229
- '[contenteditable="true"][aria-label*="发消息"]',
230
- '[contenteditable="true"][aria-label*="Message"]',
231
- 'textarea',
232
- '[contenteditable="true"]',
233
- ];
252
+ if (
253
+ !(composer instanceof HTMLTextAreaElement)
254
+ && !(composer instanceof HTMLInputElement)
255
+ && !(composer instanceof HTMLElement)
256
+ ) {
257
+ return { ok: false, reason: 'Could not find Doubao input element' };
258
+ }
234
259
 
235
- let composer = null;
236
- for (const selector of candidates) {
237
- const node = Array.from(document.querySelectorAll(selector)).find(isVisible);
238
- if (node) {
239
- composer = node;
240
- break;
260
+ try {
261
+ composer.focus();
262
+
263
+ if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) {
264
+ const length = composer.value.length;
265
+ composer.setSelectionRange(0, length);
266
+ } else {
267
+ const selection = window.getSelection();
268
+ const range = document.createRange();
269
+ range.selectNodeContents(composer);
270
+ selection?.removeAllRanges();
271
+ selection?.addRange(range);
241
272
  }
273
+ } catch (error) {
274
+ return {
275
+ ok: false,
276
+ reason: error instanceof Error ? error.message : String(error),
277
+ };
242
278
  }
243
279
 
280
+ return { ok: true };
281
+ })()
282
+ `;
283
+ }
284
+ function composerStateScript() {
285
+ return `
286
+ (() => {
287
+ ${buildDoubaoComposerLocatorScript()}
288
+ const composer = findComposer();
289
+
290
+ if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) {
291
+ return { hasText: !!composer.value.trim(), text: composer.value };
292
+ }
293
+
294
+ if (composer instanceof HTMLElement) {
295
+ const text = (composer.innerText || '').trim() || (composer.textContent || '').trim();
296
+ return {
297
+ hasText: !!text,
298
+ text,
299
+ };
300
+ }
301
+
302
+ return { hasText: false, text: '' };
303
+ })()
304
+ `;
305
+ }
306
+ function syncComposerAfterNativeTypeScript() {
307
+ return `
308
+ (() => {
309
+ ${buildDoubaoComposerLocatorScript()}
310
+ const composer = findComposer();
311
+
312
+ if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) {
313
+ const value = composer.value;
314
+ composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: value, inputType: 'insertText' }));
315
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
316
+ composer.dispatchEvent(new Event('change', { bubbles: true }));
317
+ return { hasText: !!value.trim(), text: value };
318
+ }
319
+
320
+ if (composer instanceof HTMLElement) {
321
+ const text = (composer.innerText || '').trim() || (composer.textContent || '').trim();
322
+ composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: text, inputType: 'insertText' }));
323
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: text, inputType: 'insertText' }));
324
+ composer.dispatchEvent(new Event('change', { bubbles: true }));
325
+ return { hasText: !!text, text };
326
+ }
327
+
328
+ return { hasText: false, text: '' };
329
+ })()
330
+ `;
331
+ }
332
+ function fillComposerScript(text) {
333
+ return `
334
+ ((inputText) => {
335
+ ${buildDoubaoComposerLocatorScript()}
336
+ const composer = findComposer();
337
+
244
338
  if (!composer) throw new Error('Could not find Doubao input element');
245
339
 
246
340
  composer.focus();
@@ -251,9 +345,10 @@ function fillComposerScript(text) {
251
345
  : window.HTMLInputElement.prototype;
252
346
  const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
253
347
  setter?.call(composer, inputText);
254
- composer.dispatchEvent(new Event('input', { bubbles: true }));
348
+ composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: inputText, inputType: 'insertText' }));
349
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
255
350
  composer.dispatchEvent(new Event('change', { bubbles: true }));
256
- return 'text-input';
351
+ return { hasText: !!composer.value.trim(), mode: 'text-input', text: composer.value };
257
352
  }
258
353
 
259
354
  if (composer instanceof HTMLElement) {
@@ -265,18 +360,23 @@ function fillComposerScript(text) {
265
360
  selection?.removeAllRanges();
266
361
  selection?.addRange(range);
267
362
  document.execCommand('insertText', false, inputText);
268
- composer.dispatchEvent(new Event('input', { bubbles: true }));
363
+ composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: inputText, inputType: 'insertText' }));
364
+ composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' }));
269
365
  composer.dispatchEvent(new Event('change', { bubbles: true }));
270
- return 'contenteditable';
366
+ return {
367
+ hasText: !!((composer.innerText || '').trim() || (composer.textContent || '').trim()),
368
+ mode: 'contenteditable',
369
+ text: (composer.innerText || '').trim() || (composer.textContent || '').trim(),
370
+ };
271
371
  }
272
372
 
273
373
  throw new Error('Unsupported Doubao input element');
274
374
  })(${JSON.stringify(text)})
275
375
  `;
276
376
  }
277
- function fillAndSubmitComposerScript(text) {
377
+ function detectDoubaoVerificationScript() {
278
378
  return `
279
- ((inputText) => {
379
+ (() => {
280
380
  const isVisible = (el) => {
281
381
  if (!(el instanceof HTMLElement)) return false;
282
382
  const style = window.getComputedStyle(el);
@@ -285,119 +385,115 @@ function fillAndSubmitComposerScript(text) {
285
385
  return rect.width > 0 && rect.height > 0;
286
386
  };
287
387
 
288
- const candidates = [
289
- 'textarea[data-testid="chat_input_input"]',
290
- '[data-testid="chat_input"] textarea',
291
- '.chat-input textarea',
292
- 'textarea[placeholder*="发消息"]',
293
- 'textarea[placeholder*="Message"]',
294
- 'textarea',
388
+ const challengeSelectors = [
389
+ 'iframe[src*="captcha"]',
390
+ 'iframe[src*="verify"]',
391
+ 'input[placeholder*="验证码"]',
392
+ 'input[aria-label*="验证码"]',
295
393
  ];
296
-
297
- let composer = null;
298
- for (const selector of candidates) {
299
- const node = Array.from(document.querySelectorAll(selector)).find(isVisible);
300
- if (node) {
301
- composer = node;
302
- break;
303
- }
304
- }
305
-
306
- if (!(composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement)) {
307
- throw new Error('Could not find Doubao textarea input element');
394
+ const selectorMatch = challengeSelectors.find((selector) => {
395
+ return Array.from(document.querySelectorAll(selector)).some((node) => isVisible(node));
396
+ });
397
+ if (selectorMatch) {
398
+ return { detected: true, reason: selectorMatch };
308
399
  }
309
400
 
310
- composer.focus();
311
- const proto = composer instanceof HTMLTextAreaElement
312
- ? window.HTMLTextAreaElement.prototype
313
- : window.HTMLInputElement.prototype;
314
- const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
315
- setter?.call(composer, inputText);
316
- composer.dispatchEvent(new Event('input', { bubbles: true }));
317
- composer.dispatchEvent(new Event('change', { bubbles: true }));
318
-
319
- const root = document.querySelector('[data-testid="chat_input"], .chat-input') || document.body;
320
- const buttons = Array.from(root.querySelectorAll('button, [role="button"]')).filter(isVisible);
321
- const target = buttons[buttons.length - 1];
322
-
323
- if (target) {
324
- target.click();
325
- return 'button';
326
- }
401
+ const phrasePattern = /人机验证|完成安全验证|异常访问|滑动验证|拖动滑块/i;
402
+ const candidateRoots = Array.from(
403
+ document.querySelectorAll('[role="dialog"], [aria-modal="true"], .semi-modal, .modal')
404
+ );
405
+ const match = candidateRoots.find((node) => {
406
+ if (!(node instanceof HTMLElement)) return false;
407
+ if (!isVisible(node)) return false;
408
+ const text = (node.innerText || node.textContent || '').trim();
409
+ if (!text || text.length > 400) return false;
410
+ return phrasePattern.test(text);
411
+ });
327
412
 
328
- return 'enter';
329
- })(${JSON.stringify(text)})
413
+ return {
414
+ detected: !!match,
415
+ reason: match ? ((match.innerText || match.textContent || '').trim().slice(0, 80) || 'challenge-ui') : '',
416
+ };
417
+ })()
330
418
  `;
331
419
  }
332
420
  function clickSendButtonScript() {
333
421
  return `
334
422
  (() => {
335
- const isVisible = (el) => {
336
- if (!(el instanceof HTMLElement)) return false;
337
- const style = window.getComputedStyle(el);
338
- if (style.display === 'none' || style.visibility === 'hidden') return false;
339
- const rect = el.getBoundingClientRect();
340
- return rect.width > 0 && rect.height > 0;
341
- };
423
+ ${buildDoubaoComposerLocatorScript()}
424
+ const composer = findComposer();
425
+ if (!(composer instanceof HTMLElement)) return false;
426
+
427
+ const composerRect = composer.getBoundingClientRect();
428
+ const rootCandidates = [
429
+ composer.closest('form'),
430
+ composer.closest('[role="form"]'),
431
+ composer.closest('[data-testid="chat_input"]'),
432
+ composer.closest('.chat-input'),
433
+ composer.parentElement,
434
+ composer.parentElement?.parentElement,
435
+ ].filter(Boolean);
436
+
437
+ const seen = new Set();
438
+ const buttons = [];
439
+ for (const root of rootCandidates) {
440
+ root.querySelectorAll('button, [role="button"]').forEach((node) => {
441
+ if (!(node instanceof HTMLElement)) return;
442
+ if (seen.has(node)) return;
443
+ seen.add(node);
444
+ buttons.push(node);
445
+ });
446
+ }
342
447
 
343
- const labels = ['发送', 'Send', '发消息...', 'Message...'];
344
- const root = document.querySelector('[data-testid="chat_input"], .chat-input') || document;
345
- const buttons = Array.from(root.querySelectorAll(
346
- '.chat-input-button button, .chat-input-button [role="button"], .chat-input button, button[type="submit"], [role="button"]'
347
- ));
448
+ const submitPattern = /send|发送|提交|发消息/i;
449
+ const excludedPattern = /新对话|new chat|快速|视频生成|深入研究|图像生成|帮我写作|音乐生成|更多|上传|upload|麦克风|microphone|模式|mode|工具|tools|设置|settings|云盘|history|历史/i;
450
+ let bestButton = null;
451
+ let bestScore = -Infinity;
348
452
 
349
453
  for (const button of buttons) {
350
454
  if (!isVisible(button)) continue;
351
455
  const disabled = button.getAttribute('disabled') !== null
352
456
  || button.getAttribute('aria-disabled') === 'true';
353
457
  if (disabled) continue;
458
+
354
459
  const text = (button.innerText || button.textContent || '').trim();
355
460
  const aria = (button.getAttribute('aria-label') || '').trim();
356
461
  const title = (button.getAttribute('title') || '').trim();
357
- const haystacks = [text, aria, title];
358
- if (haystacks.some((value) => labels.some((label) => value.includes(label)))) {
359
- button.click();
360
- return true;
361
- }
362
- }
462
+ const className = String(button.className || '');
463
+ const haystack = [text, aria, title].join(' ').trim();
464
+ if (excludedPattern.test(haystack)) continue;
363
465
 
364
- const styledCandidate = [...buttons].reverse().find((button) => {
365
- if (!isVisible(button)) return false;
366
- const disabled = button.getAttribute('disabled') !== null
367
- || button.getAttribute('aria-disabled') === 'true';
368
- if (disabled) return false;
369
- const className = button.className || '';
370
- return className.includes('bg-dbx-text-highlight')
466
+ const rect = button.getBoundingClientRect();
467
+ const dx = rect.left - composerRect.right;
468
+ const dy = Math.abs((rect.top + rect.height / 2) - (composerRect.top + composerRect.height / 2));
469
+ const distancePenalty = Math.abs(dx) + dy;
470
+ const isSubmitLike = submitPattern.test(haystack)
471
+ || button.getAttribute('type') === 'submit'
472
+ || className.includes('bg-dbx-text-highlight')
371
473
  || className.includes('bg-dbx-fill-highlight')
372
474
  || className.includes('text-dbx-text-static-white-primary');
373
- });
374
-
375
- if (styledCandidate) {
376
- styledCandidate.click();
377
- return true;
378
- }
475
+ if (!isSubmitLike) continue;
476
+ if (dx < -80 || dx > 280) continue;
477
+ if (dy > 140) continue;
379
478
 
380
- const inputButton = [...buttons].reverse().find((button) => {
381
- if (!isVisible(button)) return false;
382
- const disabled = button.getAttribute('disabled') !== null
383
- || button.getAttribute('aria-disabled') === 'true';
384
- if (disabled) return false;
385
- return !!button.closest('.chat-input-button');
386
- });
479
+ let score = -distancePenalty;
480
+ if (submitPattern.test(haystack)) score += 5000;
481
+ if (button.getAttribute('type') === 'submit') score += 1200;
482
+ if (button.closest('.chat-input-button')) score += 1200;
483
+ if (className.includes('bg-dbx-text-highlight')) score += 600;
484
+ if (className.includes('bg-dbx-fill-highlight')) score += 600;
485
+ if (className.includes('text-dbx-text-static-white-primary')) score += 400;
486
+ if (dx >= -40 && dx <= 240) score += 120;
487
+ if (rect.left >= composerRect.left - 40) score += 40;
387
488
 
388
- if (inputButton) {
389
- inputButton.click();
390
- return true;
489
+ if (score > bestScore) {
490
+ bestScore = score;
491
+ bestButton = button;
492
+ }
391
493
  }
392
494
 
393
- const lastEnabledButton = [...buttons].reverse().find((button) => {
394
- if (!isVisible(button)) return false;
395
- return button.getAttribute('disabled') === null
396
- && button.getAttribute('aria-disabled') !== 'true';
397
- });
398
-
399
- if (lastEnabledButton) {
400
- lastEnabledButton.click();
495
+ if (bestButton && bestScore >= 200) {
496
+ bestButton.click();
401
497
  return true;
402
498
  }
403
499
 
@@ -513,15 +609,56 @@ export async function getDoubaoTranscriptLines(page) {
513
609
  }
514
610
  export async function sendDoubaoMessage(page, text) {
515
611
  await ensureDoubaoChatPage(page);
516
- const submittedBy = await page.evaluate(fillAndSubmitComposerScript(text));
517
- if (submittedBy === 'enter') {
612
+ const normalizeComposerText = (value) => value.replace(/\r\n/g, '\n').trim();
613
+ const expectedText = normalizeComposerText(text);
614
+ const prepared = await page.evaluate(prepareDoubaoComposerScript());
615
+ if (!prepared?.ok) {
616
+ throw new CommandExecutionError(prepared?.reason || 'Could not find Doubao input element');
617
+ }
618
+ let hasText = false;
619
+ if (page.nativeType) {
620
+ try {
621
+ await page.nativeType(text);
622
+ await page.wait(0.2);
623
+ await page.evaluate(syncComposerAfterNativeTypeScript());
624
+ const nativeState = await page.evaluate(composerStateScript());
625
+ hasText = !!nativeState?.hasText && normalizeComposerText(nativeState?.text || '') === expectedText;
626
+ }
627
+ catch { }
628
+ }
629
+ if (!hasText) {
630
+ const fallbackState = await page.evaluate(fillComposerScript(text));
631
+ hasText = !!fallbackState?.hasText && normalizeComposerText(fallbackState?.text || '') === expectedText;
632
+ }
633
+ if (!hasText) {
634
+ throw new CommandExecutionError('Failed to insert text into Doubao composer');
635
+ }
636
+ let submittedBy = 'enter';
637
+ const clicked = await page.evaluate(clickSendButtonScript());
638
+ if (clicked) {
639
+ submittedBy = 'button';
640
+ }
641
+ else if (page.nativeKeyPress) {
642
+ try {
643
+ await page.nativeKeyPress('Enter');
644
+ }
645
+ catch {
646
+ await page.pressKey('Enter');
647
+ }
648
+ }
649
+ else {
518
650
  await page.pressKey('Enter');
519
651
  }
520
652
  await page.wait(0.8);
653
+ const verification = await page.evaluate(detectDoubaoVerificationScript());
654
+ if (verification?.detected) {
655
+ throw new CommandExecutionError('Doubao blocked the request with a verification challenge', verification.reason
656
+ ? `Detected challenge signal: ${verification.reason}`
657
+ : 'Please complete the challenge in the browser and try again.');
658
+ }
521
659
  return submittedBy;
522
660
  }
523
661
  export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, promptText, timeoutSeconds) {
524
- const beforeSet = new Set(beforeLines);
525
662
  const beforeTurnSet = new Set(beforeTurns
526
663
  .filter((turn) => turn.Role === 'Assistant')
527
664
  .map((turn) => `${turn.Role}::${turn.Text}`));
@@ -534,6 +671,12 @@ export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, prom
534
671
  .replace(/window\\._SSR_DATA.*$/g, '')
535
672
  .trim();
536
673
  const getCandidate = async () => {
674
+ const verification = await page.evaluate(detectDoubaoVerificationScript());
675
+ if (verification?.detected) {
676
+ throw new CommandExecutionError('Doubao blocked the request with a verification challenge', verification.reason
677
+ ? `Detected challenge signal: ${verification.reason}`
678
+ : 'Please complete the challenge in the browser and try again.');
679
+ }
537
680
  const turns = await getDoubaoVisibleTurns(page);
538
681
  const assistantCandidate = [...turns]
539
682
  .reverse()
@@ -542,10 +685,9 @@ export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, prom
542
685
  if (visibleCandidate)
543
686
  return visibleCandidate;
544
687
  const lines = await getDoubaoTranscriptLines(page);
545
- const additions = lines
546
- .filter((line) => !beforeSet.has(line))
547
- .map((line) => sanitizeCandidate(line))
548
- .filter((line) => line && line !== promptText);
688
+ const additions = collectDoubaoTranscriptAdditions(beforeLines, lines, promptText, sanitizeCandidate)
689
+ .split('\n')
690
+ .filter(Boolean);
549
691
  const shortCandidate = additions.find((line) => line.length <= 120);
550
692
  return shortCandidate || additions[additions.length - 1] || '';
551
693
  };
@@ -571,6 +713,48 @@ export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, prom
571
713
  }
572
714
  return lastCandidate;
573
715
  }
716
+ export function isLikelyDoubaoUiNoise(value) {
717
+ const text = value.replace(/\s+/g, '');
718
+ if (!text)
719
+ return false;
720
+ const exactNoise = new Set([
721
+ '快速视频生成深入研究图像生成帮我写作音乐生成更多',
722
+ ]);
723
+ return exactNoise.has(text);
724
+ }
725
+ function isAlwaysTranscriptUiNoise(value) {
726
+ const text = value.replace(/\s+/g, '');
727
+ if (!text)
728
+ return false;
729
+ const exactNoise = new Set([
730
+ 'AI创作云盘更多历史对话',
731
+ ]);
732
+ return exactNoise.has(text);
733
+ }
734
+ function isLikelyTranscriptUiNoise(rawValue, sanitizedValue, promptText) {
735
+ const normalizeWhitespace = (value) => value.replace(/\s+/g, ' ').trim();
736
+ const normalizedRaw = normalizeWhitespace(rawValue);
737
+ const normalizedPrompt = normalizeWhitespace(promptText);
738
+ if (!normalizedPrompt || !normalizedRaw.startsWith(normalizedPrompt))
739
+ return false;
740
+ const remainder = normalizedRaw.slice(normalizedPrompt.length).trim();
741
+ if (!remainder)
742
+ return true;
743
+ return isLikelyDoubaoUiNoise(remainder) || isLikelyDoubaoUiNoise(sanitizedValue);
744
+ }
745
+ export function collectDoubaoTranscriptAdditions(beforeLines, currentLines, promptText, sanitize = (value) => value.trim()) {
746
+ const normalizedBefore = new Set(beforeLines.map((line) => sanitize(line)).filter(Boolean));
747
+ return currentLines
748
+ .filter((line) => !beforeLines.includes(line))
749
+ .map((line) => ({ raw: line, sanitized: sanitize(line) }))
750
+ .filter(({ raw, sanitized }) => sanitized
751
+ && sanitized !== promptText
752
+ && !normalizedBefore.has(sanitized)
753
+ && !isAlwaysTranscriptUiNoise(sanitized)
754
+ && !isLikelyTranscriptUiNoise(raw, sanitized, promptText))
755
+ .map(({ sanitized }) => sanitized)
756
+ .join('\n');
757
+ }
574
758
  function getConversationListScript() {
575
759
  return `
576
760
  (() => {
@@ -888,6 +1072,11 @@ export async function triggerTranscriptDownload(page) {
888
1072
  const btnResult = await page.evaluate(clickTranscriptDownloadBtnScript());
889
1073
  return !btnResult.error;
890
1074
  }
1075
+ export const __test__ = {
1076
+ clickSendButtonScript,
1077
+ composerStateScript,
1078
+ detectDoubaoVerificationScript,
1079
+ };
891
1080
  export async function startNewDoubaoChat(page) {
892
1081
  await ensureDoubaoChatPage(page);
893
1082
  const clickedLabel = await page.evaluate(clickNewChatScript());