yiyan-browser-agent 1.10.3 → 1.11.0
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 +1 -1
- package/src/browser.js +176 -96
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yiyan-browser-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "AI coding agent powered by Yiyan (文心一言) via browser automation (chat.baidu.com) — no API key needed. Performance-optimized. Enhanced with comprehensive security.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/browser.js
CHANGED
|
@@ -14,12 +14,11 @@ const CrashHandler = require('./stability/crash-handler');
|
|
|
14
14
|
|
|
15
15
|
const SEL = {
|
|
16
16
|
chatInput: [
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'.editable',
|
|
20
|
-
'#chat-input',
|
|
17
|
+
'#chat-textarea',
|
|
18
|
+
'textarea.ci-textarea',
|
|
21
19
|
'textarea[placeholder]',
|
|
22
20
|
'textarea',
|
|
21
|
+
'[role="textbox"]',
|
|
23
22
|
'[contenteditable="true"][role="textbox"]',
|
|
24
23
|
'[contenteditable="true"]',
|
|
25
24
|
'[class*="input-box"]',
|
|
@@ -46,12 +45,16 @@ const SEL = {
|
|
|
46
45
|
'button[aria-label*="停止"]',
|
|
47
46
|
'[aria-label*="stop generating" i]',
|
|
48
47
|
'[data-testid="stop-button"]',
|
|
48
|
+
'[class*="stop-gen"]',
|
|
49
|
+
'[class*="stopGen"]',
|
|
49
50
|
'[class*="stop-btn"]',
|
|
50
51
|
'[class*="stopBtn"]',
|
|
51
52
|
'[class*="abort"]',
|
|
52
53
|
],
|
|
53
54
|
|
|
54
55
|
newChat: [
|
|
56
|
+
'.new-dialog-container-button',
|
|
57
|
+
'[class*="new-dialog-container"]',
|
|
55
58
|
'button[aria-label*="New chat" i]',
|
|
56
59
|
'button[aria-label*="新对话"]',
|
|
57
60
|
'button[aria-label*="New conversation" i]',
|
|
@@ -61,15 +64,19 @@ const SEL = {
|
|
|
61
64
|
'[class*="newChat"]',
|
|
62
65
|
],
|
|
63
66
|
|
|
64
|
-
// Response ready indicators
|
|
67
|
+
// Response ready indicators (chat.baidu.com)
|
|
65
68
|
responseReady: [
|
|
69
|
+
'.ai-entry-block.ai-markdown',
|
|
70
|
+
'.answer-container',
|
|
71
|
+
'.cs-answer-container',
|
|
72
|
+
'.answer-box',
|
|
66
73
|
'[class*="answer"]',
|
|
67
|
-
'[class*="response"]',
|
|
68
74
|
'[class*="markdown"]',
|
|
69
|
-
'.ds-markdown',
|
|
70
75
|
],
|
|
71
76
|
|
|
72
77
|
messageContainer: [
|
|
78
|
+
'#chat-container-main',
|
|
79
|
+
'[class*="chat-container"]',
|
|
73
80
|
'[class*="chat-content"]',
|
|
74
81
|
'[class*="message-list"]',
|
|
75
82
|
'[class*="conversation"]',
|
|
@@ -178,17 +185,22 @@ class YiyanBrowser {
|
|
|
178
185
|
|
|
179
186
|
const needsLogin = await this.page.evaluate(() => {
|
|
180
187
|
const url = window.location.href;
|
|
188
|
+
// URL 中包含登录/认证路径 → 需要登录
|
|
189
|
+
if (url.includes('/auth') || url.includes('/login') || url.includes('/sign')) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
// 存在密码输入框 → 需要登录
|
|
193
|
+
if (document.querySelector('input[type="password"]')) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
// 页面主体区域被登录界面遮挡(排除侧栏的"请登录"提示)
|
|
181
197
|
const bodyText = document.body?.innerText || '';
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
bodyText.includes('登录') ||
|
|
189
|
-
bodyText.includes('登 录') ||
|
|
190
|
-
!!document.querySelector('input[type="password"]')
|
|
191
|
-
);
|
|
198
|
+
const mainInput = document.querySelector('#chat-textarea, textarea.ci-textarea');
|
|
199
|
+
if (!mainInput) {
|
|
200
|
+
// 输入框不存在可能是需要登录
|
|
201
|
+
return bodyText.includes('Sign in') || bodyText.includes('Log in');
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
192
204
|
});
|
|
193
205
|
|
|
194
206
|
if (needsLogin) {
|
|
@@ -235,18 +247,24 @@ class YiyanBrowser {
|
|
|
235
247
|
// ── Sending Messages (stable: keyboard.type) ────────────────────────────────
|
|
236
248
|
|
|
237
249
|
async sendMessage(text) {
|
|
238
|
-
const { el } = await this._findInput();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
const { el, isTextarea } = await this._findInput();
|
|
251
|
+
|
|
252
|
+
if (isTextarea) {
|
|
253
|
+
// textarea 元素:用 fill() 快速填充
|
|
254
|
+
await el.click({ force: true });
|
|
255
|
+
await this.page.waitForTimeout(100);
|
|
256
|
+
await el.fill('');
|
|
257
|
+
await this.page.waitForTimeout(50);
|
|
258
|
+
await el.fill(text);
|
|
259
|
+
await this.page.waitForTimeout(100);
|
|
260
|
+
} else {
|
|
261
|
+
// contenteditable 元素:用键盘输入
|
|
262
|
+
await el.click({ clickCount: 3, force: true });
|
|
263
|
+
await this.page.waitForTimeout(100);
|
|
264
|
+
await this.page.keyboard.press('Delete');
|
|
265
|
+
await this.page.waitForTimeout(50);
|
|
266
|
+
await this.page.keyboard.type(text, { delay: 10 });
|
|
267
|
+
}
|
|
250
268
|
|
|
251
269
|
// Press Enter to send
|
|
252
270
|
await this.page.keyboard.press('Enter');
|
|
@@ -308,9 +326,21 @@ class YiyanBrowser {
|
|
|
308
326
|
while (Date.now() - start < timeout) {
|
|
309
327
|
const text = await this._extractLastMessage();
|
|
310
328
|
|
|
311
|
-
// ── 思考区域内容检测 ──
|
|
329
|
+
// ── 思考区域内容检测 (chat.baidu.com) ──
|
|
312
330
|
const thinkingInfo = await this.page.evaluate(() => {
|
|
313
|
-
|
|
331
|
+
// chat.baidu.com 深度思考区域
|
|
332
|
+
const thinkingSelectors = [
|
|
333
|
+
'[class*="deep-think"]',
|
|
334
|
+
'[class*="deepThink"]',
|
|
335
|
+
'[class*="deep-search"]',
|
|
336
|
+
'[class*="thinking-container"]',
|
|
337
|
+
'[class*="container__SPpah"]', // 旧版兼容
|
|
338
|
+
];
|
|
339
|
+
let thinkingEl = null;
|
|
340
|
+
for (const sel of thinkingSelectors) {
|
|
341
|
+
thinkingEl = document.querySelector(sel);
|
|
342
|
+
if (thinkingEl) break;
|
|
343
|
+
}
|
|
314
344
|
if (!thinkingEl) return { text: '', exists: false };
|
|
315
345
|
|
|
316
346
|
const s = window.getComputedStyle(thinkingEl);
|
|
@@ -320,17 +350,22 @@ class YiyanBrowser {
|
|
|
320
350
|
return { text, exists: true };
|
|
321
351
|
});
|
|
322
352
|
|
|
323
|
-
// ──
|
|
353
|
+
// ── 统一稳定性检测:答案不变即算稳定 ──
|
|
324
354
|
const thinkingStable = thinkingInfo.text === lastThinkingText;
|
|
325
355
|
const answerStable = text === lastText;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
356
|
+
// 如果有思考区域,需要两者都稳定;否则只看答案
|
|
357
|
+
const hasThinking = thinkingInfo.exists && thinkingInfo.text.length > 0;
|
|
358
|
+
const hasContent = text.length > 0;
|
|
359
|
+
const stable = hasThinking
|
|
360
|
+
? (thinkingStable && answerStable && hasContent)
|
|
361
|
+
: (answerStable && hasContent);
|
|
362
|
+
|
|
363
|
+
if (stable) {
|
|
364
|
+
// 稳定且有内容,检查稳定时间
|
|
330
365
|
if (Date.now() - lastStableTime >= stableDelay) {
|
|
331
366
|
stableCount++;
|
|
332
367
|
lastStableTime = Date.now();
|
|
333
|
-
logger.dim(`
|
|
368
|
+
logger.dim(`Stable: ${stableCount}/2 (answer: ${text.length} chars${hasThinking ? ', thinking: ' + thinkingInfo.text.length + ' chars' : ''})`);
|
|
334
369
|
}
|
|
335
370
|
} else {
|
|
336
371
|
// 任一变化,重置
|
|
@@ -340,13 +375,19 @@ class YiyanBrowser {
|
|
|
340
375
|
lastStableTime = Date.now();
|
|
341
376
|
}
|
|
342
377
|
|
|
343
|
-
// ── 完成标记检测 ──
|
|
378
|
+
// ── 完成标记检测 (chat.baidu.com) ──
|
|
344
379
|
const detected = await this.page.evaluate(() => {
|
|
345
380
|
const selectors = [
|
|
381
|
+
'.cos-icon.cos-icon-copy',
|
|
382
|
+
'.cos-icon-copy',
|
|
383
|
+
'.feedback-hover-show',
|
|
384
|
+
'[class*="feedback-wrapper"]',
|
|
385
|
+
'.cos-icon.cos-icon-share1',
|
|
386
|
+
'.cos-icon.cos-icon-feedback',
|
|
387
|
+
// 旧版兼容
|
|
346
388
|
'.dialogCardBottom__qoXjps3z',
|
|
347
389
|
'[class*="dialogCardBottom"]',
|
|
348
390
|
'[class*="dialog-bottom"]',
|
|
349
|
-
'[class*="response-footer"]',
|
|
350
391
|
];
|
|
351
392
|
for (const sel of selectors) {
|
|
352
393
|
const el = document.querySelector(sel);
|
|
@@ -400,16 +441,16 @@ class YiyanBrowser {
|
|
|
400
441
|
async _getMessageCount() {
|
|
401
442
|
return await this.page.evaluate(() => {
|
|
402
443
|
const candidates = [
|
|
403
|
-
'
|
|
404
|
-
'
|
|
405
|
-
'
|
|
406
|
-
'
|
|
407
|
-
'
|
|
408
|
-
'[
|
|
409
|
-
'[class*="
|
|
410
|
-
'.
|
|
411
|
-
'
|
|
412
|
-
'[class*="
|
|
444
|
+
'.ai-entry-block.ai-markdown',
|
|
445
|
+
'.answer-box',
|
|
446
|
+
'.cs-answer-container',
|
|
447
|
+
'.answer-container',
|
|
448
|
+
'.chat-search-answer-generate-item',
|
|
449
|
+
'[class*="answer-container"]',
|
|
450
|
+
'[class*="answer-box"]',
|
|
451
|
+
'.ai-markdown',
|
|
452
|
+
'.cs-question-bubble',
|
|
453
|
+
'[class*="question-bubble"]',
|
|
413
454
|
];
|
|
414
455
|
for (const sel of candidates) {
|
|
415
456
|
const els = document.querySelectorAll(sel);
|
|
@@ -435,10 +476,15 @@ class YiyanBrowser {
|
|
|
435
476
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
436
477
|
const tag = node.tagName.toLowerCase();
|
|
437
478
|
|
|
438
|
-
//
|
|
439
|
-
const cls = node.className
|
|
440
|
-
if (cls.includes('
|
|
441
|
-
|
|
479
|
+
// 排除思考过程区域和 UI 建议区域
|
|
480
|
+
const cls = (typeof node.className === 'string') ? node.className : '';
|
|
481
|
+
if (cls.includes('thinking') || cls.includes('Thinking') ||
|
|
482
|
+
cls.includes('deep-search') || cls.includes('deep_think') ||
|
|
483
|
+
cls.includes('suggestion') || cls.includes('follow-up') ||
|
|
484
|
+
cls.includes('question-container') || cls.includes('question-bubble') ||
|
|
485
|
+
cls.includes('quick-entrance') || cls.includes('feedback-wrapper') ||
|
|
486
|
+
cls.includes('hover-menu') || cls.includes('action-bar')) {
|
|
487
|
+
return; // 跳过思考区域和 UI 元素
|
|
442
488
|
}
|
|
443
489
|
|
|
444
490
|
if (tag === 'pre') {
|
|
@@ -474,62 +520,88 @@ class YiyanBrowser {
|
|
|
474
520
|
return result.trim();
|
|
475
521
|
}
|
|
476
522
|
|
|
477
|
-
// ──
|
|
523
|
+
// ── chat.baidu.com 新选择器 ──
|
|
524
|
+
|
|
525
|
+
// 优先: 获取最后一个回答块的内容
|
|
526
|
+
const answerBlocks = document.querySelectorAll(
|
|
527
|
+
'.ai-entry-block.ai-markdown, .answer-container.cs-enable-selection, .cs-answer-container'
|
|
528
|
+
);
|
|
529
|
+
if (answerBlocks.length > 0) {
|
|
530
|
+
const lastBlock = answerBlocks[answerBlocks.length - 1];
|
|
531
|
+
const text = getFullText(lastBlock);
|
|
532
|
+
if (text.length > 0) return text;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 回退 1: 通过 answer-box 获取
|
|
536
|
+
const answerBoxes = document.querySelectorAll('.answer-box, .last-answer-box');
|
|
537
|
+
if (answerBoxes.length > 0) {
|
|
538
|
+
const lastBox = answerBoxes[answerBoxes.length - 1];
|
|
539
|
+
const text = getFullText(lastBox);
|
|
540
|
+
if (text.length > 0) return text;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 回退 2: 旧版 #answer_text_id (兼容)
|
|
478
544
|
const answerEl = document.querySelector('#answer_text_id');
|
|
479
545
|
if (answerEl) {
|
|
480
546
|
return getFullText(answerEl);
|
|
481
547
|
}
|
|
482
548
|
|
|
483
|
-
//
|
|
549
|
+
// 回退 3: 通用 markdown 容器
|
|
550
|
+
const markdowns = document.querySelectorAll('.ai-markdown, [class*="markdown-content"]');
|
|
551
|
+
if (markdowns.length > 0) {
|
|
552
|
+
const lastMd = markdowns[markdowns.length - 1];
|
|
553
|
+
const text = getFullText(lastMd);
|
|
554
|
+
if (text.length > 0) return text;
|
|
555
|
+
}
|
|
556
|
+
|
|
484
557
|
return '';
|
|
485
558
|
});
|
|
486
559
|
}
|
|
487
560
|
|
|
488
561
|
async _isGenerating() {
|
|
489
562
|
return await this.page.evaluate(() => {
|
|
490
|
-
// ── 1.
|
|
491
|
-
const
|
|
492
|
-
'
|
|
493
|
-
'
|
|
494
|
-
'[class*="
|
|
495
|
-
'[class*="stopGen"]',
|
|
563
|
+
// ── 1. 检测 typing/generating 指示器 (chat.baidu.com) ──
|
|
564
|
+
const typingSelectors = [
|
|
565
|
+
'.cosd-markdown-content-typingall',
|
|
566
|
+
'.markdown-typing-all',
|
|
567
|
+
'[class*="typing"]',
|
|
496
568
|
'[class*="generating"]',
|
|
497
|
-
'[class*="
|
|
569
|
+
'[class*="loading-indicator"]',
|
|
570
|
+
'svg[class*="loading"]',
|
|
571
|
+
'svg[class*="spinner"]',
|
|
572
|
+
'[class*="blink"]',
|
|
573
|
+
'[class*="cursor-blink"]',
|
|
574
|
+
'[class*="pulsing"]',
|
|
498
575
|
];
|
|
499
|
-
for (const sel of
|
|
576
|
+
for (const sel of typingSelectors) {
|
|
500
577
|
const el = document.querySelector(sel);
|
|
501
578
|
if (el) {
|
|
502
579
|
const s = window.getComputedStyle(el);
|
|
503
|
-
if (s.display !== 'none' && s.visibility !== 'hidden'
|
|
580
|
+
if (s.display !== 'none' && s.visibility !== 'hidden') return true;
|
|
504
581
|
}
|
|
505
582
|
}
|
|
506
583
|
|
|
507
|
-
// ── 2.
|
|
508
|
-
const
|
|
509
|
-
'[
|
|
510
|
-
'[
|
|
511
|
-
'[class*="
|
|
512
|
-
'[class*="
|
|
513
|
-
'[class*="cursor"]',
|
|
514
|
-
'[class*="pulsing"]',
|
|
515
|
-
'[class*="thinking"]',
|
|
516
|
-
'svg[class*="loading"]',
|
|
517
|
-
'svg[class*="spinner"]',
|
|
518
|
-
'.loading-indicator',
|
|
519
|
-
'.generating-indicator',
|
|
584
|
+
// ── 2. 检测停止按钮 ──
|
|
585
|
+
const stopSelectors = [
|
|
586
|
+
'button[aria-label*="Stop" i]',
|
|
587
|
+
'button[aria-label*="停止"]',
|
|
588
|
+
'[class*="stop-gen"]',
|
|
589
|
+
'[class*="stopGen"]',
|
|
520
590
|
];
|
|
521
|
-
for (const sel of
|
|
591
|
+
for (const sel of stopSelectors) {
|
|
522
592
|
const el = document.querySelector(sel);
|
|
523
593
|
if (el) {
|
|
524
594
|
const s = window.getComputedStyle(el);
|
|
525
|
-
if (s.display !== 'none' && s.visibility !== 'hidden') return true;
|
|
595
|
+
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') return true;
|
|
526
596
|
}
|
|
527
597
|
}
|
|
528
598
|
|
|
529
|
-
// ── 3.
|
|
599
|
+
// ── 3. 检测完成标记 — 如果有回答但没有完成标记 → 还在生成 ──
|
|
530
600
|
const completionMarkers = [
|
|
531
|
-
'
|
|
532
|
-
'.
|
|
601
|
+
'.cos-icon.cos-icon-copy',
|
|
602
|
+
'.cos-icon-copy',
|
|
603
|
+
'.cos-icon.cos-icon-share1',
|
|
604
|
+
'.cos-icon-feedback',
|
|
533
605
|
'[class*="copy-btn"]',
|
|
534
606
|
'[class*="copyBtn"]',
|
|
535
607
|
'[aria-label*="Copy" i]',
|
|
@@ -537,6 +609,7 @@ class YiyanBrowser {
|
|
|
537
609
|
'[class*="regenerate"]',
|
|
538
610
|
'[class*="retry"]',
|
|
539
611
|
'[class*="action-btn"]',
|
|
612
|
+
'.feedback-hover-show',
|
|
540
613
|
];
|
|
541
614
|
let hasCompletionMarker = false;
|
|
542
615
|
for (const sel of completionMarkers) {
|
|
@@ -547,7 +620,9 @@ class YiyanBrowser {
|
|
|
547
620
|
}
|
|
548
621
|
|
|
549
622
|
// 如果有响应内容但没有完成标记 → 还在生成
|
|
550
|
-
const answerArea = document.querySelector(
|
|
623
|
+
const answerArea = document.querySelector(
|
|
624
|
+
'.ai-entry-block.ai-markdown, .answer-container, .cs-answer-container, .answer-box'
|
|
625
|
+
);
|
|
551
626
|
if (answerArea && answerArea.innerText && answerArea.innerText.length > 5) {
|
|
552
627
|
if (!hasCompletionMarker) {
|
|
553
628
|
return true;
|
|
@@ -568,29 +643,34 @@ class YiyanBrowser {
|
|
|
568
643
|
'正在思考中',
|
|
569
644
|
'正在思考',
|
|
570
645
|
'思考过程',
|
|
571
|
-
'
|
|
572
|
-
'我需要',
|
|
646
|
+
'深度思考',
|
|
573
647
|
'根据搜索结果',
|
|
574
648
|
'参考',
|
|
575
|
-
'picaole需要',
|
|
576
649
|
];
|
|
577
650
|
|
|
578
|
-
// Remove lines that
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
651
|
+
// Remove lines that are standalone thinking/UI markers
|
|
652
|
+
const lines = text.split('\n');
|
|
653
|
+
text = lines.filter(line => {
|
|
654
|
+
const trimmed = line.trim();
|
|
655
|
+
// 移除纯 UI 标记行
|
|
656
|
+
if (thinkingMarkers.some(m => trimmed === m)) return false;
|
|
657
|
+
// 移除建议追问行(新 UI 的 follow-up 按钮)
|
|
658
|
+
if (/^(能否|能再|能帮|可以|请用|用一句|用一段)/.test(trimmed) && trimmed.length < 50) return false;
|
|
659
|
+
return true;
|
|
660
|
+
}).join('\n');
|
|
661
|
+
|
|
662
|
+
// Remove everything before "准备输出结果" (thinking process end marker)
|
|
586
663
|
const outputMarker = '准备输出结果';
|
|
587
664
|
const markerIndex = text.indexOf(outputMarker);
|
|
588
665
|
if (markerIndex !== -1) {
|
|
589
666
|
text = text.slice(markerIndex + outputMarker.length).trim();
|
|
590
667
|
}
|
|
591
668
|
|
|
592
|
-
// Remove everything after regenerate/suggestion markers (
|
|
593
|
-
const cutMarkers = [
|
|
669
|
+
// Remove everything after regenerate/suggestion markers (UI elements)
|
|
670
|
+
const cutMarkers = [
|
|
671
|
+
'重新生成', '重新生成的', '换个回答', '输出更详细的', '再多提供',
|
|
672
|
+
'内容由AI生成', '查看使用规则',
|
|
673
|
+
];
|
|
594
674
|
for (const marker of cutMarkers) {
|
|
595
675
|
const cutIndex = text.indexOf(marker);
|
|
596
676
|
if (cutIndex !== -1) {
|