yiyan-browser-agent 1.4.5 → 1.4.7
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/LICENSE +1 -1
- package/README.md +241 -153
- package/package.json +25 -30
- package/src/agent.js +63 -0
- package/src/browser.js +589 -0
- package/src/calibrate.js +178 -0
- package/src/config.js +68 -0
- package/src/index.js +115 -0
- package/src/logger.js +118 -0
- package/src/parser.js +272 -0
- package/src/postinstall.js +26 -0
- package/src/prompt.js +188 -0
- package/src/tools.js +547 -0
- package/dist/cli.d.ts +0 -27
- package/dist/cli.js +0 -857
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -73
- package/dist/index.js +0 -666
- package/dist/index.js.map +0 -1
- package/dist/postinstall.d.ts +0 -2
- package/dist/postinstall.js +0 -70
- package/dist/postinstall.js.map +0 -1
- package/dist/types-BhQ78DYf.d.ts +0 -39
package/src/browser.js
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// src/browser.js — Playwright controller for Yiyan (yiyan.baidu.com)
|
|
2
|
+
// Performance optimized: smart waits, paste input, MutationObserver
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { chromium } = require('playwright');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
const logger = require('./logger');
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Selector banks — ordered by likelihood, with fallbacks
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const SEL = {
|
|
15
|
+
chatInput: [
|
|
16
|
+
'.editable__T7WAW4uW',
|
|
17
|
+
'[role="textbox"]',
|
|
18
|
+
'.editable',
|
|
19
|
+
'#chat-input',
|
|
20
|
+
'textarea[placeholder]',
|
|
21
|
+
'textarea',
|
|
22
|
+
'[contenteditable="true"][role="textbox"]',
|
|
23
|
+
'[contenteditable="true"]',
|
|
24
|
+
'[class*="input-box"]',
|
|
25
|
+
'[class*="chatInput"]',
|
|
26
|
+
'[class*="editor"]',
|
|
27
|
+
'.input-area textarea',
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
sendButton: [
|
|
31
|
+
'button[aria-label*="Send" i]',
|
|
32
|
+
'button[aria-label*="发送"]',
|
|
33
|
+
'button[aria-label*="send" i]',
|
|
34
|
+
'[data-testid="send-button"]',
|
|
35
|
+
'button[type="submit"]',
|
|
36
|
+
'[class*="send-btn"]',
|
|
37
|
+
'[class*="sendBtn"]',
|
|
38
|
+
'[class*="send-button"]',
|
|
39
|
+
'[class*="submit"]',
|
|
40
|
+
'.send-btn',
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
stopButton: [
|
|
44
|
+
'button[aria-label*="Stop" i]',
|
|
45
|
+
'button[aria-label*="停止"]',
|
|
46
|
+
'[aria-label*="stop generating" i]',
|
|
47
|
+
'[data-testid="stop-button"]',
|
|
48
|
+
'[class*="stop-btn"]',
|
|
49
|
+
'[class*="stopBtn"]',
|
|
50
|
+
'[class*="abort"]',
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
newChat: [
|
|
54
|
+
'button[aria-label*="New chat" i]',
|
|
55
|
+
'button[aria-label*="新对话"]',
|
|
56
|
+
'button[aria-label*="New conversation" i]',
|
|
57
|
+
'a[href="/"][aria-label]',
|
|
58
|
+
'[data-testid="new-chat"]',
|
|
59
|
+
'[class*="new-chat"]',
|
|
60
|
+
'[class*="newChat"]',
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
// Response ready indicators
|
|
64
|
+
responseReady: [
|
|
65
|
+
'[class*="answer"]',
|
|
66
|
+
'[class*="response"]',
|
|
67
|
+
'[class*="markdown"]',
|
|
68
|
+
'.ds-markdown',
|
|
69
|
+
],
|
|
70
|
+
|
|
71
|
+
messageContainer: [
|
|
72
|
+
'[class*="chat-content"]',
|
|
73
|
+
'[class*="message-list"]',
|
|
74
|
+
'[class*="conversation"]',
|
|
75
|
+
'main',
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
// YiyanBrowser class (optimized)
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
class YiyanBrowser {
|
|
84
|
+
constructor() {
|
|
85
|
+
this.context = null;
|
|
86
|
+
this.page = null;
|
|
87
|
+
this._closed = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
async launch() {
|
|
93
|
+
logger.info('Launching browser with persistent session...');
|
|
94
|
+
|
|
95
|
+
const sessionDir = path.resolve(config.SESSION_DIR);
|
|
96
|
+
|
|
97
|
+
this.context = await chromium.launchPersistentContext(sessionDir, {
|
|
98
|
+
headless : config.HEADLESS,
|
|
99
|
+
viewport : { width: 1280, height: 900 },
|
|
100
|
+
userAgent : [
|
|
101
|
+
'Mozilla/5.0 (X11; Linux x86_64)',
|
|
102
|
+
'AppleWebKit/537.36 (KHTML, like Gecko)',
|
|
103
|
+
'Chrome/124.0.0.0 Safari/537.36',
|
|
104
|
+
].join(' '),
|
|
105
|
+
args: [
|
|
106
|
+
'--disable-blink-features=AutomationControlled',
|
|
107
|
+
'--no-first-run',
|
|
108
|
+
'--disable-default-apps',
|
|
109
|
+
'--no-sandbox',
|
|
110
|
+
'--disable-setuid-sandbox',
|
|
111
|
+
],
|
|
112
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const pages = this.context.pages();
|
|
116
|
+
this.page = pages.length > 0 ? pages[0] : await this.context.newPage();
|
|
117
|
+
|
|
118
|
+
await this.page.addInitScript(() => {
|
|
119
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await this._navigate(config.YIYAN_URL);
|
|
123
|
+
await this._ensureLoggedIn();
|
|
124
|
+
|
|
125
|
+
logger.success('Browser ready!');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async close() {
|
|
129
|
+
if (this._closed) return;
|
|
130
|
+
this._closed = true;
|
|
131
|
+
try { await this.context?.close(); } catch {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Navigation (optimized: smart wait) ─────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async _navigate(url) {
|
|
137
|
+
try {
|
|
138
|
+
// Fast navigation - domcontentloaded is faster than networkidle
|
|
139
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 });
|
|
140
|
+
await this.page.waitForTimeout(300); // Minimal wait for SPA render
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.warn(`Navigation warning: ${err.message}`);
|
|
143
|
+
await this.page.waitForTimeout(800);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async newChat() {
|
|
148
|
+
try {
|
|
149
|
+
for (const sel of SEL.newChat) {
|
|
150
|
+
try {
|
|
151
|
+
const el = await this.page.waitForSelector(sel, { timeout: 1_500, state: 'visible' });
|
|
152
|
+
if (el) {
|
|
153
|
+
await el.click();
|
|
154
|
+
await this.page.waitForTimeout(200);
|
|
155
|
+
logger.dim('Started new chat session');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
162
|
+
await this._navigate(config.YIYAN_URL);
|
|
163
|
+
logger.dim('Navigated to Yiyan home (new chat)');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Login handling (optimized) ─────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
async _ensureLoggedIn() {
|
|
169
|
+
// Quick check, no unnecessary waiting
|
|
170
|
+
await this.page.waitForTimeout(200);
|
|
171
|
+
|
|
172
|
+
const needsLogin = await this.page.evaluate(() => {
|
|
173
|
+
const url = window.location.href;
|
|
174
|
+
const bodyText = document.body?.innerText || '';
|
|
175
|
+
return (
|
|
176
|
+
url.includes('/auth') ||
|
|
177
|
+
url.includes('/login') ||
|
|
178
|
+
url.includes('/sign') ||
|
|
179
|
+
bodyText.includes('Sign in') ||
|
|
180
|
+
bodyText.includes('Log in') ||
|
|
181
|
+
bodyText.includes('登录') ||
|
|
182
|
+
bodyText.includes('登 录') ||
|
|
183
|
+
!!document.querySelector('input[type="password"]')
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (needsLogin) {
|
|
188
|
+
this._printLoginBanner();
|
|
189
|
+
await this._waitForEnter();
|
|
190
|
+
await this.page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 15_000 }).catch(() => {});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_printLoginBanner() {
|
|
195
|
+
console.log('');
|
|
196
|
+
logger.warn('╔══════════════════════════════════════════════╗');
|
|
197
|
+
logger.warn('║ 🔐 LOGIN REQUIRED ║');
|
|
198
|
+
logger.warn('║ ║');
|
|
199
|
+
logger.warn('║ 1. Log in to Yiyan (文心一言) in browser ║');
|
|
200
|
+
logger.warn('║ 2. Return here and press ENTER to continue║');
|
|
201
|
+
logger.warn('╚══════════════════════════════════════════════╝');
|
|
202
|
+
console.log('');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async _waitForEnter() {
|
|
206
|
+
return new Promise(resolve => {
|
|
207
|
+
const stdin = process.stdin;
|
|
208
|
+
const wasRaw = stdin.isRaw;
|
|
209
|
+
const wasPaused = !stdin.readable;
|
|
210
|
+
|
|
211
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
212
|
+
stdin.resume();
|
|
213
|
+
|
|
214
|
+
const handler = chunk => {
|
|
215
|
+
const s = chunk.toString();
|
|
216
|
+
if (s.includes('\n') || s.includes('\r')) {
|
|
217
|
+
stdin.removeListener('data', handler);
|
|
218
|
+
if (stdin.isTTY && wasRaw) stdin.setRawMode(true);
|
|
219
|
+
if (wasPaused) stdin.pause();
|
|
220
|
+
resolve();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
stdin.on('data', handler);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Sending Messages (stable: keyboard.type) ────────────────────────────────
|
|
229
|
+
|
|
230
|
+
async sendMessage(text) {
|
|
231
|
+
const { el } = await this._findInput();
|
|
232
|
+
|
|
233
|
+
// Focus and select all existing content
|
|
234
|
+
await el.click({ clickCount: 3, force: true });
|
|
235
|
+
await this.page.waitForTimeout(100);
|
|
236
|
+
|
|
237
|
+
// Clear by pressing Delete
|
|
238
|
+
await this.page.keyboard.press('Delete');
|
|
239
|
+
await this.page.waitForTimeout(50);
|
|
240
|
+
|
|
241
|
+
// Type text character by character (stable, works reliably)
|
|
242
|
+
await this.page.keyboard.type(text, { delay: 10 });
|
|
243
|
+
|
|
244
|
+
// Press Enter to send
|
|
245
|
+
await this.page.keyboard.press('Enter');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async _findInput() {
|
|
249
|
+
for (const sel of SEL.chatInput) {
|
|
250
|
+
try {
|
|
251
|
+
// Increased timeout for initial page load
|
|
252
|
+
const el = await this.page.waitForSelector(sel, { timeout: 8_000, state: 'visible' });
|
|
253
|
+
if (!el) continue;
|
|
254
|
+
const tagName = await el.evaluate(e => e.tagName.toLowerCase());
|
|
255
|
+
const isContentEditable = await el.evaluate(e => e.isContentEditable);
|
|
256
|
+
return { el, isTextarea: tagName === 'textarea' && !isContentEditable };
|
|
257
|
+
} catch {}
|
|
258
|
+
}
|
|
259
|
+
throw new Error(
|
|
260
|
+
'Cannot find the Yiyan chat input box.\n' +
|
|
261
|
+
' → Make sure the page is fully loaded and you are logged in.\n' +
|
|
262
|
+
' → Run with --debug to inspect DOM selectors.\n' +
|
|
263
|
+
' → Run: node src/calibrate.js to auto-detect selectors.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Waiting for Response (stable polling) ────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Wait until Yiyan finishes generating and return the response text.
|
|
271
|
+
*/
|
|
272
|
+
async waitForResponse() {
|
|
273
|
+
const timeout = config.RESPONSE_TIMEOUT;
|
|
274
|
+
const stableDelay = config.STABLE_DELAY;
|
|
275
|
+
const start = Date.now();
|
|
276
|
+
|
|
277
|
+
// Phase 1: wait for a new message to appear
|
|
278
|
+
const initialCount = await this._getMessageCount();
|
|
279
|
+
let appeared = false;
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < 40; i++) {
|
|
282
|
+
const count = await this._getMessageCount();
|
|
283
|
+
if (count > initialCount) { appeared = true; break; }
|
|
284
|
+
await this.page.waitForTimeout(200);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!appeared) logger.warn('Response may have been delayed — continuing to wait...');
|
|
288
|
+
|
|
289
|
+
// Phase 2: wait for text to stabilise
|
|
290
|
+
let lastText = '';
|
|
291
|
+
let stableStart = null;
|
|
292
|
+
let dotCount = 0;
|
|
293
|
+
|
|
294
|
+
while (Date.now() - start < timeout) {
|
|
295
|
+
const text = await this._extractLastMessage();
|
|
296
|
+
|
|
297
|
+
if (text !== lastText) {
|
|
298
|
+
lastText = text;
|
|
299
|
+
stableStart = null;
|
|
300
|
+
} else if (text.length > 0) {
|
|
301
|
+
if (!stableStart) stableStart = Date.now();
|
|
302
|
+
else if (Date.now() - stableStart >= stableDelay) {
|
|
303
|
+
if (!await this._isGenerating()) break;
|
|
304
|
+
stableStart = null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Progress indicator
|
|
309
|
+
dotCount = (dotCount + 1) % 4;
|
|
310
|
+
logger.thinking(`Receiving response${'.'.repeat(dotCount)} (${text.length} chars)`);
|
|
311
|
+
}
|
|
312
|
+
await this.page.waitForTimeout(200);
|
|
313
|
+
|
|
314
|
+
logger.clearLine();
|
|
315
|
+
|
|
316
|
+
const final = await this._extractLastMessage();
|
|
317
|
+
return this._cleanText(final);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── DOM Extraction ─────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
async _getMessageCount() {
|
|
323
|
+
return await this.page.evaluate(() => {
|
|
324
|
+
const candidates = [
|
|
325
|
+
'[class*="answer"]',
|
|
326
|
+
'[class*="response"]',
|
|
327
|
+
'[class*="content"]',
|
|
328
|
+
'[class*="markdown"]',
|
|
329
|
+
'[class*="assistant"][class*="message"]',
|
|
330
|
+
'[data-role="assistant"]',
|
|
331
|
+
'[class*="markdown-content"]',
|
|
332
|
+
'.ds-markdown',
|
|
333
|
+
'[class*="chat-message"]',
|
|
334
|
+
'[class*="message-bubble"]',
|
|
335
|
+
];
|
|
336
|
+
for (const sel of candidates) {
|
|
337
|
+
const els = document.querySelectorAll(sel);
|
|
338
|
+
if (els.length > 0) return els.length;
|
|
339
|
+
}
|
|
340
|
+
const textBlocks = Array.from(document.querySelectorAll('div, section, article'))
|
|
341
|
+
.filter(el => el.innerText && el.innerText.length > 50);
|
|
342
|
+
return textBlocks.length;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async _extractLastMessage() {
|
|
347
|
+
return await this.page.evaluate(() => {
|
|
348
|
+
function getFullText(el) {
|
|
349
|
+
if (!el) return '';
|
|
350
|
+
let result = '';
|
|
351
|
+
|
|
352
|
+
function walk(node) {
|
|
353
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
354
|
+
result += node.textContent;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
358
|
+
const tag = node.tagName.toLowerCase();
|
|
359
|
+
|
|
360
|
+
if (tag === 'pre') {
|
|
361
|
+
const codeEl = node.querySelector('code');
|
|
362
|
+
if (codeEl) {
|
|
363
|
+
const cls = codeEl.className || '';
|
|
364
|
+
const lang = (cls.match(/language-(\S+)/) || [])[1] || '';
|
|
365
|
+
const body = codeEl.textContent || '';
|
|
366
|
+
result += '\n```' + lang + '\n' + body + '\n```\n';
|
|
367
|
+
} else {
|
|
368
|
+
result += '\n```\n' + node.textContent + '\n```\n';
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (tag === 'code') {
|
|
374
|
+
const parentTag = node.parentElement && node.parentElement.tagName
|
|
375
|
+
? node.parentElement.tagName.toLowerCase() : '';
|
|
376
|
+
if (parentTag !== 'pre') {
|
|
377
|
+
result += '`' + node.textContent + '`';
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const child of node.childNodes) walk(child);
|
|
383
|
+
|
|
384
|
+
if (['p','div','li','br','h1','h2','h3','h4','h5','h6'].includes(tag)) {
|
|
385
|
+
result += '\n';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
walk(el);
|
|
390
|
+
return result.trim();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const directSelectors = [
|
|
394
|
+
// Yiyan specific: answer text container (highest priority)
|
|
395
|
+
'#answer_text_id',
|
|
396
|
+
'[id="answer_text_id"]',
|
|
397
|
+
// Generic selectors
|
|
398
|
+
'[class*="answer"]',
|
|
399
|
+
'[class*="response"]',
|
|
400
|
+
'[class*="message"][class*="content"]',
|
|
401
|
+
'.ds-markdown',
|
|
402
|
+
'[class*="assistant"] [class*="markdown"]',
|
|
403
|
+
'[class*="assistant"] [class*="content"]',
|
|
404
|
+
'[data-role="assistant"] [class*="content"]',
|
|
405
|
+
'[class*="ai-message"] [class*="content"]',
|
|
406
|
+
'[class*="bot-message"] [class*="content"]',
|
|
407
|
+
'[class*="response-content"]',
|
|
408
|
+
'[class*="message-content"]:last-child',
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
for (const sel of directSelectors) {
|
|
412
|
+
try {
|
|
413
|
+
const els = document.querySelectorAll(sel);
|
|
414
|
+
if (els.length > 0) {
|
|
415
|
+
const t = getFullText(els[els.length - 1]);
|
|
416
|
+
if (t.length > 10) return t;
|
|
417
|
+
}
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const markdownEls = document.querySelectorAll(
|
|
423
|
+
'[class*="markdown"], [class*="prose"], [class*="rendered"], [class*="content"]'
|
|
424
|
+
);
|
|
425
|
+
if (markdownEls.length > 0) {
|
|
426
|
+
const t = getFullText(markdownEls[markdownEls.length - 1]);
|
|
427
|
+
if (t.length > 10) return t;
|
|
428
|
+
}
|
|
429
|
+
} catch {}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const allBlocks = Array.from(
|
|
433
|
+
document.querySelectorAll('[class*="message"], [class*="chat-item"], [class*="turn"], [class*="answer"], [class*="content"]')
|
|
434
|
+
);
|
|
435
|
+
const candidates = allBlocks.filter(el => {
|
|
436
|
+
const cls = el.className || '';
|
|
437
|
+
const id = el.id || '';
|
|
438
|
+
return (
|
|
439
|
+
!cls.toLowerCase().includes('input') &&
|
|
440
|
+
!cls.toLowerCase().includes('user') &&
|
|
441
|
+
!cls.toLowerCase().includes('editable') &&
|
|
442
|
+
!id.toLowerCase().includes('input') &&
|
|
443
|
+
!el.querySelector('textarea, input[type="text"], [contenteditable="true"]') &&
|
|
444
|
+
(el.innerText || '').length > 20
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (candidates.length > 0) {
|
|
449
|
+
return getFullText(candidates[candidates.length - 1]);
|
|
450
|
+
}
|
|
451
|
+
} catch {}
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const allDivs = Array.from(document.querySelectorAll('div, section'));
|
|
455
|
+
const textBlocks = allDivs.filter(el => {
|
|
456
|
+
const text = el.innerText || '';
|
|
457
|
+
const cls = el.className || '';
|
|
458
|
+
if (cls.includes('input') || cls.includes('editable') || cls.includes('user')) return false;
|
|
459
|
+
return text.length > 50 && !el.querySelector('textarea, [contenteditable]');
|
|
460
|
+
});
|
|
461
|
+
if (textBlocks.length > 0) {
|
|
462
|
+
textBlocks.sort((a, b) => (b.innerText || '').length - (a.innerText || '').length);
|
|
463
|
+
return getFullText(textBlocks[0]);
|
|
464
|
+
}
|
|
465
|
+
} catch {}
|
|
466
|
+
|
|
467
|
+
return '';
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async _isGenerating() {
|
|
472
|
+
return await this.page.evaluate(() => {
|
|
473
|
+
const stopSelectors = [
|
|
474
|
+
'button[aria-label*="Stop" i]',
|
|
475
|
+
'[class*="stop-gen"]',
|
|
476
|
+
'[class*="stopGen"]',
|
|
477
|
+
'[class*="generating"]',
|
|
478
|
+
];
|
|
479
|
+
for (const sel of stopSelectors) {
|
|
480
|
+
const el = document.querySelector(sel);
|
|
481
|
+
if (el) {
|
|
482
|
+
const s = window.getComputedStyle(el);
|
|
483
|
+
if (s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0') return true;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const loaderSelectors = [
|
|
488
|
+
'[class*="typing"]',
|
|
489
|
+
'[class*="loading"]',
|
|
490
|
+
'[class*="spinner"]',
|
|
491
|
+
'[class*="blink"]',
|
|
492
|
+
'[class*="cursor"]',
|
|
493
|
+
'[class*="pulsing"]',
|
|
494
|
+
'svg[class*="loading"]',
|
|
495
|
+
'svg[class*="spinner"]',
|
|
496
|
+
];
|
|
497
|
+
for (const sel of loaderSelectors) {
|
|
498
|
+
const el = document.querySelector(sel);
|
|
499
|
+
if (el) {
|
|
500
|
+
const s = window.getComputedStyle(el);
|
|
501
|
+
if (s.display !== 'none' && s.visibility !== 'hidden') return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return false;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ── Text Cleanup ───────────────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
_cleanText(text) {
|
|
512
|
+
if (!text) return '';
|
|
513
|
+
|
|
514
|
+
// Remove everything before "准备输出结果" (Yiyan's thinking process)
|
|
515
|
+
const outputMarker = '准备输出结果';
|
|
516
|
+
const markerIndex = text.indexOf(outputMarker);
|
|
517
|
+
if (markerIndex !== -1) {
|
|
518
|
+
text = text.slice(markerIndex + outputMarker.length).trim();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Remove everything after regenerate/suggestion markers (Yiyan's UI elements)
|
|
522
|
+
const cutMarkers = ['重新生成', '重新生成的', '换个回答', '输出更详细的', '再多提供'];
|
|
523
|
+
for (const marker of cutMarkers) {
|
|
524
|
+
const cutIndex = text.indexOf(marker);
|
|
525
|
+
if (cutIndex !== -1) {
|
|
526
|
+
text = text.slice(0, cutIndex).trim();
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return text
|
|
532
|
+
// Strip any remaining AI thinking blocks
|
|
533
|
+
.replace(/<think>[\s\S]*?<\/think>\n?/gi, '')
|
|
534
|
+
// Strip copy-code button artifacts
|
|
535
|
+
.replace(/^\d+(?:Copy|Run|Insert|Edit)\b.*$/gm, '')
|
|
536
|
+
// Collapse multiple blank lines
|
|
537
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
538
|
+
.trim();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── Debug / Calibration Utilities ─────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
async dumpDebugInfo() {
|
|
544
|
+
const info = await this.page.evaluate(() => {
|
|
545
|
+
const classFreq = {};
|
|
546
|
+
document.querySelectorAll('*').forEach(el => {
|
|
547
|
+
el.classList.forEach(c => {
|
|
548
|
+
if (c.match(/message|chat|input|send|stop|markdown|content|assistant|user|bot/i)) {
|
|
549
|
+
classFreq[c] = (classFreq[c] || 0) + 1;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const inputs = Array.from(document.querySelectorAll('textarea, [contenteditable]')).map(e => ({
|
|
555
|
+
tag : e.tagName,
|
|
556
|
+
id : e.id || null,
|
|
557
|
+
class : e.className?.slice(0, 80) || null,
|
|
558
|
+
placeholder : e.placeholder || null,
|
|
559
|
+
editable : e.isContentEditable,
|
|
560
|
+
visible : e.offsetParent !== null,
|
|
561
|
+
}));
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
url : window.location.href,
|
|
565
|
+
title : document.title,
|
|
566
|
+
classes: Object.entries(classFreq).sort((a, b) => b[1] - a[1]).slice(0, 40),
|
|
567
|
+
inputs,
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
console.log('\n' + '═'.repeat(60));
|
|
572
|
+
console.log(' DOM DEBUG INFO');
|
|
573
|
+
console.log('═'.repeat(60));
|
|
574
|
+
console.log('URL :', info.url);
|
|
575
|
+
console.log('Title :', info.title);
|
|
576
|
+
console.log('\nInput elements:');
|
|
577
|
+
info.inputs.forEach(i => console.log(' ', JSON.stringify(i)));
|
|
578
|
+
console.log('\nMatching CSS classes (by frequency):');
|
|
579
|
+
info.classes.forEach(([cls, count]) => console.log(` ${String(count).padStart(3)}x .${cls}`));
|
|
580
|
+
console.log('═'.repeat(60) + '\n');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async screenshot(filePath = '/tmp/yiyan-agent-debug.png') {
|
|
584
|
+
await this.page.screenshot({ path: filePath, fullPage: false });
|
|
585
|
+
logger.info(`Screenshot saved: ${filePath}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = YiyanBrowser;
|