yiyan-browser-agent 1.1.3 → 1.3.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/dist/index.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/agent.ts
4
- import path3 from "path";
5
- import os2 from "os";
10
+ import path from "path";
11
+ import os from "os";
6
12
 
7
13
  // src/types.ts
8
14
  var YiyanAgentError = class extends Error {
@@ -21,214 +27,74 @@ var DEFAULT_OPTIONS = {
21
27
  };
22
28
  var YIYAN_CHAT_URL = "https://yiyan.baidu.com/";
23
29
 
24
- // src/profile.ts
25
- import path from "path";
26
- import fs from "fs";
27
- import fsp from "fs/promises";
28
- function getChromeExecutablePath(platform) {
29
- switch (platform) {
30
- case "win32":
31
- const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
32
- return path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe");
33
- case "darwin":
34
- return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
35
- case "linux":
36
- return "/usr/bin/google-chrome";
37
- default:
38
- return null;
39
- }
40
- }
41
- var PROFILE_TARGET_NAME = "chrome-profile";
42
- async function clearCopiedProfile(profileDir) {
43
- const targetPath = path.join(profileDir, PROFILE_TARGET_NAME);
44
- if (fs.existsSync(targetPath)) {
45
- await fsp.rm(targetPath, { recursive: true, force: true });
46
- }
47
- }
48
- function profileExists(profileDir) {
49
- const targetPath = path.join(profileDir, PROFILE_TARGET_NAME);
50
- return fs.existsSync(targetPath);
51
- }
52
-
53
30
  // src/browser.ts
54
- import { chromium } from "playwright-core";
55
- import http from "http";
56
- import { spawn, execSync } from "child_process";
57
- import path2 from "path";
58
- import os from "os";
59
- import fs2 from "fs";
60
- var CDP_PORT = 19222;
61
- var DEBUG_DIR = path2.join(os.homedir(), ".yiyan-browser-agent", "debug");
31
+ import { chromium } from "playwright";
32
+ import fs from "fs";
62
33
  function log(verbose, msg) {
63
34
  if (verbose) {
64
35
  process.stderr.write(`[yiyan-agent] ${msg}
65
36
  `);
66
37
  }
67
38
  }
68
- function ensureDebugDir() {
69
- if (!fs2.existsSync(DEBUG_DIR)) {
70
- fs2.mkdirSync(DEBUG_DIR, { recursive: true });
71
- }
72
- }
73
- function killProcessOnPort(port) {
74
- try {
75
- if (process.platform === "win32") {
76
- const output = execSync(
77
- `netstat -ano | findstr :${port} | findstr LISTENING`,
78
- { encoding: "utf-8", timeout: 5e3 }
79
- ).trim();
80
- if (output) {
81
- const lines = output.split("\n").filter((l) => l.trim());
82
- const pids = /* @__PURE__ */ new Set();
83
- for (const line of lines) {
84
- const parts = line.trim().split(/\s+/);
85
- const pid = parts[parts.length - 1];
86
- if (pid && /^\d+$/.test(pid)) pids.add(pid);
87
- }
88
- for (const pid of pids) {
89
- try {
90
- execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5e3 });
91
- } catch {
92
- }
93
- }
94
- }
95
- } else {
96
- const output = execSync(
97
- `lsof -ti :${port}`,
98
- { encoding: "utf-8", timeout: 5e3 }
99
- ).trim();
100
- if (output) {
101
- const pids = output.split("\n").filter((l) => l.trim());
102
- for (const pid of pids) {
103
- try {
104
- process.kill(parseInt(pid, 10), "SIGKILL");
105
- } catch {
106
- }
107
- }
108
- }
109
- }
110
- } catch {
111
- }
112
- }
113
- function killProcessTree(pid) {
114
- try {
115
- if (process.platform === "win32") {
116
- execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5e3 });
117
- } else {
118
- try {
119
- process.kill(-pid, "SIGKILL");
120
- } catch {
121
- process.kill(pid, "SIGKILL");
122
- }
123
- }
124
- } catch {
125
- }
126
- }
127
- function waitForCDP(port, timeout = 15e3) {
128
- return new Promise((resolve, reject) => {
129
- const start = Date.now();
130
- const check = () => {
131
- http.get(`http://localhost:${port}/json/version`, (res) => {
132
- let data = "";
133
- res.on("data", (chunk) => data += chunk);
134
- res.on("end", () => {
135
- try {
136
- resolve(JSON.parse(data));
137
- } catch (e) {
138
- reject(e);
139
- }
140
- });
141
- }).on("error", () => {
142
- if (Date.now() - start > timeout) reject(new Error("CDP connection timeout"));
143
- else setTimeout(check, 500);
144
- });
145
- };
146
- check();
147
- });
148
- }
149
39
  async function launchBrowser(options) {
150
- const { chromePath, profilePath, headless, timeout = 3e4, verbose = false } = options;
40
+ const { sessionDir, headless, timeout = 3e4, verbose = false } = options;
151
41
  try {
152
- log(verbose, "\u6E05\u7406\u6B8B\u7559 Chrome \u8FDB\u7A0B...");
153
- killProcessOnPort(CDP_PORT);
154
- await new Promise((resolve) => setTimeout(resolve, 500));
155
- const chromeArgs = [
156
- `--remote-debugging-port=${CDP_PORT}`,
157
- `--user-data-dir=${profilePath}`,
158
- "--no-first-run",
159
- "--no-default-browser-check",
160
- "--disable-blink-features=AutomationControlled"
161
- ];
162
- if (headless) {
163
- chromeArgs.push("--headless=new");
42
+ log(verbose, "\u542F\u52A8 Chromium \u6D4F\u89C8\u5668...");
43
+ if (!fs.existsSync(sessionDir)) {
44
+ fs.mkdirSync(sessionDir, { recursive: true });
164
45
  }
165
- log(verbose, `\u542F\u52A8 Chrome: ${chromePath} ${headless ? "(headless)" : "(headed)"}`);
166
- const chromeProcess = spawn(chromePath, chromeArgs, {
167
- detached: true,
168
- stdio: "ignore"
169
- });
170
- chromeProcess.unref();
171
- const spawnError = new Promise((_, reject) => {
172
- chromeProcess.on("error", (err) => {
173
- reject(new YiyanAgentError("BROWSER_LAUNCH", `Failed to spawn Chrome: ${err.message}`));
174
- });
46
+ const context = await chromium.launchPersistentContext(sessionDir, {
47
+ headless,
48
+ viewport: { width: 1280, height: 900 },
49
+ userAgent: [
50
+ "Mozilla/5.0 (X11; Linux x86_64)",
51
+ "AppleWebKit/537.36 (KHTML, like Gecko)",
52
+ "Chrome/124.0.0.0 Safari/537.36"
53
+ ].join(" "),
54
+ args: [
55
+ "--disable-blink-features=AutomationControlled",
56
+ "--no-first-run",
57
+ "--no-default-browser-check",
58
+ "--no-sandbox",
59
+ "--disable-setuid-sandbox",
60
+ "--disable-dev-shm-usage"
61
+ ],
62
+ ignoreDefaultArgs: ["--enable-automation"]
175
63
  });
176
- log(verbose, "\u7B49\u5F85 CDP \u7AEF\u53E3\u5C31\u7EEA...");
177
- await Promise.race([
178
- waitForCDP(CDP_PORT, timeout),
179
- spawnError
180
- ]);
181
- log(verbose, "\u901A\u8FC7 CDP \u8FDE\u63A5\u6D4F\u89C8\u5668...");
182
- const cdpBrowser = await chromium.connectOverCDP(`http://localhost:${CDP_PORT}`);
183
- const contexts = cdpBrowser.contexts();
184
- const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
185
64
  context.setDefaultTimeout(timeout);
186
65
  const pages = context.pages();
187
66
  const page = pages.length > 0 ? pages[0] : await context.newPage();
67
+ await page.addInitScript(() => {
68
+ Object.defineProperty(navigator, "webdriver", { get: () => false });
69
+ });
188
70
  log(verbose, "\u6D4F\u89C8\u5668\u542F\u52A8\u5B8C\u6210");
189
- return { browser: context, cdpBrowser, page, chromeProcess };
71
+ return { context, page };
190
72
  } catch (error) {
191
- killProcessOnPort(CDP_PORT);
192
73
  throw new YiyanAgentError(
193
74
  "BROWSER_LAUNCH",
194
75
  `Failed to launch browser: ${error instanceof Error ? error.message : String(error)}`
195
76
  );
196
77
  }
197
78
  }
198
- async function closeBrowser(browser, chromeProcess, cdpBrowser, verbose) {
79
+ async function closeBrowser(context, verbose) {
199
80
  log(!!verbose, "\u5173\u95ED\u6D4F\u89C8\u5668...");
200
- if (cdpBrowser) {
201
- try {
202
- await cdpBrowser.close();
203
- } catch {
204
- }
205
- }
206
81
  try {
207
- const pages = browser.pages();
208
- for (const page of pages) {
209
- await page.close().catch(() => {
210
- });
211
- }
82
+ await context.close();
83
+ log(!!verbose, "\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED");
212
84
  } catch {
213
85
  }
214
- if (chromeProcess && chromeProcess.pid) {
215
- killProcessTree(chromeProcess.pid);
216
- }
217
- killProcessOnPort(CDP_PORT);
218
- log(!!verbose, "\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED");
219
86
  }
220
87
  async function navigateToYiyan(page, verbose = false) {
221
88
  log(verbose, "\u5BFC\u822A\u5230\u6587\u5FC3\u4E00\u8A00\u804A\u5929\u9875\u9762...");
222
- await page.goto(YIYAN_CHAT_URL, {
223
- waitUntil: "networkidle",
224
- timeout: 6e4
225
- });
226
- await page.waitForTimeout(5e3);
227
89
  try {
228
- await page.waitForSelector('[contenteditable="true"]', { timeout: 15e3 });
229
- log(verbose, "\u9875\u9762\u52A0\u8F7D\u5B8C\u6210\uFF0C\u8F93\u5165\u6846\u5DF2\u5C31\u7EEA");
230
- } catch {
231
- log(verbose, "\u8B66\u544A\uFF1A\u672A\u68C0\u6D4B\u5230\u8F93\u5165\u6846\uFF0C\u7EE7\u7EED\u6267\u884C");
90
+ await page.goto(YIYAN_CHAT_URL, {
91
+ waitUntil: "domcontentloaded",
92
+ timeout: 3e4
93
+ });
94
+ await page.waitForTimeout(1500);
95
+ log(verbose, "\u9875\u9762\u52A0\u8F7D\u5B8C\u6210");
96
+ } catch (err) {
97
+ log(verbose, `\u5BFC\u822A\u8B66\u544A: ${err instanceof Error ? err.message : String(err)}`);
232
98
  }
233
99
  }
234
100
  async function checkCaptcha(page, verbose = false) {
@@ -284,7 +150,7 @@ async function waitForUserAction(page, reason, verbose = false) {
284
150
  const maxWait = 18e4;
285
151
  const startTime = Date.now();
286
152
  while (Date.now() - startTime < maxWait) {
287
- await new Promise((resolve) => setTimeout(resolve, 2e3));
153
+ await page.waitForTimeout(2e3);
288
154
  if (reason === "captcha") {
289
155
  const stillHasCaptcha = await checkCaptcha(page, false);
290
156
  if (!stillHasCaptcha) {
@@ -313,24 +179,38 @@ async function waitForUserAction(page, reason, verbose = false) {
313
179
  log(verbose, "\u26A0 \u7B49\u5F85\u7528\u6237\u64CD\u4F5C\u8D85\u65F6\uFF083\u5206\u949F\uFF09");
314
180
  }
315
181
  async function sendMessage(page, message, verbose = false, headful = false) {
316
- const inputSelector = '[contenteditable="true"]';
182
+ const inputSelectors = [
183
+ '[contenteditable="true"][role="textbox"]',
184
+ '[contenteditable="true"]',
185
+ "textarea[placeholder]",
186
+ "textarea"
187
+ ];
317
188
  log(verbose, "\u7B49\u5F85\u8F93\u5165\u6846\u51FA\u73B0...");
318
- const inputElement = await page.waitForSelector(inputSelector, { timeout: 15e3 });
189
+ let inputElement = null;
190
+ for (const sel of inputSelectors) {
191
+ try {
192
+ inputElement = await page.waitForSelector(sel, { timeout: 4e3, state: "visible" });
193
+ if (inputElement) break;
194
+ } catch {
195
+ }
196
+ }
319
197
  if (!inputElement) {
320
- log(verbose, "\u26A0 \u8F93\u5165\u6846\u672A\u627E\u5230\uFF01");
321
- throw new YiyanAgentError("NETWORK", "Input element not found on page");
198
+ throw new YiyanAgentError("NETWORK", "Cannot find the Yiyan chat input box");
322
199
  }
323
200
  log(verbose, "\u2713 \u8F93\u5165\u6846\u5DF2\u627E\u5230");
324
- await inputElement.click();
325
- await page.waitForTimeout(500);
201
+ await inputElement.click({ force: true });
202
+ await page.waitForTimeout(200);
203
+ await page.keyboard.press("Control+a");
204
+ await page.waitForTimeout(100);
205
+ await page.evaluate((element, content) => {
206
+ element.focus();
207
+ document.execCommand("selectAll", false, null);
208
+ document.execCommand("delete", false, null);
209
+ document.execCommand("insertText", false, content);
210
+ element.dispatchEvent(new InputEvent("input", { bubbles: true, data: content }));
211
+ }, inputElement, message);
326
212
  log(verbose, `\u8F93\u5165\u95EE\u9898: "${message}"`);
327
- await page.keyboard.type(message, { delay: 30 });
328
- const filledValue = await inputElement.innerText();
329
- if (filledValue.includes(message)) {
330
- log(verbose, "\u2713 \u95EE\u9898\u5DF2\u6210\u529F\u586B\u5165\u8F93\u5165\u6846");
331
- } else {
332
- log(verbose, `\u26A0 \u8F93\u5165\u6846\u5185\u5BB9: "${filledValue.substring(0, 50)}"`);
333
- }
213
+ await page.waitForTimeout(400);
334
214
  log(verbose, "\u6309 Enter \u53D1\u9001\u6D88\u606F...");
335
215
  await page.keyboard.press("Enter");
336
216
  await page.waitForTimeout(3e3);
@@ -338,47 +218,32 @@ async function sendMessage(page, message, verbose = false, headful = false) {
338
218
  if (headful) {
339
219
  await waitForUserAction(page, "captcha", verbose);
340
220
  log(verbose, "\u9A8C\u8BC1\u7801\u5DF2\u901A\u8FC7\uFF0C\u91CD\u65B0\u53D1\u9001\u6D88\u606F...");
341
- const inputEl = await page.waitForSelector(inputSelector, { timeout: 1e4 });
342
- if (inputEl) {
343
- await inputEl.click();
344
- await page.waitForTimeout(300);
345
- await page.keyboard.type(message, { delay: 30 });
346
- await page.keyboard.press("Enter");
347
- await page.waitForTimeout(3e3);
221
+ for (const sel of inputSelectors) {
222
+ try {
223
+ const inputEl = await page.waitForSelector(sel, { timeout: 1e4 });
224
+ if (inputEl) {
225
+ await inputEl.click({ force: true });
226
+ await page.waitForTimeout(300);
227
+ await page.evaluate((element, content) => {
228
+ element.focus();
229
+ document.execCommand("selectAll", false, null);
230
+ document.execCommand("delete", false, null);
231
+ document.execCommand("insertText", false, content);
232
+ }, inputEl, message);
233
+ await page.keyboard.press("Enter");
234
+ await page.waitForTimeout(3e3);
235
+ break;
236
+ }
237
+ } catch {
238
+ }
348
239
  }
349
240
  } else {
350
241
  throw new YiyanAgentError(
351
242
  "CAPTCHA",
352
- 'Yiyan detected automation and triggered a captcha. Use headed mode (default) to manually solve it, or run "login" first.'
243
+ "Yiyan detected automation and triggered a captcha. Use headed mode to manually solve it."
353
244
  );
354
245
  }
355
246
  }
356
- const url = page.url();
357
- log(verbose, `\u5F53\u524D URL: ${url}`);
358
- if (url.includes("/chat/")) {
359
- log(verbose, "\u2713 \u5DF2\u81EA\u52A8\u8FDB\u5165\u5BF9\u8BDD\u9875\u9762");
360
- } else if (url === "https://yiyan.baidu.com/" || url === "https://yiyan.baidu.com") {
361
- log(verbose, "\u4ECD\u5728\u6B22\u8FCE\u9875\uFF0C\u5C1D\u8BD5\u70B9\u51FB\u53D1\u9001\u6309\u94AE...");
362
- try {
363
- const sendBtn = page.locator('[class*="send__"]').first();
364
- await sendBtn.click({ timeout: 5e3 });
365
- await page.waitForTimeout(3e3);
366
- log(verbose, "\u2713 \u5DF2\u70B9\u51FB\u53D1\u9001\u6309\u94AE");
367
- } catch {
368
- log(verbose, "\u26A0 \u70B9\u51FB\u53D1\u9001\u6309\u94AE\u5931\u8D25\uFF0C\u7EE7\u7EED\u7B49\u5F85\u56DE\u590D...");
369
- }
370
- } else {
371
- log(verbose, `\u5F53\u524D\u9875\u9762: ${url}`);
372
- }
373
- }
374
- async function saveDebugScreenshot(page, name, verbose) {
375
- try {
376
- ensureDebugDir();
377
- const screenshotPath = path2.join(DEBUG_DIR, `${name}.png`);
378
- await page.screenshot({ path: screenshotPath, fullPage: true });
379
- log(verbose, `\u8C03\u8BD5\u622A\u56FE\u5DF2\u4FDD\u5B58: ${screenshotPath}`);
380
- } catch {
381
- }
382
247
  }
383
248
  async function waitForReply(page, timeout, verbose = false, headful = false) {
384
249
  const maxWait = Math.min(timeout, 6e4);
@@ -414,13 +279,13 @@ async function waitForReply(page, timeout, verbose = false, headful = false) {
414
279
  });
415
280
  if (state.hasAiReply && state.aiTextLen > 0) {
416
281
  if (!replyStarted) {
417
- log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5F00\u59CB\u751F\u6210\uFF08${state.aiTextLen}\u5B57\uFF09: ${state.aiPreview.substring(0, 60)}...`);
282
+ log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5F00\u59CB\u751F\u6210\uFF08${state.aiTextLen}\u5B57\uFF09`);
418
283
  replyStarted = true;
419
284
  }
420
285
  if (state.aiTextLen === lastLen) {
421
286
  stableCount++;
422
287
  if (stableCount >= 3) {
423
- log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5B8C\u6210\uFF08${state.aiTextLen}\u5B57\uFF0C\u8FDE\u7EED ${stableCount} \u6B21\u957F\u5EA6\u4E0D\u53D8\uFF09`);
288
+ log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5B8C\u6210\uFF08${state.aiTextLen}\u5B57\uFF09`);
424
289
  break;
425
290
  }
426
291
  } else {
@@ -428,134 +293,31 @@ async function waitForReply(page, timeout, verbose = false, headful = false) {
428
293
  lastLen = state.aiTextLen;
429
294
  }
430
295
  }
431
- await page.waitForTimeout(1500);
296
+ await page.waitForTimeout(500);
432
297
  }
433
298
  if (!replyStarted) {
434
299
  if (await checkCaptcha(page, verbose)) {
435
300
  if (headful) {
436
301
  await waitForUserAction(page, "captcha", verbose);
437
- const retryStart = Date.now();
438
- const retryMaxWait = 3e4;
439
- while (Date.now() - retryStart < retryMaxWait) {
440
- const retryState = await page.evaluate(() => {
441
- const answerBox = document.querySelector('[class*="answerBox"]');
442
- if (answerBox) {
443
- const text = answerBox.innerText?.trim() || "";
444
- return { hasAiReply: text.length > 0, aiTextLen: text.length };
445
- }
446
- return { hasAiReply: false, aiTextLen: 0 };
447
- });
448
- if (retryState.hasAiReply) {
449
- log(verbose, "\u2713 \u9A8C\u8BC1\u7801\u901A\u8FC7\u540E AI \u5DF2\u5F00\u59CB\u56DE\u590D");
450
- replyStarted = true;
451
- break;
452
- }
453
- await page.waitForTimeout(1500);
454
- }
455
302
  } else {
456
303
  throw new YiyanAgentError(
457
304
  "CAPTCHA",
458
- 'Yiyan detected automation and triggered a captcha. Use --headful flag to manually solve it, or run "login" first.'
305
+ "Yiyan detected automation and triggered a captcha."
459
306
  );
460
307
  }
461
308
  } else if (headful) {
462
309
  await waitForUserAction(page, "no-reply", verbose);
463
- const retryStart = Date.now();
464
- const retryMaxWait = 6e4;
465
- while (Date.now() - retryStart < retryMaxWait) {
466
- const retryState = await page.evaluate(() => {
467
- const answerBox = document.querySelector('[class*="answerBox"]');
468
- if (answerBox) {
469
- const text = answerBox.innerText?.trim() || "";
470
- return { hasAiReply: text.length > 0, aiTextLen: text.length };
471
- }
472
- return { hasAiReply: false, aiTextLen: 0 };
473
- });
474
- if (retryState.hasAiReply) {
475
- if (retryState.aiTextLen === lastLen) {
476
- stableCount++;
477
- if (stableCount >= 3) {
478
- log(verbose, `\u2713 \u7528\u6237\u64CD\u4F5C\u540E AI \u56DE\u590D\u5DF2\u5B8C\u6210\uFF08${retryState.aiTextLen}\u5B57\uFF09`);
479
- replyStarted = true;
480
- break;
481
- }
482
- } else {
483
- stableCount = 0;
484
- lastLen = retryState.aiTextLen;
485
- }
486
- }
487
- await page.waitForTimeout(1500);
488
- }
489
310
  } else {
490
311
  throw new YiyanAgentError(
491
312
  "TIMEOUT",
492
- 'AI reply timeout in headless mode. Try running "yiyan-agent login" first, or use --headful flag.'
313
+ "AI reply timeout in headless mode. Try running login first, or use headed mode."
493
314
  );
494
315
  }
495
316
  }
496
- await page.waitForTimeout(2e3);
497
- const pageDump = await page.evaluate(() => {
498
- const result = {};
499
- const selectors = [
500
- '[class*="answerBox"]',
501
- '[class*="dialogueCardList"]',
502
- '[class*="dialogue_card_item"]',
503
- '[class*="chatViewer"]',
504
- '[class*="flowBox"]',
505
- '[class*="roleSystem"]',
506
- '[class*="mdRenderContainer"]',
507
- '[class*="agent-markdown"]',
508
- '[class*="markdown"]',
509
- '[class*="content"]',
510
- '[class*="chat"]',
511
- '[class*="message"]'
512
- ];
513
- for (const sel of selectors) {
514
- result[`selector:${sel}`] = document.querySelectorAll(sel).length;
515
- }
516
- const answerBox = document.querySelector('[class*="answerBox"]');
517
- if (answerBox) {
518
- result["answerBox"] = {
519
- textLen: answerBox.innerText?.trim().length || 0,
520
- preview: (answerBox.innerText?.trim() || "").substring(0, 200),
521
- classes: answerBox.className?.toString().substring(0, 100) || ""
522
- };
523
- }
524
- result["bodyText"] = document.body.innerText?.substring(0, 1e3) || "";
525
- const longTextEls = [];
526
- const mainContent = document.querySelector('[class*="chatViewer"]') || document.querySelector('[class*="chat"]') || document.body;
527
- for (const el of Array.from(mainContent.querySelectorAll("div, section, article"))) {
528
- const text = el.innerText?.trim() || "";
529
- if (text.length > 50 && text.length < 1e4) {
530
- longTextEls.push({
531
- tag: el.tagName,
532
- classes: el.className?.toString().split(" ").slice(0, 3).join(" ") || "",
533
- textLen: text.length,
534
- preview: text.substring(0, 80)
535
- });
536
- }
537
- }
538
- result["longTextElements"] = longTextEls.sort((a, b) => b.textLen - a.textLen).slice(0, 10);
539
- return result;
540
- });
541
- log(verbose, "=== \u9875\u9762\u8BCA\u65AD ===");
542
- for (const [key, value] of Object.entries(pageDump)) {
543
- if (key === "bodyText" || key === "longTextElements") continue;
544
- log(verbose, ` ${key}: ${JSON.stringify(value)}`);
545
- }
546
- if (pageDump["longTextElements"] && pageDump["longTextElements"].length > 0) {
547
- log(verbose, " \u957F\u6587\u672C\u5143\u7D20 TOP 10:");
548
- for (const el of pageDump["longTextElements"]) {
549
- log(verbose, ` <${el.tag}> .${el.classes} (${el.textLen}\u5B57): ${el.preview}...`);
550
- }
551
- }
552
- if (pageDump["bodyText"]) {
553
- log(verbose, ` \u9875\u9762\u6587\u672C(\u524D1000\u5B57): ${pageDump["bodyText"].substring(0, 1e3)}`);
554
- }
317
+ await page.waitForTimeout(1e3);
555
318
  }
556
319
  async function extractReply(page, question, verbose = false) {
557
320
  log(verbose, "\u63D0\u53D6 AI \u56DE\u590D\u5185\u5BB9...");
558
- await saveDebugScreenshot(page, "before-extract", verbose);
559
321
  const reply = await page.evaluate((userQuestion) => {
560
322
  const debugInfo = [];
561
323
  const answerBox = document.querySelector('[class*="answerBox"]');
@@ -563,19 +325,12 @@ async function extractReply(page, question, verbose = false) {
563
325
  if (answerBox) {
564
326
  const clone = answerBox.cloneNode(true);
565
327
  const processItems = clone.querySelectorAll('[class*="processItem"], [class*="processContent"]');
566
- for (const item of processItems) {
567
- item.remove();
568
- }
328
+ for (const item of processItems) item.remove();
569
329
  const toolMessages = clone.querySelectorAll('[class*="toolMessage"]');
570
- for (const msg of toolMessages) {
571
- msg.remove();
572
- }
330
+ for (const msg of toolMessages) msg.remove();
573
331
  const thinkHeaders = clone.querySelectorAll('[class*="headerMask"], [class*="topHeader"]');
574
- for (const h of thinkHeaders) {
575
- h.remove();
576
- }
332
+ for (const h of thinkHeaders) h.remove();
577
333
  const text = clone.innerText?.trim() || "";
578
- debugInfo.push(`answerBox after removing thinking: ${text.length} chars`);
579
334
  const cleanedText = text.replace(/^参考\d+个网页\s*/, "").replace(/^深度思考已完成\s*/, "").replace(/^思考完成[::]\s*/, "").replace(/^准备输出结果\s*/, "").trim();
580
335
  if (cleanedText.length > 0) {
581
336
  debugInfo.push(`method1 success: ${cleanedText.length} chars`);
@@ -587,12 +342,10 @@ async function extractReply(page, question, verbose = false) {
587
342
  for (const container of allMdContainers) {
588
343
  const parent = container.parentElement;
589
344
  const parentClass = parent?.className?.toString() || "";
590
- if (parentClass.includes("toolMessage") || parentClass.includes("process")) {
591
- continue;
592
- }
345
+ if (parentClass.includes("toolMessage") || parentClass.includes("process")) continue;
593
346
  const text = container.innerText?.trim() || "";
594
347
  if (text.length > 10) {
595
- debugInfo.push(`method2 success: ${text.length} chars (parent: ${parentClass.substring(0, 50)})`);
348
+ debugInfo.push(`method2 success: ${text.length} chars`);
596
349
  return JSON.stringify({ text, debug: debugInfo });
597
350
  }
598
351
  }
@@ -611,21 +364,6 @@ async function extractReply(page, question, verbose = false) {
611
364
  }
612
365
  }
613
366
  }
614
- const flowBoxes = document.querySelectorAll('[class*="flowBox"]');
615
- debugInfo.push(`flowBoxes count: ${flowBoxes.length}`);
616
- for (let i = flowBoxes.length - 1; i >= 0; i--) {
617
- const box = flowBoxes[i];
618
- const boxClass = box.className?.toString() || "";
619
- if (boxClass.includes("questionBox")) continue;
620
- const clone = box.cloneNode(true);
621
- const thinkEls = clone.querySelectorAll('[class*="processItem"], [class*="processContent"], [class*="toolMessage"]');
622
- for (const el of thinkEls) el.remove();
623
- const text = clone.innerText?.trim() || "";
624
- if (text.length > 10) {
625
- debugInfo.push(`method4 success: flowBox[${i}] ${text.length} chars`);
626
- return JSON.stringify({ text, debug: debugInfo });
627
- }
628
- }
629
367
  const fullText = document.body.innerText;
630
368
  const lines = fullText.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
631
369
  const uiWords = /* @__PURE__ */ new Set([
@@ -642,27 +380,15 @@ async function extractReply(page, question, verbose = false) {
642
380
  "\u672A\u767B\u5F55",
643
381
  "\u767B\u5F55",
644
382
  "\u5185\u5BB9\u7531AI\u751F\u6210\uFF0C\u4EC5\u4F9B\u53C2\u8003\uFF0C\u8BF7\u4ED4\u7EC6\u7504\u522B",
645
- "\u53C2\u8003",
646
- "\u6DF1\u5EA6\u5206\u6790\u9700\u6C42\u5E76\u89E3\u7B54",
647
- "\u901A\u7528\u5199\u4F5C",
648
- "\u9605\u8BFB\u5206\u6790",
649
- "\u7F51\u9875\u5DE5\u574A",
650
- "\u667A\u80FD\u7FFB\u8BD1",
651
- "\u4EE3\u7801\u7F16\u7A0B"
383
+ "\u53C2\u8003"
652
384
  ]);
653
385
  const thinkingKeywords = [
654
386
  "\u6DF1\u5EA6\u601D\u8003\u5DF2\u5B8C\u6210",
655
387
  "\u601D\u8003\u5B8C\u6210",
656
- "\u51C6\u5907\u8F93\u51FA\u7ED3\u679C",
657
- "picaole\u5728\u8BE2\u95EE",
658
- "picaole\u95EE\u4E86\u4E00\u4E2A",
659
- "picaole\u5728\u641C\u7D22",
660
- "\u76F4\u63A5\u56DE\u7B54\u5373\u53EF",
661
- "\u4E0D\u9700\u8981\u590D\u6742\u5206\u6790",
662
- "\u6309\u7167\u56DE\u7B54\u65B9\u5F0F"
388
+ "\u51C6\u5907\u8F93\u51FA\u7ED3\u679C"
663
389
  ];
664
390
  const userQIdx = lines.findIndex((l) => l === userQuestion || l.includes(userQuestion));
665
- debugInfo.push(`userQ idx in body text: ${userQIdx}, total lines: ${lines.length}`);
391
+ debugInfo.push(`userQ idx: ${userQIdx}, lines: ${lines.length}`);
666
392
  if (userQIdx >= 0) {
667
393
  const replyLines = [];
668
394
  for (let i = userQIdx + 1; i < lines.length; i++) {
@@ -671,34 +397,15 @@ async function extractReply(page, question, verbose = false) {
671
397
  if (thinkingKeywords.some((kw) => line.includes(kw))) continue;
672
398
  if (line.startsWith("\u641C\u7D22") || line.includes("\u7BC7\u8D44\u6599")) continue;
673
399
  if (line.match(/^参考\d+个网页/)) continue;
674
- if (line.length > 0) {
675
- replyLines.push(line);
676
- }
400
+ if (line.length > 0) replyLines.push(line);
677
401
  if (line === "\u5FEB\u901F" || line === "\u66F4\u591A" || line.includes("\u5185\u5BB9\u7531AI\u751F\u6210")) break;
678
402
  }
679
403
  if (replyLines.length > 0) {
680
- debugInfo.push(`method5 success: ${replyLines.length} lines`);
404
+ debugInfo.push(`method4 success: ${replyLines.length} lines`);
681
405
  return JSON.stringify({ text: replyLines.join("\n"), debug: debugInfo });
682
406
  }
683
407
  }
684
- const allTextBlocks = [];
685
- const blockElements = document.querySelectorAll("div, section, article, p");
686
- for (const el of blockElements) {
687
- const elText = el.innerText?.trim() || "";
688
- if (elText.length > 50 && elText.length < 1e4) {
689
- if (uiWords.has(elText)) continue;
690
- if (thinkingKeywords.some((kw) => elText.includes(kw))) continue;
691
- allTextBlocks.push({ text: elText, length: elText.length });
692
- }
693
- }
694
- allTextBlocks.sort((a, b) => b.length - a.length);
695
- if (allTextBlocks.length > 0) {
696
- debugInfo.push(`method6 fallback: longest text block ${allTextBlocks[0].length} chars`);
697
- return JSON.stringify({ text: allTextBlocks[0].text, debug: debugInfo });
698
- }
699
- debugInfo.push(`ALL METHODS FAILED`);
700
- debugInfo.push(`body text length: ${fullText.length}`);
701
- debugInfo.push(`body text first 500: ${fullText.substring(0, 500)}`);
408
+ debugInfo.push("ALL METHODS FAILED");
702
409
  return JSON.stringify({ text: "", debug: debugInfo });
703
410
  }, question);
704
411
  let parsed;
@@ -714,68 +421,39 @@ async function extractReply(page, question, verbose = false) {
714
421
  log(verbose, `\u63D0\u53D6\u6210\u529F\uFF0C\u56DE\u590D\u957F\u5EA6: ${parsed.text.length} \u5B57\u7B26`);
715
422
  return parsed.text;
716
423
  }
717
- log(verbose, "\u63D0\u53D6\u5931\u8D25\uFF0C\u6240\u6709\u65B9\u6CD5\u5747\u672A\u627E\u5230\u56DE\u590D\u5185\u5BB9");
718
424
  throw new YiyanAgentError("TIMEOUT", "Failed to extract reply content");
719
425
  }
720
426
 
721
427
  // src/agent.ts
722
- var DEFAULT_PROFILE_BASE_DIR = path3.join(
723
- os2.homedir(),
724
- ".yiyan-browser-agent"
725
- );
428
+ var DEFAULT_SESSION_DIR = path.join(os.homedir(), ".yiyan-browser-agent", "session");
726
429
  var YiyanAgent = class {
727
430
  options;
728
- profileDir;
431
+ sessionDir;
729
432
  verbose;
730
433
  constructor(options) {
731
434
  this.options = {
732
435
  ...DEFAULT_OPTIONS,
733
436
  ...options
734
437
  };
735
- this.profileDir = this.options.profileDir || DEFAULT_PROFILE_BASE_DIR;
438
+ this.sessionDir = this.options.profileDir || DEFAULT_SESSION_DIR;
736
439
  this.verbose = options?.verbose ?? false;
737
440
  }
738
441
  /**
739
442
  * 发送问题并获取答案
740
443
  * @param question 问题文本
741
- * @param headful 是否使用有头浏览器(可见窗口),默认为 false(优先 headless,登录后无需弹窗)
742
- *
743
- * 策略:先尝试 headless(不弹窗),如果失败(验证码/未登录)自动回退到有头模式
444
+ * @param headful 是否使用有头浏览器(可见窗口),默认为 falseheadless
744
445
  */
745
446
  async ask(question, headful = false) {
746
- if (headful) {
747
- return this.executeAsk(question, true);
748
- }
749
- try {
750
- return await this.executeAsk(question, false);
751
- } catch (error) {
752
- if (error instanceof YiyanAgentError && (error.type === "CAPTCHA" || error.type === "TIMEOUT")) {
753
- if (this.verbose) {
754
- process.stderr.write("[yiyan-agent] headless \u6A21\u5F0F\u5931\u8D25\uFF0C\u81EA\u52A8\u5207\u6362\u5230\u6709\u5934\u6A21\u5F0F...\n");
755
- }
756
- return this.executeAsk(question, true);
757
- }
758
- throw error;
759
- }
447
+ return this.executeAsk(question, headful);
760
448
  }
761
449
  /**
762
450
  * 执行单次问答
763
451
  */
764
452
  async executeAsk(question, headful) {
765
- const platform = os2.platform();
766
- const chromePath = this.options.chromePath || getChromeExecutablePath(platform);
767
- if (!chromePath) {
768
- throw new YiyanAgentError(
769
- "BROWSER_LAUNCH",
770
- `Unsupported platform: ${platform}`
771
- );
772
- }
773
- const profilePath = path3.join(this.profileDir, "chrome-profile");
774
- const { browser, cdpBrowser, page, chromeProcess } = await launchBrowser({
775
- chromePath,
776
- profilePath,
453
+ const { context, page } = await launchBrowser({
454
+ sessionDir: this.sessionDir,
777
455
  headless: !headful,
778
- timeout: 3e4,
456
+ timeout: this.options.timeout,
779
457
  verbose: this.verbose
780
458
  });
781
459
  try {
@@ -796,7 +474,7 @@ var YiyanAgent = class {
796
474
  const reply = await extractReply(page, question, this.verbose);
797
475
  return reply;
798
476
  } finally {
799
- await closeBrowser(browser, chromeProcess, cdpBrowser, this.verbose);
477
+ await closeBrowser(context, this.verbose);
800
478
  }
801
479
  }
802
480
  /**
@@ -804,66 +482,96 @@ var YiyanAgent = class {
804
482
  * 登录成功后,后续的 ask 调用将自动使用登录状态
805
483
  */
806
484
  async login() {
807
- const platform = os2.platform();
808
- const chromePath = this.options.chromePath || getChromeExecutablePath(platform);
809
- if (!chromePath) {
810
- throw new YiyanAgentError(
811
- "BROWSER_LAUNCH",
812
- `Unsupported platform: ${platform}`
813
- );
814
- }
815
- const profilePath = path3.join(this.profileDir, "chrome-profile");
816
- const { browser, cdpBrowser, page, chromeProcess } = await launchBrowser({
817
- chromePath,
818
- profilePath,
485
+ const { context, page } = await launchBrowser({
486
+ sessionDir: this.sessionDir,
819
487
  headless: false,
820
488
  timeout: 6e4,
821
489
  verbose: this.verbose
822
490
  });
823
491
  try {
824
- await page.goto("https://yiyan.baidu.com/", {
825
- waitUntil: "networkidle",
826
- timeout: 6e4
827
- });
828
- await page.waitForTimeout(5e3);
829
- process.stderr.write("\n[yiyan-agent] ============================================\n");
830
- process.stderr.write("[yiyan-agent] \u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u767B\u5F55\u6587\u5FC3\u4E00\u8A00\n");
831
- process.stderr.write("[yiyan-agent] \u767B\u5F55\u6210\u529F\u540E\uFF0C\u56DE\u5230\u6B64\u5904\u6309 Enter \u952E\u7EE7\u7EED\n");
832
- process.stderr.write("[yiyan-agent] ============================================\n\n");
833
- const readline = await import("readline");
834
- await new Promise((resolve) => {
835
- const rl = readline.createInterface({
836
- input: process.stdin,
837
- output: process.stderr
838
- });
839
- rl.question("\u6309 Enter \u7EE7\u7EED...", () => {
840
- rl.close();
841
- resolve();
842
- });
843
- });
844
- process.stderr.write("[yiyan-agent] \u2713 \u767B\u5F55\u5B8C\u6210\uFF0C\u6B63\u5728\u4FDD\u5B58\u767B\u5F55\u72B6\u6001...\n");
492
+ await navigateToYiyan(page, this.verbose);
493
+ process.stderr.write("\n[yiyan-agent] \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n");
494
+ process.stderr.write("[yiyan-agent] \u2551 \u{1F510} LOGIN REQUIRED \u2551\n");
495
+ process.stderr.write("[yiyan-agent] \u2551 \u2551\n");
496
+ process.stderr.write("[yiyan-agent] \u2551 1. Log in to Yiyan in the browser window \u2551\n");
497
+ process.stderr.write("[yiyan-agent] \u2551 2. Return here and press ENTER to continue \u2551\n");
498
+ process.stderr.write("[yiyan-agent] \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n\n");
499
+ await this.waitForEnter();
500
+ process.stderr.write("[yiyan-agent] \u2713 Login complete, saving session...\n");
845
501
  await new Promise((resolve) => setTimeout(resolve, 3e3));
846
502
  } finally {
847
- await closeBrowser(browser, chromeProcess, cdpBrowser, this.verbose);
503
+ await closeBrowser(context, this.verbose);
848
504
  }
849
505
  }
850
- sleep(ms) {
851
- return new Promise((resolve) => setTimeout(resolve, ms));
506
+ /**
507
+ * 等待用户按 Enter
508
+ */
509
+ async waitForEnter() {
510
+ const readline = await import("readline");
511
+ return new Promise((resolve) => {
512
+ const rl = readline.createInterface({
513
+ input: process.stdin,
514
+ output: process.stderr
515
+ });
516
+ rl.question("Press ENTER to continue...", () => {
517
+ rl.close();
518
+ resolve();
519
+ });
520
+ });
852
521
  }
522
+ /**
523
+ * 清除保存的 session
524
+ */
853
525
  async reset() {
854
- await clearCopiedProfile(this.profileDir);
526
+ const fs3 = await import("fs/promises");
527
+ try {
528
+ await fs3.rm(this.sessionDir, { recursive: true, force: true });
529
+ } catch {
530
+ }
855
531
  }
532
+ /**
533
+ * 检查状态
534
+ */
856
535
  status() {
536
+ const fs3 = __require("fs");
857
537
  return {
858
- loggedIn: profileExists(this.profileDir),
859
- profilePath: path3.join(this.profileDir, "chrome-profile")
538
+ loggedIn: fs3.existsSync(this.sessionDir),
539
+ sessionPath: this.sessionDir
860
540
  };
861
541
  }
862
542
  };
543
+
544
+ // src/profile.ts
545
+ import path2 from "path";
546
+ import os2 from "os";
547
+ import fs2 from "fs";
548
+ var CONFIG_FILE_PATH = path2.join(os2.homedir(), ".yiyan-browser-agent", "config.json");
549
+ var DEFAULT_SESSION_DIR2 = path2.join(os2.homedir(), ".yiyan-browser-agent", "session");
550
+ function readConfig() {
551
+ try {
552
+ if (fs2.existsSync(CONFIG_FILE_PATH)) {
553
+ const content = fs2.readFileSync(CONFIG_FILE_PATH, "utf-8");
554
+ return JSON.parse(content);
555
+ }
556
+ } catch {
557
+ }
558
+ return {};
559
+ }
560
+ function saveConfig(config) {
561
+ const configDir = path2.dirname(CONFIG_FILE_PATH);
562
+ if (!fs2.existsSync(configDir)) {
563
+ fs2.mkdirSync(configDir, { recursive: true });
564
+ }
565
+ fs2.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2));
566
+ }
863
567
  export {
568
+ CONFIG_FILE_PATH,
864
569
  DEFAULT_OPTIONS,
570
+ DEFAULT_SESSION_DIR2 as DEFAULT_SESSION_DIR,
865
571
  YIYAN_CHAT_URL,
866
572
  YiyanAgent,
867
- YiyanAgentError
573
+ YiyanAgentError,
574
+ readConfig,
575
+ saveConfig
868
576
  };
869
577
  //# sourceMappingURL=index.js.map