@bsbofmusic/cdper-doubao 1.0.0 → 1.0.1

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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/doubao.js +84 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsbofmusic/cdper-doubao",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Standalone Doubao CLI controlled through a user's real Chrome/Edge via CDP Bridge",
5
5
  "type": "commonjs",
6
6
  "main": "src/public-api.js",
@@ -39,7 +39,7 @@
39
39
  "node": ">=18.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@bsbofmusic/cdper-core": "^1.2.4",
42
+ "@bsbofmusic/cdper-core": "^1.2.5",
43
43
  "yargs": "^17.7.2"
44
44
  }
45
- }
45
+ }
package/src/doubao.js CHANGED
@@ -43,7 +43,7 @@ function buildDoubaoUrlAction(queryText, mode = 'deep') {
43
43
  use_deep_think: '1',
44
44
  };
45
45
  action.options = {
46
- deepThinkingActiveType: '3',
46
+ deepThinkingActiveType: '2',
47
47
  superTaskStatus: { switchValue: 0, taskMode: 0, safeToken: '' },
48
48
  };
49
49
  }
@@ -161,6 +161,7 @@ async function getDoubaoUiState(page) {
161
161
  return ['快速', 'Quick'].includes(meta.text) && meta.visible;
162
162
  });
163
163
  const expertButton = buttons.find((button) => /^(?:专家|Expert)($|\n)/.test(visibleButton(button).text) && visibleButton(button).visible);
164
+ const superModeButton = buttons.find((button) => /^(?:超能模式|Super|Deep)($|\n)/i.test(visibleButton(button).text) && visibleButton(button).visible);
164
165
  const menu = document.querySelector('[role="menu"]');
165
166
  const menuItems = menu
166
167
  ? Array.from(menu.querySelectorAll('[role="menuitem"]')).map((node) => ({
@@ -179,7 +180,7 @@ async function getDoubaoUiState(page) {
179
180
  });
180
181
  const loading = !!document.querySelector('[class*="loading"], [class*="spinner"], [class*="skeleton"]');
181
182
  const unavailable = bodyText.includes('该页面暂时不可用');
182
- return { url: location.href, title: document.title || '', hasInput, hasQuickButton: !!quickButton, hasExpertButton: !!expertButton, menuVisible: !!menu, menuItems, loading, unavailable };
183
+ return { url: location.href, title: document.title || '', hasInput, hasQuickButton: !!quickButton, hasExpertButton: !!expertButton, hasSuperModeButton: !!superModeButton, menuVisible: !!menu, menuItems, loading, unavailable };
183
184
  });
184
185
  }
185
186
 
@@ -230,6 +231,17 @@ async function adoptExistingDoubaoPage(browser, fallbackPage) {
230
231
  return fallbackPage;
231
232
  }
232
233
 
234
+ async function clickHandleAtCenter(page, handle, label) {
235
+ try {
236
+ const box = await handle.boundingBox();
237
+ if (box && box.width > 0 && box.height > 0) {
238
+ await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { delay: 80 });
239
+ return true;
240
+ }
241
+ } catch (error) { log('WARN', '[doubao]', label + '坐标点击失败: ' + error.message); }
242
+ return false;
243
+ }
244
+
233
245
  async function clickHandleWithoutCoordinates(handle, label) {
234
246
  try {
235
247
  await withLocalTimeout(handle.evaluate((element) => element.click()), label + ' DOM click');
@@ -270,6 +282,20 @@ async function clickButtonByExactText(page, text, label) {
270
282
  }
271
283
 
272
284
  async function clickMenuItemByPrefix(page, prefix, label) {
285
+ try {
286
+ const items = await page.$$('[role="menuitem"]');
287
+ for (const item of items) {
288
+ const meta = await item.evaluate((node) => {
289
+ const text = (node.innerText || '').trim();
290
+ const rect = node.getBoundingClientRect();
291
+ return { text, visible: rect.width > 0 && rect.height > 0 };
292
+ }).catch(() => null);
293
+ if (!meta || !meta.visible || !meta.text.startsWith(prefix)) continue;
294
+ if (await clickHandleAtCenter(page, item, label + '坐标')) return true;
295
+ if (await clickHandleWithoutCoordinates(item, label)) return true;
296
+ }
297
+ } catch (error) { log('WARN', '[doubao]', label + '坐标点击链路失败: ' + error.message); }
298
+
273
299
  try {
274
300
  const clicked = await page.evaluate((targetPrefix) => {
275
301
  const menu = document.querySelector('[role="menu"]');
@@ -352,12 +378,19 @@ async function getDoubaoDirectState(page) {
352
378
  return rect.width >= 80 && rect.height >= 20 && element.getAttribute('aria-hidden') !== 'true' && !element.disabled;
353
379
  });
354
380
  const inputText = String(input?.value || input?.innerText || input?.textContent || '').trim();
355
- const stopButton = Array.from(document.querySelectorAll('button')).find((button) => {
381
+ const isVisible = (element) => {
382
+ if (!element) return false;
383
+ const rect = element.getBoundingClientRect();
384
+ const style = window.getComputedStyle(element);
385
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none' && element.getAttribute('aria-hidden') !== 'true';
386
+ };
387
+ const stopButton = Array.from(document.querySelectorAll('button,[role="button"]')).find((button) => {
388
+ if (!isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') return false;
356
389
  const text = button.innerText || '';
357
390
  const aria = button.getAttribute('aria-label') || '';
358
- return /停止|stop/i.test(`${text} ${aria}`);
391
+ return /停止生成|停止回答|Stop generating|stop-button/i.test(`${text} ${aria}`) || !!button.querySelector('svg[icon="stop"]');
359
392
  });
360
- const loading = document.querySelector('[class*="loading"], [class*="spinner"], [class*="skeleton"]');
393
+ const loading = Array.from(document.querySelectorAll('[class*="loading"], [class*="spinner"], [class*="skeleton"]')).find((element) => isVisible(element) && /loading|spinner|生成中|思考中|回答中/i.test(String(element.className || '') + ' ' + (element.innerText || '')));
361
394
  const replyContainers = Array.from(document.querySelectorAll('[class*="flow-markdown-body"], [class*="container-P2rR72"], [class*="mdbox-theme"], [class*="message"], [class*="response"], [class*="answer"], .prose'))
362
395
  .filter((element) => {
363
396
  const cls = String(element.className || '');
@@ -426,11 +459,9 @@ async function typeIntoDoubaoInput(page, inputHandle, text) {
426
459
  await inputHandle.focus();
427
460
  await humanPause(160, 420);
428
461
  const value = String(text || '');
429
- if (shouldUseDirectTextInsert(value)) {
430
- await insertTextViaCdp(page, value);
431
- return;
432
- }
433
- await page.keyboard.type(value, { delay: computeTypingDelay(value) });
462
+ // Doubao composer is React-controlled; use CDP insertText for both short and
463
+ // multiline prompts to avoid stale keyboard/IME edge cases.
464
+ await insertTextViaCdp(page, value);
434
465
  }
435
466
 
436
467
  async function clickDoubaoSend(page) {
@@ -500,7 +531,10 @@ async function clickNewChat(page, options = {}) {
500
531
  }
501
532
 
502
533
  async function switchToExpert(page, sessionId) {
503
- const isExpertButton = async () => (await getDoubaoUiState(page)).hasExpertButton;
534
+ const isExpertButton = async () => {
535
+ const state = await getDoubaoUiState(page);
536
+ return state.hasExpertButton;
537
+ };
504
538
 
505
539
  if (await isExpertButton()) { log('INFO', '[doubao]', '专家按钮已存在,模式已就绪'); return true; }
506
540
 
@@ -539,6 +573,9 @@ async function switchToExpert(page, sessionId) {
539
573
  };
540
574
 
541
575
  for (const candidate of candidates) {
576
+ if (await clickHandleAtCenter(page, candidate.handle, candidate.label + '坐标')) {
577
+ if (await waitForExpertMenu().catch(() => false)) return true;
578
+ }
542
579
  const clicked = await clickHandleWithoutCoordinates(candidate.handle, candidate.label);
543
580
  if (clicked && await waitForExpertMenu().catch(() => false)) return true;
544
581
  if (await activate(candidate.handle, 'ArrowDown')) return true;
@@ -657,17 +694,14 @@ async function queryDoubao(queryText, opts = {}) {
657
694
  }
658
695
  assertFollowupAnchorActive(page, session);
659
696
  } else if (conversationPlan.effective === 'fresh') {
660
- // Fresh Doubao queries use the official URL action entrypoint. It
661
- // submits the prompt server-side and enables deep thinking without
662
- // brittle menu switching. The legacy composer path remains for
663
- // anchored followups and rare manual-submit fallback.
697
+ // Fresh queries: open a clean doubao home and confirm no prior content.
698
+ // We intentionally avoid the url-action entrypoint because doubao
699
+ // cannot reliably open/fetch external URLs through it the page says
700
+ // "无法打开外链". Instead we open the composer, switch to expert mode
701
+ // (in ensure_mode), then type the full prompt including any URLs, which
702
+ // lets doubao's built-in link preview handle them natively.
703
+ inputReadyAfterOpen = await ensureFreshDoubaoHome(page, session.sessionId);
664
704
  initialReplySnapshot = emptyDoubaoSnapshot();
665
- const direct = await openDoubaoUrlAction(page, queryText, urlActionMode, session.sessionId);
666
- usedUrlAction = true;
667
- urlActionNeedsManualSubmit = direct.needsManualSubmit;
668
- inputReadyAfterOpen = Boolean(direct.state?.hasInput || direct.accepted);
669
- promptSubmitted = direct.accepted;
670
- if (!inputReadyAfterOpen) throw new Error('Doubao url-action did not prepare a prompt or conversation');
671
705
  } else {
672
706
  inputReadyAfterOpen = await openDoubaoHome(page, session.sessionId);
673
707
  }
@@ -696,14 +730,22 @@ async function queryDoubao(queryText, opts = {}) {
696
730
 
697
731
  throwIfAborted(abortSignal, 'Doubao query');
698
732
  expertModeConfirmed = await runState(flow, 'ensure_mode', async () => {
699
- if (usedUrlAction) return false;
733
+ // If url-action already auto-submitted the prompt server-side, mode
734
+ // switching is too late for the current query — skip gracefully.
735
+ if (usedUrlAction && promptSubmitted && !urlActionNeedsManualSubmit) return false;
736
+ // For all other cases (including url-action that still needs manual
737
+ // submission), verify and switch to expert mode via the 快速→专家 menu.
700
738
  const confirmed = await switchToExpert(page, session.sessionId);
701
739
  await assertNoChallenge(page, session.sessionId);
702
740
  if (confirmed) await waitForDoubaoModeSettled(page, { timeoutMs: 6000, sessionId: session.sessionId }).catch(() => false);
703
741
  return confirmed;
704
742
  }, { timeoutMs: STATE_TIMEOUTS.ensure_mode });
705
743
  log('INFO', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'flow', action: 'stage_complete', stage: 'ensure_mode', status: 'ok', ...buildDoubaoModeState(usedUrlAction, urlActionMode, expertModeConfirmed), ...(usedUrlAction ? {} : { expertModeConfirmed }) });
706
- if (!usedUrlAction && !expertModeConfirmed) {
744
+ // Block sending only when expert mode could not be confirmed and the
745
+ // prompt hasn't already been submitted. Already-submitted url-action
746
+ // prompts cannot be un-sent, so we let them through.
747
+ const modeBlockingBypass = usedUrlAction && promptSubmitted && !urlActionNeedsManualSubmit;
748
+ if (!modeBlockingBypass && !expertModeConfirmed) {
707
749
  log('WARN', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'mode', action: 'expert_confirm', status: 'blocked', message: '未确认进入专家模式,停止发送以避免错误模式输出' });
708
750
  throw new Error('Doubao 专家模式未确认,需要人工接管');
709
751
  }
@@ -744,8 +786,11 @@ async function queryDoubao(queryText, opts = {}) {
744
786
  captureUserTurn: async (page, baseline) => {
745
787
  const baselineCount = Number(baseline?.candidateCount || 0);
746
788
  return page.evaluate((bc) => {
747
- // Doubao: check if number of visible reply containers increased
748
- const replyContainers = document.querySelectorAll('[class*="flow-markdown-body"], [class*="container-P2rR72"]');
789
+ // Broad selector set matching smart_wait's selectBestDoubaoReplyText
790
+ const replyContainers = document.querySelectorAll(
791
+ '[class*="flow-markdown-body"], [class*="container-P2rR72"],' +
792
+ '[class*="mdbox-theme"], [class*="response"], [class*="answer"], .prose'
793
+ );
749
794
  return replyContainers.length > bc;
750
795
  }, baselineCount);
751
796
  },
@@ -754,15 +799,24 @@ async function queryDoubao(queryText, opts = {}) {
754
799
  const textarea = document.querySelector('textarea') || document.querySelector('[contenteditable="true"]');
755
800
  if (!textarea) return false;
756
801
  const val = textarea.value || textarea.innerText || '';
757
- return val.trim().length <= 5; // allow very short residual whitespace
802
+ return val.trim().length <= 5;
758
803
  });
759
804
  },
760
805
  captureGenerating: async (page) => {
761
806
  return page.evaluate(() => {
762
- const stopBtn = Array.from(document.querySelectorAll('button')).find(
763
- (b) => b.innerText?.includes('停止') || b.getAttribute('aria-label')?.includes('stop')
764
- );
765
- const loading = document.querySelector('[class*="loading"]') || document.querySelector('[class*="skeleton"]');
807
+ const isVisible = (el) => {
808
+ if (!el) return false;
809
+ const rect = el.getBoundingClientRect();
810
+ const style = window.getComputedStyle(el);
811
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none' && el.getAttribute('aria-hidden') !== 'true';
812
+ };
813
+ const stopBtn = Array.from(document.querySelectorAll('button,[role="button"]')).find((button) => {
814
+ if (!isVisible(button) || button.disabled || button.getAttribute('aria-disabled') === 'true') return false;
815
+ const text = button.innerText || '';
816
+ const aria = button.getAttribute('aria-label') || '';
817
+ return /停止生成|停止回答|Stop generating|stop-button/i.test(`${text} ${aria}`) || !!button.querySelector('svg[icon="stop"]');
818
+ });
819
+ const loading = Array.from(document.querySelectorAll('[class*="loading"], [class*="skeleton"], [class*="spinner"]')).find((element) => isVisible(element) && /loading|spinner|生成中|思考中|回答中/i.test(String(element.className || '') + ' ' + (element.innerText || '')));
766
820
  return !!stopBtn || !!loading;
767
821
  });
768
822
  },