yiyan-browser-agent 1.10.3 → 1.11.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 +1 -1
  2. package/src/browser.js +193 -101
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.1",
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');
@@ -302,15 +320,28 @@ class YiyanBrowser {
302
320
  let lastThinkingText = '';
303
321
  let stableCount = 0;
304
322
  let lastStableTime = Date.now();
323
+ let textChangedAt = Date.now(); // 文本最后变化时间
305
324
  let dotCount = 0;
306
325
  let hasCompletionMarker = false;
307
326
 
308
327
  while (Date.now() - start < timeout) {
309
328
  const text = await this._extractLastMessage();
310
329
 
311
- // ── 思考区域内容检测 ──
330
+ // ── 思考区域内容检测 (chat.baidu.com) ──
312
331
  const thinkingInfo = await this.page.evaluate(() => {
313
- const thinkingEl = document.querySelector('.container__SPpahQHm, [class*="container__SPpah"]');
332
+ // chat.baidu.com 深度思考区域
333
+ const thinkingSelectors = [
334
+ '[class*="deep-think"]',
335
+ '[class*="deepThink"]',
336
+ '[class*="deep-search"]',
337
+ '[class*="thinking-container"]',
338
+ '[class*="container__SPpah"]', // 旧版兼容
339
+ ];
340
+ let thinkingEl = null;
341
+ for (const sel of thinkingSelectors) {
342
+ thinkingEl = document.querySelector(sel);
343
+ if (thinkingEl) break;
344
+ }
314
345
  if (!thinkingEl) return { text: '', exists: false };
315
346
 
316
347
  const s = window.getComputedStyle(thinkingEl);
@@ -320,17 +351,22 @@ class YiyanBrowser {
320
351
  return { text, exists: true };
321
352
  });
322
353
 
323
- // ── 统一稳定性检测:两者都不变才算稳定 ──
354
+ // ── 统一稳定性检测:答案不变即算稳定 ──
324
355
  const thinkingStable = thinkingInfo.text === lastThinkingText;
325
356
  const answerStable = text === lastText;
326
- const hasContent = thinkingInfo.text.length > 5 && text.length > 0;
327
-
328
- if (thinkingStable && answerStable && hasContent) {
329
- // 两者都稳定且有内容,检查稳定时间
357
+ // 如果有思考区域,需要两者都稳定;否则只看答案
358
+ const hasThinking = thinkingInfo.exists && thinkingInfo.text.length > 0;
359
+ const hasContent = text.length > 0;
360
+ const stable = hasThinking
361
+ ? (thinkingStable && answerStable && hasContent)
362
+ : (answerStable && hasContent);
363
+
364
+ if (stable) {
365
+ // 稳定且有内容,检查稳定时间
330
366
  if (Date.now() - lastStableTime >= stableDelay) {
331
367
  stableCount++;
332
368
  lastStableTime = Date.now();
333
- logger.dim(`Both stable: ${stableCount}/2 (thinking: ${thinkingInfo.text.length} chars, answer: ${text.length} chars)`);
369
+ logger.dim(`Stable: ${stableCount}/2 (answer: ${text.length} chars${hasThinking ? ', thinking: ' + thinkingInfo.text.length + ' chars' : ''})`);
334
370
  }
335
371
  } else {
336
372
  // 任一变化,重置
@@ -338,15 +374,22 @@ class YiyanBrowser {
338
374
  lastText = text;
339
375
  stableCount = 0;
340
376
  lastStableTime = Date.now();
377
+ textChangedAt = Date.now();
341
378
  }
342
379
 
343
- // ── 完成标记检测 ──
380
+ // ── 完成标记检测 (chat.baidu.com) ──
344
381
  const detected = await this.page.evaluate(() => {
345
382
  const selectors = [
383
+ '.cos-icon.cos-icon-copy',
384
+ '.cos-icon-copy',
385
+ '.feedback-hover-show',
386
+ '[class*="feedback-wrapper"]',
387
+ '.cos-icon.cos-icon-share1',
388
+ '.cos-icon.cos-icon-feedback',
389
+ // 旧版兼容
346
390
  '.dialogCardBottom__qoXjps3z',
347
391
  '[class*="dialogCardBottom"]',
348
392
  '[class*="dialog-bottom"]',
349
- '[class*="response-footer"]',
350
393
  ];
351
394
  for (const sel of selectors) {
352
395
  const el = document.querySelector(sel);
@@ -363,10 +406,12 @@ class YiyanBrowser {
363
406
  }
364
407
 
365
408
  // ── 完成判断 ──
366
- // 条件1: 标记出现 + 两者都稳定 2 次
367
- // 条件2(备用): 两者都稳定 3 次(即使没有完成标记)
409
+ // 条件1: 完成标记出现 + 稳定 2 次
410
+ // 条件2: 稳定 3 次(即使没有完成标记)
411
+ // 条件3(兜底): 文本长时间稳定不变(≥5秒),强制认为完成
368
412
  const condition1 = hasCompletionMarker && stableCount >= 2;
369
413
  const condition2 = stableCount >= 3;
414
+ const longStable = (Date.now() - textChangedAt) >= 5000 && text.length > 0;
370
415
 
371
416
  if (condition1 || condition2) {
372
417
  // 检查 _isGenerating 作为最终确认
@@ -379,6 +424,13 @@ class YiyanBrowser {
379
424
  // _isGenerating 说还在生成,重置计数继续等待
380
425
  stableCount = 0;
381
426
  lastStableTime = Date.now();
427
+ } else if (longStable) {
428
+ // 兜底:文本已经 5 秒没变化了,强制完成
429
+ logger.dim('Long stable fallback — text unchanged for 5s, forcing completion');
430
+ await this.page.waitForTimeout(300);
431
+ logger.clearLine();
432
+ logger.success('Response complete (stable timeout)');
433
+ break;
382
434
  }
383
435
 
384
436
  // Progress indicator
@@ -400,16 +452,16 @@ class YiyanBrowser {
400
452
  async _getMessageCount() {
401
453
  return await this.page.evaluate(() => {
402
454
  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"]',
455
+ '.ai-entry-block.ai-markdown',
456
+ '.answer-box',
457
+ '.cs-answer-container',
458
+ '.answer-container',
459
+ '.chat-search-answer-generate-item',
460
+ '[class*="answer-container"]',
461
+ '[class*="answer-box"]',
462
+ '.ai-markdown',
463
+ '.cs-question-bubble',
464
+ '[class*="question-bubble"]',
413
465
  ];
414
466
  for (const sel of candidates) {
415
467
  const els = document.querySelectorAll(sel);
@@ -435,10 +487,15 @@ class YiyanBrowser {
435
487
  if (node.nodeType !== Node.ELEMENT_NODE) return;
436
488
  const tag = node.tagName.toLowerCase();
437
489
 
438
- // 排除思考过程区域
439
- const cls = node.className || '';
440
- if (cls.includes('container__SPpahQHm') || cls.includes('thinking') || cls.includes('Thinking')) {
441
- return; // 跳过思考区域
490
+ // 排除思考过程区域和 UI 建议区域
491
+ const cls = (typeof node.className === 'string') ? node.className : '';
492
+ if (cls.includes('thinking') || cls.includes('Thinking') ||
493
+ cls.includes('deep-search') || cls.includes('deep_think') ||
494
+ cls.includes('suggestion') || cls.includes('follow-up') ||
495
+ cls.includes('question-container') || cls.includes('question-bubble') ||
496
+ cls.includes('quick-entrance') || cls.includes('feedback-wrapper') ||
497
+ cls.includes('hover-menu') || cls.includes('action-bar')) {
498
+ return; // 跳过思考区域和 UI 元素
442
499
  }
443
500
 
444
501
  if (tag === 'pre') {
@@ -474,27 +531,53 @@ class YiyanBrowser {
474
531
  return result.trim();
475
532
  }
476
533
 
477
- // ── answer_text_id 提取内容 ──
534
+ // ── chat.baidu.com 新选择器 ──
535
+
536
+ // 优先: 获取最后一个回答块的内容
537
+ const answerBlocks = document.querySelectorAll(
538
+ '.ai-entry-block.ai-markdown, .answer-container.cs-enable-selection, .cs-answer-container'
539
+ );
540
+ if (answerBlocks.length > 0) {
541
+ const lastBlock = answerBlocks[answerBlocks.length - 1];
542
+ const text = getFullText(lastBlock);
543
+ if (text.length > 0) return text;
544
+ }
545
+
546
+ // 回退 1: 通过 answer-box 获取
547
+ const answerBoxes = document.querySelectorAll('.answer-box, .last-answer-box');
548
+ if (answerBoxes.length > 0) {
549
+ const lastBox = answerBoxes[answerBoxes.length - 1];
550
+ const text = getFullText(lastBox);
551
+ if (text.length > 0) return text;
552
+ }
553
+
554
+ // 回退 2: 旧版 #answer_text_id (兼容)
478
555
  const answerEl = document.querySelector('#answer_text_id');
479
556
  if (answerEl) {
480
557
  return getFullText(answerEl);
481
558
  }
482
559
 
483
- // answer_text_id 不存在,返回空字符串
560
+ // 回退 3: 通用 markdown 容器
561
+ const markdowns = document.querySelectorAll('.ai-markdown, [class*="markdown-content"]');
562
+ if (markdowns.length > 0) {
563
+ const lastMd = markdowns[markdowns.length - 1];
564
+ const text = getFullText(lastMd);
565
+ if (text.length > 0) return text;
566
+ }
567
+
484
568
  return '';
485
569
  });
486
570
  }
487
571
 
488
572
  async _isGenerating() {
489
573
  return await this.page.evaluate(() => {
490
- // ── 1. 检测停止按钮/生成状态 UI ──
574
+ // ── 1. 检测停止按钮(最可靠的生成中信号)──
491
575
  const stopSelectors = [
492
576
  'button[aria-label*="Stop" i]',
493
577
  'button[aria-label*="停止"]',
494
578
  '[class*="stop-gen"]',
495
579
  '[class*="stopGen"]',
496
- '[class*="generating"]',
497
- '[class*=" Generating"]',
580
+ '[class*="stop-btn"]',
498
581
  ];
499
582
  for (const sel of stopSelectors) {
500
583
  const el = document.querySelector(sel);
@@ -504,21 +587,15 @@ class YiyanBrowser {
504
587
  }
505
588
  }
506
589
 
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"]',
590
+ // ── 2. 检测活跃的 CSS 动画(真正的 loading spinner,不是永久类名)──
591
+ // 只检测 svg 动画元素和明确的 loading 指示器
592
+ const activeAnimSelectors = [
516
593
  'svg[class*="loading"]',
517
594
  'svg[class*="spinner"]',
518
- '.loading-indicator',
519
- '.generating-indicator',
595
+ '[class*="loading-indicator"]',
596
+ '[class*="generating-indicator"]',
520
597
  ];
521
- for (const sel of loaderSelectors) {
598
+ for (const sel of activeAnimSelectors) {
522
599
  const el = document.querySelector(sel);
523
600
  if (el) {
524
601
  const s = window.getComputedStyle(el);
@@ -526,30 +603,40 @@ class YiyanBrowser {
526
603
  }
527
604
  }
528
605
 
529
- // ── 3. 检测是否缺少完成标记元素 ──
606
+ // ── 3. 如果有完成标记 → 肯定已完成 ──
530
607
  const completionMarkers = [
531
- '[class*="dialogCardBottom"]',
532
- '.dialogCardBottom__qoXjps3z',
608
+ '.cos-icon.cos-icon-copy',
609
+ '.cos-icon-copy',
610
+ '.feedback-hover-show',
611
+ '.cos-icon.cos-icon-share1',
612
+ '.cos-icon.cos-icon-feedback',
533
613
  '[class*="copy-btn"]',
534
- '[class*="copyBtn"]',
535
614
  '[aria-label*="Copy" i]',
536
615
  '[aria-label*="复制"]',
537
- '[class*="regenerate"]',
538
- '[class*="retry"]',
539
- '[class*="action-btn"]',
540
616
  ];
541
- let hasCompletionMarker = false;
542
617
  for (const sel of completionMarkers) {
543
- if (document.querySelector(sel)) {
544
- hasCompletionMarker = true;
545
- break;
546
- }
618
+ if (document.querySelector(sel)) return false;
547
619
  }
548
620
 
549
- // 如果有响应内容但没有完成标记 还在生成
550
- const answerArea = document.querySelector('[class*="answer"], [class*="response"], [class*="markdown"]');
551
- if (answerArea && answerArea.innerText && answerArea.innerText.length > 5) {
552
- if (!hasCompletionMarker) {
621
+ // ── 4. 检查是否有光标闪烁(生成中的光标)──
622
+ // getComputedStyle 检测 animation,而不是类名
623
+ const answerArea = document.querySelector(
624
+ '.ai-entry-block.ai-markdown, .answer-container, .cs-answer-container'
625
+ );
626
+ if (answerArea) {
627
+ // 查找最后子元素是否有动画光标
628
+ const lastChild = answerArea.lastElementChild;
629
+ if (lastChild) {
630
+ const s = window.getComputedStyle(lastChild);
631
+ // 如果最后一个元素有活跃的动画 → 还在生成
632
+ if (s.animationName && s.animationName !== 'none' &&
633
+ s.display !== 'none' && s.visibility !== 'hidden') {
634
+ return true;
635
+ }
636
+ }
637
+
638
+ // 有回答内容但没有完成标记 → 可能还在生成
639
+ if (answerArea.innerText && answerArea.innerText.length > 5) {
553
640
  return true;
554
641
  }
555
642
  }
@@ -568,29 +655,34 @@ class YiyanBrowser {
568
655
  '正在思考中',
569
656
  '正在思考',
570
657
  '思考过程',
571
- '我来',
572
- '我需要',
658
+ '深度思考',
573
659
  '根据搜索结果',
574
660
  '参考',
575
- 'picaole需要',
576
661
  ];
577
662
 
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)
663
+ // Remove lines that are standalone thinking/UI markers
664
+ const lines = text.split('\n');
665
+ text = lines.filter(line => {
666
+ const trimmed = line.trim();
667
+ // 移除纯 UI 标记行
668
+ if (thinkingMarkers.some(m => trimmed === m)) return false;
669
+ // 移除建议追问行(新 UI 的 follow-up 按钮)
670
+ if (/^(能否|能再|能帮|可以|请用|用一句|用一段)/.test(trimmed) && trimmed.length < 50) return false;
671
+ return true;
672
+ }).join('\n');
673
+
674
+ // Remove everything before "准备输出结果" (thinking process end marker)
586
675
  const outputMarker = '准备输出结果';
587
676
  const markerIndex = text.indexOf(outputMarker);
588
677
  if (markerIndex !== -1) {
589
678
  text = text.slice(markerIndex + outputMarker.length).trim();
590
679
  }
591
680
 
592
- // Remove everything after regenerate/suggestion markers (Yiyan's UI elements)
593
- const cutMarkers = ['重新生成', '重新生成的', '换个回答', '输出更详细的', '再多提供'];
681
+ // Remove everything after regenerate/suggestion markers (UI elements)
682
+ const cutMarkers = [
683
+ '重新生成', '重新生成的', '换个回答', '输出更详细的', '再多提供',
684
+ '内容由AI生成', '查看使用规则',
685
+ ];
594
686
  for (const marker of cutMarkers) {
595
687
  const cutIndex = text.indexOf(marker);
596
688
  if (cutIndex !== -1) {