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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.10.3",
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
- '.editable__T7WAW4uW',
18
- '[role="textbox"]',
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
- return (
183
- url.includes('/auth') ||
184
- url.includes('/login') ||
185
- url.includes('/sign') ||
186
- bodyText.includes('Sign in') ||
187
- bodyText.includes('Log in') ||
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
- // Focus and select all existing content
241
- await el.click({ clickCount: 3, force: true });
242
- await this.page.waitForTimeout(100);
243
-
244
- // Clear by pressing Delete
245
- await this.page.keyboard.press('Delete');
246
- await this.page.waitForTimeout(50);
247
-
248
- // Type text character by character (stable, works reliably)
249
- await this.page.keyboard.type(text, { delay: 10 });
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
- const thinkingEl = document.querySelector('.container__SPpahQHm, [class*="container__SPpah"]');
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
- const hasContent = thinkingInfo.text.length > 5 && text.length > 0;
327
-
328
- if (thinkingStable && answerStable && hasContent) {
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(`Both stable: ${stableCount}/2 (thinking: ${thinkingInfo.text.length} chars, answer: ${text.length} chars)`);
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
- '[class*="answer"]',
404
- '[class*="response"]',
405
- '[class*="content"]',
406
- '[class*="markdown"]',
407
- '[class*="assistant"][class*="message"]',
408
- '[data-role="assistant"]',
409
- '[class*="markdown-content"]',
410
- '.ds-markdown',
411
- '[class*="chat-message"]',
412
- '[class*="message-bubble"]',
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('container__SPpahQHm') || cls.includes('thinking') || cls.includes('Thinking')) {
441
- return; // 跳过思考区域
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
- // ── answer_text_id 提取内容 ──
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
- // answer_text_id 不存在,返回空字符串
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. 检测停止按钮/生成状态 UI ──
491
- const stopSelectors = [
492
- 'button[aria-label*="Stop" i]',
493
- 'button[aria-label*="停止"]',
494
- '[class*="stop-gen"]',
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*=" Generating"]',
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 stopSelectors) {
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' && s.opacity !== '0') return true;
580
+ if (s.display !== 'none' && s.visibility !== 'hidden') return true;
504
581
  }
505
582
  }
506
583
 
507
- // ── 2. 检测加载动画/typing指示器 ──
508
- const loaderSelectors = [
509
- '[class*="typing"]',
510
- '[class*="loading"]',
511
- '[class*="spinner"]',
512
- '[class*="blink"]',
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 loaderSelectors) {
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
- '[class*="dialogCardBottom"]',
532
- '.dialogCardBottom__qoXjps3z',
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('[class*="answer"], [class*="response"], [class*="markdown"]');
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 contain thinking markers
579
- for (const marker of thinkingMarkers) {
580
- // 如果整行包含思考标记,移除该行
581
- const lines = text.split('\n');
582
- text = lines.filter(line => !line.includes(marker)).join('\n');
583
- }
584
-
585
- // Remove everything before "准备输出结果" (Yiyan's thinking process end marker)
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 (Yiyan's UI elements)
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) {