@bsbofmusic/cdper-doubao 1.0.2 → 1.0.3

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 (3) hide show
  1. package/README.md +15 -4
  2. package/package.json +3 -3
  3. package/src/doubao.js +16 -154
package/README.md CHANGED
@@ -11,9 +11,9 @@ npm install -g @bsbofmusic/cdper-doubao
11
11
  Requirements:
12
12
 
13
13
  - Node.js 18+
14
- - CDP Bridge running on the user's browser host
15
- - Doubao logged in in that browser profile
16
- - CDP WS configured through `CDP_WS`, `~/.cdp-auth.json`, or `~/.cdp-bridge/config.json`
14
+ - A real Chrome/Edge profile with Doubao logged in
15
+ - Either local Chrome DevTools on `127.0.0.1:9222` or a remote CDP Bridge
16
+ - Optional explicit CDP WS through `CDP_WS`, `~/.cdp-auth.json`, or `~/.cdp-bridge/config.json`
17
17
 
18
18
  ## Use
19
19
 
@@ -54,6 +54,17 @@ const result = await queryDoubao('请用一句话回答:1+1等于几?', {
54
54
  });
55
55
  ```
56
56
 
57
+
58
+ ## Release notes — 1.0.3
59
+
60
+ - Depends on `@bsbofmusic/cdper-core@^1.2.7`, so `status --json` correctly reports healthy local CDP as `ok=true`.
61
+ - Removed the dead `url-action` query branch from the live Doubao query path. Fresh/followup queries now consistently use the composer path: open clean Doubao home, switch `快速 → 专家`, insert prompt text, click send, and verify submission.
62
+ - Kept `url-action` only as a URL-pattern guard for conversation-id parsing (`/chat/url-action` is not a real followup anchor).
63
+ - Pack/install smoke was verified from the generated tarball in an isolated npm prefix before publish.
64
+
65
+ Design note: Doubao link reading is more reliable when URLs are included in the composer prompt and handled by the page’s native preview/search flow. A hidden url-action branch created false confidence and made fallback logic unreachable.
66
+
67
+ ---
57
68
  ## Security
58
69
 
59
- All browser control happens through the user's CDP Bridge. Tokens are redacted from diagnostic output. Do not commit `CDP_WS` or full WebSocket URLs containing `token=`.
70
+ All browser control happens through the user's real browser via local CDP or CDP Bridge. Tokens are redacted from diagnostic output. Do not commit `CDP_WS` or full WebSocket URLs containing `token=`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsbofmusic/cdper-doubao",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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.6",
42
+ "@bsbofmusic/cdper-core": "^1.2.7",
43
43
  "yargs": "^17.7.2"
44
44
  }
45
- }
45
+ }
package/src/doubao.js CHANGED
@@ -12,8 +12,8 @@ const { runState } = require('@bsbofmusic/cdper-core/state_machine');
12
12
  const { getRuntimeStatus } = require('@bsbofmusic/cdper-core/runtime_status');
13
13
  const { WRAPPER_STATUSES, buildWrapperResult } = require('@bsbofmusic/cdper-core/wrapper_result');
14
14
  const {
15
- randomInt, humanPause, withLocalTimeout, withAbortableTimeout, throwIfAborted, ensureMinimumPageDwell, humanWarmup,
16
- computeSendTimeoutMs, computeTypingDelay, shouldUseDirectTextInsert, insertTextViaCdp,
15
+ randomInt, humanPause, withLocalTimeout, throwIfAborted, ensureMinimumPageDwell, humanWarmup,
16
+ computeSendTimeoutMs, insertTextViaCdp,
17
17
  buildWaitPlan, createProgressRecorder,
18
18
  resolveConversationPolicy, buildConversationAnchor,
19
19
  prefersShortAnswer, handlePluginQueryError,
@@ -22,7 +22,6 @@ const {
22
22
 
23
23
  const PLATFORM = 'doubao';
24
24
  const STATE_TIMEOUTS = { open: 150000, ensure_session: 30000, ensure_mode: 45000, send: 45000, extract: 10000 };
25
- const FALLBACK_TIMEOUT_MS = 90000;
26
25
  const WAIT_PROFILES = {
27
26
  short: { minWait: 8000, maxWait: 90000, maxExtends: 2, pollInterval: 2500 },
28
27
  normal: { minWait: 12000, maxWait: 300000, maxExtends: 6, pollInterval: 2500 },
@@ -30,40 +29,11 @@ const WAIT_PROFILES = {
30
29
  very_long: { minWait: 30000, maxWait: 45 * 60 * 1000, maxExtends: 10, pollInterval: 2500 },
31
30
  };
32
31
 
33
- function directActionMode(mode) {
34
- return mode === 'quick' ? 'quick' : 'deep';
35
- }
36
-
37
- function buildDoubaoUrlAction(queryText, mode = 'deep') {
38
- const payload = { text: String(queryText || '') };
39
- const action = { pluginId: 'Send_Message', payload };
40
- if (directActionMode(mode) === 'deep') {
41
- payload.extraExt = {
42
- input_skill: JSON.stringify({ skill_type: 20 }),
43
- use_deep_think: '1',
44
- };
45
- action.options = {
46
- deepThinkingActiveType: '2',
47
- superTaskStatus: { switchValue: 0, taskMode: 0, safeToken: '' },
48
- };
49
- }
50
- const url = new URL('https://www.doubao.com/chat/url-action');
51
- url.searchParams.set('action', JSON.stringify(action));
52
- return url.toString();
53
- }
54
-
55
32
  function emptyDoubaoSnapshot() {
56
33
  return { text: '', normalized: '', length: 0, candidateCount: 0 };
57
34
  }
58
35
 
59
- function buildDoubaoModeState(usedUrlAction, urlActionMode, expertModeConfirmed) {
60
- if (usedUrlAction) {
61
- return {
62
- modePath: urlActionMode === 'quick' ? 'url_action_quick' : 'url_action_deep',
63
- modeConfirmed: true,
64
- modeConfidence: 'high',
65
- };
66
- }
36
+ function buildDoubaoModeState(expertModeConfirmed) {
67
37
  return {
68
38
  modePath: expertModeConfirmed ? 'composer_expert' : 'composer_quick',
69
39
  modeConfirmed: Boolean(expertModeConfirmed),
@@ -369,68 +339,6 @@ async function openDoubaoHome(page, sessionId) {
369
339
  throw new Error('Doubao 页面未准备好:输入框未出现');
370
340
  }
371
341
 
372
- async function getDoubaoDirectState(page) {
373
- return page.evaluate(() => {
374
- const bodyText = document.body?.innerText || '';
375
- const inputs = Array.from(document.querySelectorAll('textarea, [contenteditable="true"], [role="textbox"]'));
376
- const input = inputs.find((element) => {
377
- const rect = element.getBoundingClientRect();
378
- return rect.width >= 80 && rect.height >= 20 && element.getAttribute('aria-hidden') !== 'true' && !element.disabled;
379
- });
380
- const inputText = String(input?.value || input?.innerText || input?.textContent || '').trim();
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;
389
- const text = button.innerText || '';
390
- const aria = button.getAttribute('aria-label') || '';
391
- return /停止生成|停止回答|Stop generating|stop-button/i.test(`${text} ${aria}`) || !!button.querySelector('svg[icon="stop"]');
392
- });
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 || '')));
394
- const replyContainers = Array.from(document.querySelectorAll('[class*="flow-markdown-body"], [class*="container-P2rR72"], [class*="mdbox-theme"], [class*="message"], [class*="response"], [class*="answer"], .prose'))
395
- .filter((element) => {
396
- const cls = String(element.className || '');
397
- if (/message-list|suggest|recommend|bg-g-send/i.test(cls)) return false;
398
- const rect = element.getBoundingClientRect();
399
- const text = (element.innerText || '').trim();
400
- return rect.width > 0 && rect.height > 0 && text && !/历史对话|内容由豆包 AI 生成/.test(text);
401
- });
402
- return {
403
- url: location.href,
404
- title: document.title || '',
405
- bodySample: bodyText.slice(0, 1200),
406
- hasInput: !!input,
407
- inputText,
408
- isGenerating: !!stopButton || !!loading,
409
- candidateCount: replyContainers.length,
410
- unavailable: bodyText.includes('该页面暂时不可用'),
411
- };
412
- });
413
- }
414
-
415
- async function openDoubaoUrlAction(page, queryText, mode, sessionId) {
416
- const targetUrl = buildDoubaoUrlAction(queryText, mode);
417
- await page.goto(targetUrl, { timeout: 60000, waitUntil: 'domcontentloaded' });
418
- await humanPause(1800, 3800);
419
- await recoverDoubaoUnavailablePage(page);
420
- await assertNoChallenge(page, sessionId);
421
- const deadline = Date.now() + 30000;
422
- let state = null;
423
- while (Date.now() < deadline) {
424
- state = await getDoubaoDirectState(page);
425
- if (state.unavailable) await recoverDoubaoUnavailablePage(page);
426
- if (state.candidateCount > 0 || state.isGenerating) return { accepted: true, needsManualSubmit: false, state, url: targetUrl };
427
- if (state.hasInput && state.inputText) return { accepted: false, needsManualSubmit: true, state, url: targetUrl };
428
- await humanPause(400, 800);
429
- }
430
- state = state || await getDoubaoDirectState(page).catch(() => null);
431
- return { accepted: false, needsManualSubmit: true, state, url: targetUrl };
432
- }
433
-
434
342
  async function hasDoubaoConversationContent(page) {
435
343
  const snapshot = await captureDoubaoReplySnapshot(page).catch(() => null);
436
344
  return Number(snapshot?.length || 0) > 0;
@@ -635,7 +543,7 @@ async function switchToExpert(page, sessionId) {
635
543
 
636
544
  // ─── Main query ───────────────────────────────────────────────────────────────
637
545
  async function queryDoubao(queryText, opts = {}) {
638
- const { mode = 'expert', sessionId, _retried = false, _quickFallback = false } = opts;
546
+ const { mode = 'expert', sessionId, _retried = false } = opts;
639
547
  const abortSignal = opts.abortSignal || null;
640
548
  const fallbackMeta = opts._fallbackMeta || null;
641
549
  throwIfAborted(abortSignal, 'Doubao query');
@@ -654,9 +562,6 @@ async function queryDoubao(queryText, opts = {}) {
654
562
  let initialReplySnapshot = null;
655
563
  let inputReadyAfterOpen = false;
656
564
  let promptSubmitted = false;
657
- let usedUrlAction = false;
658
- let urlActionNeedsManualSubmit = false;
659
- let urlActionMode = directActionMode(mode);
660
565
  let expertModeConfirmed = false;
661
566
  let page, browser;
662
567
 
@@ -713,7 +618,6 @@ async function queryDoubao(queryText, opts = {}) {
713
618
 
714
619
  throwIfAborted(abortSignal, 'Doubao query');
715
620
  await runState(flow, 'ensure_session', async () => {
716
- if (usedUrlAction && promptSubmitted) return;
717
621
  if (inputReadyAfterOpen && await hasDoubaoInput(page, 1800)) return;
718
622
  const existingInput = await hasDoubaoInput(page, 12000);
719
623
  if (!existingInput) {
@@ -730,46 +634,29 @@ async function queryDoubao(queryText, opts = {}) {
730
634
 
731
635
  throwIfAborted(abortSignal, 'Doubao query');
732
636
  expertModeConfirmed = await runState(flow, 'ensure_mode', async () => {
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.
637
+ // Fresh/followup queries always use the composer path; verify and switch
638
+ // to expert mode via the 快速→专家 menu before sending.
738
639
  const confirmed = await switchToExpert(page, session.sessionId);
739
640
  await assertNoChallenge(page, session.sessionId);
740
641
  if (confirmed) await waitForDoubaoModeSettled(page, { timeoutMs: 6000, sessionId: session.sessionId }).catch(() => false);
741
642
  return confirmed;
742
643
  }, { timeoutMs: STATE_TIMEOUTS.ensure_mode });
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 }) });
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) {
644
+ log('INFO', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'flow', action: 'stage_complete', stage: 'ensure_mode', status: 'ok', ...buildDoubaoModeState(expertModeConfirmed), expertModeConfirmed });
645
+ // Block sending when expert mode could not be confirmed; composer path
646
+ // should not silently degrade into the quick model.
647
+ if (!expertModeConfirmed) {
749
648
  log('WARN', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'mode', action: 'expert_confirm', status: 'blocked', message: '未确认进入专家模式,停止发送以避免错误模式输出' });
750
649
  throw new Error('Doubao 专家模式未确认,需要人工接管');
751
650
  }
752
651
 
753
652
  throwIfAborted(abortSignal, 'Doubao query');
754
653
  await runState(flow, 'send', async () => {
755
- if (usedUrlAction) {
756
- if (!initialReplySnapshot) initialReplySnapshot = emptyDoubaoSnapshot();
757
- if (promptSubmitted && !urlActionNeedsManualSubmit) {
758
- flow.enter('send', { submitVerified: true, submitSignal: 'url_action_accepted', submitElapsed: 0, mode: urlActionMode });
759
- log('INFO', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'query', action: 'submit', status: 'ok', message: '问题已通过 url-action 发送' });
760
- return;
761
- }
762
- }
763
654
  const input = await hasDoubaoInput(page, 18000);
764
655
  if (!input) throw new Error('Doubao 输入框未就绪');
765
656
  await assertNoChallenge(page, session.sessionId);
766
- if (!usedUrlAction) {
767
- initialReplySnapshot = await captureDoubaoReplySnapshot(page).catch(() => null);
768
- await clearDoubaoInput(page, input.handle);
769
- await typeIntoDoubaoInput(page, input.handle, queryText);
770
- } else {
771
- await input.handle.focus();
772
- }
657
+ initialReplySnapshot = await captureDoubaoReplySnapshot(page).catch(() => null);
658
+ await clearDoubaoInput(page, input.handle);
659
+ await typeIntoDoubaoInput(page, input.handle, queryText);
773
660
  await assertNoChallenge(page, session.sessionId);
774
661
  await humanPause(900, 1800);
775
662
  const clicked = await clickDoubaoSend(page);
@@ -848,18 +735,6 @@ async function queryDoubao(queryText, opts = {}) {
848
735
 
849
736
  const completenessScore = result.completeness?.score || 0;
850
737
  const contentMarkedIncomplete = result.completeness?.isComplete === false;
851
- if (!_quickFallback && conversationPlan.effective === 'fresh' && usedUrlAction && urlActionMode === 'deep' && completenessScore < 70 && contentMarkedIncomplete && !prefersShortAnswer(queryText)) {
852
- log('WARN', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'query', action: 'fallback', status: 'degraded', message: 'deep url-action produced low-completeness content; retrying quick url-action (score=' + completenessScore + ')' });
853
- if (page || browser) await disconnect(page, browser).catch(() => undefined);
854
- page = null;
855
- browser = null;
856
- return await withAbortableTimeout(
857
- (signal) => queryDoubao(queryText, { ...opts, mode: 'quick', conversationPolicy: 'fresh', _quickFallback: true, abortSignal: signal, _fallbackMeta: { attempted: true, fromMode: urlActionMode, toMode: 'quick', reason: 'low_completeness', timeoutMs: FALLBACK_TIMEOUT_MS } }),
858
- 'quick url-action fallback',
859
- FALLBACK_TIMEOUT_MS
860
- );
861
- }
862
-
863
738
  if (wrapperStatus === WRAPPER_STATUSES.PARTIAL_TIMEOUT && result.content) {
864
739
  return buildWrapperResult({
865
740
  status: WRAPPER_STATUSES.PARTIAL_TIMEOUT,
@@ -875,7 +750,7 @@ async function queryDoubao(queryText, opts = {}) {
875
750
  return updateSession(session.sessionId, { turns: session.turns + 1, conversation });
876
751
  }, { timeoutMs: STATE_TIMEOUTS.extract });
877
752
 
878
- const modeState = buildDoubaoModeState(usedUrlAction, urlActionMode, expertModeConfirmed);
753
+ const modeState = buildDoubaoModeState(expertModeConfirmed);
879
754
  const contentIsComplete = result.completeness?.isComplete === true;
880
755
  const finalStatus = ['complete', 'complete_short'].includes(result.exitReason) && contentIsComplete ? 'ok' : 'degraded';
881
756
  log('INFO', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'query', action: 'complete', status: finalStatus, message: '回复完成:' + result.content.length + ' 字,完整度 ' + (result.completeness?.score || 0) + '分,exitReason=' + (result.exitReason || 'unknown') });
@@ -885,7 +760,7 @@ async function queryDoubao(queryText, opts = {}) {
885
760
  session: activeSession,
886
761
  flow,
887
762
  runtimeStatus: await getRuntimeStatus('doubao'),
888
- doneMeta: { status: finalStatus, turn: activeSession.turns, ...modeState, ...(usedUrlAction ? {} : { expertModeConfirmed }), usedUrlAction, urlActionMode: usedUrlAction ? urlActionMode : null, conversationPolicy: conversationPlan.effective },
763
+ doneMeta: { status: finalStatus, turn: activeSession.turns, ...modeState, expertModeConfirmed, conversationPolicy: conversationPlan.effective },
889
764
  payload: {
890
765
  content: result.content,
891
766
  score: completenessScore,
@@ -894,9 +769,7 @@ async function queryDoubao(queryText, opts = {}) {
894
769
  fallback: fallbackMeta,
895
770
  exitReason: result.exitReason || 'unknown',
896
771
  ...modeState,
897
- ...(usedUrlAction ? {} : { expertModeConfirmed }),
898
- usedUrlAction,
899
- urlActionMode: usedUrlAction ? urlActionMode : null,
772
+ expertModeConfirmed,
900
773
  detectedChallenge: result.detectedChallenge || null,
901
774
  conversationPolicy: conversationPlan.requested,
902
775
  effectiveConversationPolicy: conversationPlan.effective,
@@ -906,17 +779,6 @@ async function queryDoubao(queryText, opts = {}) {
906
779
  },
907
780
  });
908
781
  } catch (error) {
909
- if (!_quickFallback && error.code !== 'FALLBACK_TIMEOUT' && conversationPlan.effective === 'fresh' && usedUrlAction && urlActionMode === 'deep') {
910
- log('WARN', '[doubao]', { component: 'doubao', plugin: 'cdper-doubao', session_id: session.sessionId, step: 'query', action: 'fallback', status: 'degraded', message: 'deep url-action failed; retrying quick url-action: ' + error.message });
911
- if (page || browser) await disconnect(page, browser).catch(() => undefined);
912
- page = null;
913
- browser = null;
914
- return await withAbortableTimeout(
915
- (signal) => queryDoubao(queryText, { ...opts, mode: 'quick', conversationPolicy: 'fresh', _quickFallback: true, abortSignal: signal, _fallbackMeta: { attempted: true, fromMode: urlActionMode, toMode: 'quick', reason: error.message, timeoutMs: FALLBACK_TIMEOUT_MS } }),
916
- 'quick url-action fallback',
917
- FALLBACK_TIMEOUT_MS
918
- );
919
- }
920
782
  return handlePluginQueryError(error, {
921
783
  flow, pluginName: 'doubao', retried: _retried,
922
784
  retryFn: () => queryDoubao(queryText, { ...opts, _retried: true }),