@bsbofmusic/cdper-doubao 1.0.0 → 1.0.2
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.
- package/package.json +3 -3
- 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.
|
|
3
|
+
"version": "1.0.2",
|
|
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.
|
|
42
|
+
"@bsbofmusic/cdper-core": "^1.2.6",
|
|
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: '
|
|
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
|
|
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
|
|
391
|
+
return /停止生成|停止回答|Stop generating|stop-button/i.test(`${text} ${aria}`) || !!button.querySelector('svg[icon="stop"]');
|
|
359
392
|
});
|
|
360
|
-
const loading = document.
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 () =>
|
|
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
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
748
|
-
const replyContainers = document.querySelectorAll(
|
|
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;
|
|
802
|
+
return val.trim().length <= 5;
|
|
758
803
|
});
|
|
759
804
|
},
|
|
760
805
|
captureGenerating: async (page) => {
|
|
761
806
|
return page.evaluate(() => {
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
},
|