yiyan-browser-agent 1.2.0 → 1.3.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.
- package/dist/cli.d.ts +1 -3
- package/dist/cli.js +171 -631
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +17 -21
- package/dist/index.js +183 -568
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.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
|
|
5
|
-
import
|
|
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,279 +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 os from "os";
|
|
27
|
-
import fs from "fs";
|
|
28
|
-
import fsp from "fs/promises";
|
|
29
|
-
var CONFIG_FILE_PATH = path.join(os.homedir(), ".yiyan-browser-agent", "config.json");
|
|
30
|
-
function readConfig() {
|
|
31
|
-
try {
|
|
32
|
-
if (fs.existsSync(CONFIG_FILE_PATH)) {
|
|
33
|
-
const content = fs.readFileSync(CONFIG_FILE_PATH, "utf-8");
|
|
34
|
-
return JSON.parse(content);
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
function saveConfig(config) {
|
|
41
|
-
const configDir = path.dirname(CONFIG_FILE_PATH);
|
|
42
|
-
if (!fs.existsSync(configDir)) {
|
|
43
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2));
|
|
46
|
-
}
|
|
47
|
-
var LINUX_CHROME_PATHS = [
|
|
48
|
-
"/usr/bin/google-chrome-stable",
|
|
49
|
-
"/usr/bin/google-chrome",
|
|
50
|
-
"/usr/bin/chromium-browser",
|
|
51
|
-
"/usr/bin/chromium",
|
|
52
|
-
"/snap/bin/google-chrome"
|
|
53
|
-
// Ubuntu snap 安装
|
|
54
|
-
];
|
|
55
|
-
function detectLinuxChromePath() {
|
|
56
|
-
for (const chromePath of LINUX_CHROME_PATHS) {
|
|
57
|
-
if (fs.existsSync(chromePath)) {
|
|
58
|
-
return chromePath;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
function getChromeExecutablePath(platform, userPath) {
|
|
64
|
-
if (userPath && fs.existsSync(userPath)) {
|
|
65
|
-
return userPath;
|
|
66
|
-
}
|
|
67
|
-
switch (platform) {
|
|
68
|
-
case "win32":
|
|
69
|
-
const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
|
|
70
|
-
const winPath = path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe");
|
|
71
|
-
if (fs.existsSync(winPath)) {
|
|
72
|
-
return winPath;
|
|
73
|
-
}
|
|
74
|
-
const programFilesX86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
|
|
75
|
-
const winPathX86 = path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe");
|
|
76
|
-
if (fs.existsSync(winPathX86)) {
|
|
77
|
-
return winPathX86;
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
case "darwin":
|
|
81
|
-
const macPath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
82
|
-
if (fs.existsSync(macPath)) {
|
|
83
|
-
return macPath;
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
86
|
-
case "linux":
|
|
87
|
-
return detectLinuxChromePath();
|
|
88
|
-
default:
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
var PROFILE_TARGET_NAME = "chrome-profile";
|
|
93
|
-
async function clearCopiedProfile(profileDir) {
|
|
94
|
-
const targetPath = path.join(profileDir, PROFILE_TARGET_NAME);
|
|
95
|
-
if (fs.existsSync(targetPath)) {
|
|
96
|
-
await fsp.rm(targetPath, { recursive: true, force: true });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function profileExists(profileDir) {
|
|
100
|
-
const targetPath = path.join(profileDir, PROFILE_TARGET_NAME);
|
|
101
|
-
return fs.existsSync(targetPath);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
30
|
// src/browser.ts
|
|
105
|
-
import { chromium } from "playwright
|
|
106
|
-
import
|
|
107
|
-
import { spawn, execSync } from "child_process";
|
|
108
|
-
import path2 from "path";
|
|
109
|
-
import os2 from "os";
|
|
110
|
-
import fs2 from "fs";
|
|
111
|
-
var CDP_PORT = 19222;
|
|
112
|
-
var DEBUG_DIR = path2.join(os2.homedir(), ".yiyan-browser-agent", "debug");
|
|
31
|
+
import { chromium } from "playwright";
|
|
32
|
+
import fs from "fs";
|
|
113
33
|
function log(verbose, msg) {
|
|
114
34
|
if (verbose) {
|
|
115
35
|
process.stderr.write(`[yiyan-agent] ${msg}
|
|
116
36
|
`);
|
|
117
37
|
}
|
|
118
38
|
}
|
|
119
|
-
function ensureDebugDir() {
|
|
120
|
-
if (!fs2.existsSync(DEBUG_DIR)) {
|
|
121
|
-
fs2.mkdirSync(DEBUG_DIR, { recursive: true });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function killProcessOnPort(port) {
|
|
125
|
-
try {
|
|
126
|
-
if (process.platform === "win32") {
|
|
127
|
-
const output = execSync(
|
|
128
|
-
`netstat -ano | findstr :${port} | findstr LISTENING`,
|
|
129
|
-
{ encoding: "utf-8", timeout: 5e3 }
|
|
130
|
-
).trim();
|
|
131
|
-
if (output) {
|
|
132
|
-
const lines = output.split("\n").filter((l) => l.trim());
|
|
133
|
-
const pids = /* @__PURE__ */ new Set();
|
|
134
|
-
for (const line of lines) {
|
|
135
|
-
const parts = line.trim().split(/\s+/);
|
|
136
|
-
const pid = parts[parts.length - 1];
|
|
137
|
-
if (pid && /^\d+$/.test(pid)) pids.add(pid);
|
|
138
|
-
}
|
|
139
|
-
for (const pid of pids) {
|
|
140
|
-
try {
|
|
141
|
-
execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5e3 });
|
|
142
|
-
} catch {
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
const output = execSync(
|
|
148
|
-
`lsof -ti :${port}`,
|
|
149
|
-
{ encoding: "utf-8", timeout: 5e3 }
|
|
150
|
-
).trim();
|
|
151
|
-
if (output) {
|
|
152
|
-
const pids = output.split("\n").filter((l) => l.trim());
|
|
153
|
-
for (const pid of pids) {
|
|
154
|
-
try {
|
|
155
|
-
process.kill(parseInt(pid, 10), "SIGKILL");
|
|
156
|
-
} catch {
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function killProcessTree(pid) {
|
|
165
|
-
try {
|
|
166
|
-
if (process.platform === "win32") {
|
|
167
|
-
execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5e3 });
|
|
168
|
-
} else {
|
|
169
|
-
try {
|
|
170
|
-
process.kill(-pid, "SIGKILL");
|
|
171
|
-
} catch {
|
|
172
|
-
process.kill(pid, "SIGKILL");
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
} catch {
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function waitForCDP(port, timeout = 15e3) {
|
|
179
|
-
return new Promise((resolve, reject) => {
|
|
180
|
-
const start = Date.now();
|
|
181
|
-
const check = () => {
|
|
182
|
-
http.get(`http://localhost:${port}/json/version`, (res) => {
|
|
183
|
-
let data = "";
|
|
184
|
-
res.on("data", (chunk) => data += chunk);
|
|
185
|
-
res.on("end", () => {
|
|
186
|
-
try {
|
|
187
|
-
resolve(JSON.parse(data));
|
|
188
|
-
} catch (e) {
|
|
189
|
-
reject(e);
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
}).on("error", () => {
|
|
193
|
-
if (Date.now() - start > timeout) reject(new Error("CDP connection timeout"));
|
|
194
|
-
else setTimeout(check, 500);
|
|
195
|
-
});
|
|
196
|
-
};
|
|
197
|
-
check();
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
39
|
async function launchBrowser(options) {
|
|
201
|
-
const {
|
|
202
|
-
if (!chromePath || !fs2.existsSync(chromePath)) {
|
|
203
|
-
throw new YiyanAgentError(
|
|
204
|
-
"BROWSER_LAUNCH",
|
|
205
|
-
`Chrome not found at: ${chromePath || "unknown path"}. Please install Chrome or use --chrome-path to specify a custom path.`
|
|
206
|
-
);
|
|
207
|
-
}
|
|
40
|
+
const { sessionDir, headless, timeout = 3e4, verbose = false } = options;
|
|
208
41
|
try {
|
|
209
|
-
log(verbose, "\
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
42
|
+
log(verbose, "\u542F\u52A8 Chromium \u6D4F\u89C8\u5668...");
|
|
43
|
+
if (!fs.existsSync(sessionDir)) {
|
|
44
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
45
|
+
}
|
|
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",
|
|
221
58
|
"--no-sandbox",
|
|
222
59
|
"--disable-setuid-sandbox",
|
|
223
60
|
"--disable-dev-shm-usage"
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (headless) {
|
|
227
|
-
chromeArgs.push("--headless=new");
|
|
228
|
-
}
|
|
229
|
-
log(verbose, `\u542F\u52A8 Chrome: ${chromePath} ${headless ? "(headless)" : "(headed)"}`);
|
|
230
|
-
log(verbose, `Chrome \u53C2\u6570: ${chromeArgs.join(" ")}`);
|
|
231
|
-
const chromeProcess = spawn(chromePath, chromeArgs, {
|
|
232
|
-
detached: true,
|
|
233
|
-
stdio: "ignore"
|
|
234
|
-
});
|
|
235
|
-
chromeProcess.unref();
|
|
236
|
-
const spawnError = new Promise((_, reject) => {
|
|
237
|
-
chromeProcess.on("error", (err) => {
|
|
238
|
-
reject(new YiyanAgentError("BROWSER_LAUNCH", `Failed to spawn Chrome: ${err.message}`));
|
|
239
|
-
});
|
|
61
|
+
],
|
|
62
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
240
63
|
});
|
|
241
|
-
log(verbose, "\u7B49\u5F85 CDP \u7AEF\u53E3\u5C31\u7EEA...");
|
|
242
|
-
await Promise.race([
|
|
243
|
-
waitForCDP(CDP_PORT, timeout),
|
|
244
|
-
spawnError
|
|
245
|
-
]);
|
|
246
|
-
log(verbose, "\u901A\u8FC7 CDP \u8FDE\u63A5\u6D4F\u89C8\u5668...");
|
|
247
|
-
const cdpBrowser = await chromium.connectOverCDP(`http://localhost:${CDP_PORT}`);
|
|
248
|
-
const contexts = cdpBrowser.contexts();
|
|
249
|
-
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
250
64
|
context.setDefaultTimeout(timeout);
|
|
251
65
|
const pages = context.pages();
|
|
252
66
|
const page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
67
|
+
await page.addInitScript(() => {
|
|
68
|
+
Object.defineProperty(navigator, "webdriver", { get: () => false });
|
|
69
|
+
});
|
|
253
70
|
log(verbose, "\u6D4F\u89C8\u5668\u542F\u52A8\u5B8C\u6210");
|
|
254
|
-
return {
|
|
71
|
+
return { context, page };
|
|
255
72
|
} catch (error) {
|
|
256
|
-
killProcessOnPort(CDP_PORT);
|
|
257
73
|
throw new YiyanAgentError(
|
|
258
74
|
"BROWSER_LAUNCH",
|
|
259
75
|
`Failed to launch browser: ${error instanceof Error ? error.message : String(error)}`
|
|
260
76
|
);
|
|
261
77
|
}
|
|
262
78
|
}
|
|
263
|
-
async function closeBrowser(
|
|
79
|
+
async function closeBrowser(context, verbose) {
|
|
264
80
|
log(!!verbose, "\u5173\u95ED\u6D4F\u89C8\u5668...");
|
|
265
|
-
if (cdpBrowser) {
|
|
266
|
-
try {
|
|
267
|
-
await cdpBrowser.close();
|
|
268
|
-
} catch {
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
81
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
await page.close().catch(() => {
|
|
275
|
-
});
|
|
276
|
-
}
|
|
82
|
+
await context.close();
|
|
83
|
+
log(!!verbose, "\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED");
|
|
277
84
|
} catch {
|
|
278
85
|
}
|
|
279
|
-
if (chromeProcess && chromeProcess.pid) {
|
|
280
|
-
killProcessTree(chromeProcess.pid);
|
|
281
|
-
}
|
|
282
|
-
killProcessOnPort(CDP_PORT);
|
|
283
|
-
log(!!verbose, "\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED");
|
|
284
86
|
}
|
|
285
87
|
async function navigateToYiyan(page, verbose = false) {
|
|
286
88
|
log(verbose, "\u5BFC\u822A\u5230\u6587\u5FC3\u4E00\u8A00\u804A\u5929\u9875\u9762...");
|
|
287
|
-
await page.goto(YIYAN_CHAT_URL, {
|
|
288
|
-
waitUntil: "networkidle",
|
|
289
|
-
timeout: 6e4
|
|
290
|
-
});
|
|
291
|
-
await page.waitForTimeout(5e3);
|
|
292
89
|
try {
|
|
293
|
-
await page.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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)}`);
|
|
297
98
|
}
|
|
298
99
|
}
|
|
299
100
|
async function checkCaptcha(page, verbose = false) {
|
|
@@ -349,7 +150,7 @@ async function waitForUserAction(page, reason, verbose = false) {
|
|
|
349
150
|
const maxWait = 18e4;
|
|
350
151
|
const startTime = Date.now();
|
|
351
152
|
while (Date.now() - startTime < maxWait) {
|
|
352
|
-
await
|
|
153
|
+
await page.waitForTimeout(2e3);
|
|
353
154
|
if (reason === "captcha") {
|
|
354
155
|
const stillHasCaptcha = await checkCaptcha(page, false);
|
|
355
156
|
if (!stillHasCaptcha) {
|
|
@@ -378,24 +179,38 @@ async function waitForUserAction(page, reason, verbose = false) {
|
|
|
378
179
|
log(verbose, "\u26A0 \u7B49\u5F85\u7528\u6237\u64CD\u4F5C\u8D85\u65F6\uFF083\u5206\u949F\uFF09");
|
|
379
180
|
}
|
|
380
181
|
async function sendMessage(page, message, verbose = false, headful = false) {
|
|
381
|
-
const
|
|
182
|
+
const inputSelectors = [
|
|
183
|
+
'[contenteditable="true"][role="textbox"]',
|
|
184
|
+
'[contenteditable="true"]',
|
|
185
|
+
"textarea[placeholder]",
|
|
186
|
+
"textarea"
|
|
187
|
+
];
|
|
382
188
|
log(verbose, "\u7B49\u5F85\u8F93\u5165\u6846\u51FA\u73B0...");
|
|
383
|
-
|
|
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
|
+
}
|
|
384
197
|
if (!inputElement) {
|
|
385
|
-
|
|
386
|
-
throw new YiyanAgentError("NETWORK", "Input element not found on page");
|
|
198
|
+
throw new YiyanAgentError("NETWORK", "Cannot find the Yiyan chat input box");
|
|
387
199
|
}
|
|
388
200
|
log(verbose, "\u2713 \u8F93\u5165\u6846\u5DF2\u627E\u5230");
|
|
389
|
-
await inputElement.click();
|
|
390
|
-
await page.waitForTimeout(
|
|
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
|
+
}, { element: inputElement, content: message });
|
|
391
212
|
log(verbose, `\u8F93\u5165\u95EE\u9898: "${message}"`);
|
|
392
|
-
await page.
|
|
393
|
-
const filledValue = await inputElement.innerText();
|
|
394
|
-
if (filledValue.includes(message)) {
|
|
395
|
-
log(verbose, "\u2713 \u95EE\u9898\u5DF2\u6210\u529F\u586B\u5165\u8F93\u5165\u6846");
|
|
396
|
-
} else {
|
|
397
|
-
log(verbose, `\u26A0 \u8F93\u5165\u6846\u5185\u5BB9: "${filledValue.substring(0, 50)}"`);
|
|
398
|
-
}
|
|
213
|
+
await page.waitForTimeout(400);
|
|
399
214
|
log(verbose, "\u6309 Enter \u53D1\u9001\u6D88\u606F...");
|
|
400
215
|
await page.keyboard.press("Enter");
|
|
401
216
|
await page.waitForTimeout(3e3);
|
|
@@ -403,47 +218,32 @@ async function sendMessage(page, message, verbose = false, headful = false) {
|
|
|
403
218
|
if (headful) {
|
|
404
219
|
await waitForUserAction(page, "captcha", verbose);
|
|
405
220
|
log(verbose, "\u9A8C\u8BC1\u7801\u5DF2\u901A\u8FC7\uFF0C\u91CD\u65B0\u53D1\u9001\u6D88\u606F...");
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}, { element: inputEl, content: message });
|
|
233
|
+
await page.keyboard.press("Enter");
|
|
234
|
+
await page.waitForTimeout(3e3);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
413
239
|
}
|
|
414
240
|
} else {
|
|
415
241
|
throw new YiyanAgentError(
|
|
416
242
|
"CAPTCHA",
|
|
417
|
-
|
|
243
|
+
"Yiyan detected automation and triggered a captcha. Use headed mode to manually solve it."
|
|
418
244
|
);
|
|
419
245
|
}
|
|
420
246
|
}
|
|
421
|
-
const url = page.url();
|
|
422
|
-
log(verbose, `\u5F53\u524D URL: ${url}`);
|
|
423
|
-
if (url.includes("/chat/")) {
|
|
424
|
-
log(verbose, "\u2713 \u5DF2\u81EA\u52A8\u8FDB\u5165\u5BF9\u8BDD\u9875\u9762");
|
|
425
|
-
} else if (url === "https://yiyan.baidu.com/" || url === "https://yiyan.baidu.com") {
|
|
426
|
-
log(verbose, "\u4ECD\u5728\u6B22\u8FCE\u9875\uFF0C\u5C1D\u8BD5\u70B9\u51FB\u53D1\u9001\u6309\u94AE...");
|
|
427
|
-
try {
|
|
428
|
-
const sendBtn = page.locator('[class*="send__"]').first();
|
|
429
|
-
await sendBtn.click({ timeout: 5e3 });
|
|
430
|
-
await page.waitForTimeout(3e3);
|
|
431
|
-
log(verbose, "\u2713 \u5DF2\u70B9\u51FB\u53D1\u9001\u6309\u94AE");
|
|
432
|
-
} catch {
|
|
433
|
-
log(verbose, "\u26A0 \u70B9\u51FB\u53D1\u9001\u6309\u94AE\u5931\u8D25\uFF0C\u7EE7\u7EED\u7B49\u5F85\u56DE\u590D...");
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
log(verbose, `\u5F53\u524D\u9875\u9762: ${url}`);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
async function saveDebugScreenshot(page, name, verbose) {
|
|
440
|
-
try {
|
|
441
|
-
ensureDebugDir();
|
|
442
|
-
const screenshotPath = path2.join(DEBUG_DIR, `${name}.png`);
|
|
443
|
-
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
444
|
-
log(verbose, `\u8C03\u8BD5\u622A\u56FE\u5DF2\u4FDD\u5B58: ${screenshotPath}`);
|
|
445
|
-
} catch {
|
|
446
|
-
}
|
|
447
247
|
}
|
|
448
248
|
async function waitForReply(page, timeout, verbose = false, headful = false) {
|
|
449
249
|
const maxWait = Math.min(timeout, 6e4);
|
|
@@ -479,13 +279,13 @@ async function waitForReply(page, timeout, verbose = false, headful = false) {
|
|
|
479
279
|
});
|
|
480
280
|
if (state.hasAiReply && state.aiTextLen > 0) {
|
|
481
281
|
if (!replyStarted) {
|
|
482
|
-
log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5F00\u59CB\u751F\u6210\uFF08${state.aiTextLen}\u5B57\uFF09
|
|
282
|
+
log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5F00\u59CB\u751F\u6210\uFF08${state.aiTextLen}\u5B57\uFF09`);
|
|
483
283
|
replyStarted = true;
|
|
484
284
|
}
|
|
485
285
|
if (state.aiTextLen === lastLen) {
|
|
486
286
|
stableCount++;
|
|
487
287
|
if (stableCount >= 3) {
|
|
488
|
-
log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5B8C\u6210\uFF08${state.aiTextLen}\u5B57\
|
|
288
|
+
log(verbose, `\u2713 AI \u56DE\u590D\u5DF2\u5B8C\u6210\uFF08${state.aiTextLen}\u5B57\uFF09`);
|
|
489
289
|
break;
|
|
490
290
|
}
|
|
491
291
|
} else {
|
|
@@ -493,134 +293,31 @@ async function waitForReply(page, timeout, verbose = false, headful = false) {
|
|
|
493
293
|
lastLen = state.aiTextLen;
|
|
494
294
|
}
|
|
495
295
|
}
|
|
496
|
-
await page.waitForTimeout(
|
|
296
|
+
await page.waitForTimeout(500);
|
|
497
297
|
}
|
|
498
298
|
if (!replyStarted) {
|
|
499
299
|
if (await checkCaptcha(page, verbose)) {
|
|
500
300
|
if (headful) {
|
|
501
301
|
await waitForUserAction(page, "captcha", verbose);
|
|
502
|
-
const retryStart = Date.now();
|
|
503
|
-
const retryMaxWait = 3e4;
|
|
504
|
-
while (Date.now() - retryStart < retryMaxWait) {
|
|
505
|
-
const retryState = await page.evaluate(() => {
|
|
506
|
-
const answerBox = document.querySelector('[class*="answerBox"]');
|
|
507
|
-
if (answerBox) {
|
|
508
|
-
const text = answerBox.innerText?.trim() || "";
|
|
509
|
-
return { hasAiReply: text.length > 0, aiTextLen: text.length };
|
|
510
|
-
}
|
|
511
|
-
return { hasAiReply: false, aiTextLen: 0 };
|
|
512
|
-
});
|
|
513
|
-
if (retryState.hasAiReply) {
|
|
514
|
-
log(verbose, "\u2713 \u9A8C\u8BC1\u7801\u901A\u8FC7\u540E AI \u5DF2\u5F00\u59CB\u56DE\u590D");
|
|
515
|
-
replyStarted = true;
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
await page.waitForTimeout(1500);
|
|
519
|
-
}
|
|
520
302
|
} else {
|
|
521
303
|
throw new YiyanAgentError(
|
|
522
304
|
"CAPTCHA",
|
|
523
|
-
|
|
305
|
+
"Yiyan detected automation and triggered a captcha."
|
|
524
306
|
);
|
|
525
307
|
}
|
|
526
308
|
} else if (headful) {
|
|
527
309
|
await waitForUserAction(page, "no-reply", verbose);
|
|
528
|
-
const retryStart = Date.now();
|
|
529
|
-
const retryMaxWait = 6e4;
|
|
530
|
-
while (Date.now() - retryStart < retryMaxWait) {
|
|
531
|
-
const retryState = await page.evaluate(() => {
|
|
532
|
-
const answerBox = document.querySelector('[class*="answerBox"]');
|
|
533
|
-
if (answerBox) {
|
|
534
|
-
const text = answerBox.innerText?.trim() || "";
|
|
535
|
-
return { hasAiReply: text.length > 0, aiTextLen: text.length };
|
|
536
|
-
}
|
|
537
|
-
return { hasAiReply: false, aiTextLen: 0 };
|
|
538
|
-
});
|
|
539
|
-
if (retryState.hasAiReply) {
|
|
540
|
-
if (retryState.aiTextLen === lastLen) {
|
|
541
|
-
stableCount++;
|
|
542
|
-
if (stableCount >= 3) {
|
|
543
|
-
log(verbose, `\u2713 \u7528\u6237\u64CD\u4F5C\u540E AI \u56DE\u590D\u5DF2\u5B8C\u6210\uFF08${retryState.aiTextLen}\u5B57\uFF09`);
|
|
544
|
-
replyStarted = true;
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
} else {
|
|
548
|
-
stableCount = 0;
|
|
549
|
-
lastLen = retryState.aiTextLen;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
await page.waitForTimeout(1500);
|
|
553
|
-
}
|
|
554
310
|
} else {
|
|
555
311
|
throw new YiyanAgentError(
|
|
556
312
|
"TIMEOUT",
|
|
557
|
-
|
|
313
|
+
"AI reply timeout in headless mode. Try running login first, or use headed mode."
|
|
558
314
|
);
|
|
559
315
|
}
|
|
560
316
|
}
|
|
561
|
-
await page.waitForTimeout(
|
|
562
|
-
const pageDump = await page.evaluate(() => {
|
|
563
|
-
const result = {};
|
|
564
|
-
const selectors = [
|
|
565
|
-
'[class*="answerBox"]',
|
|
566
|
-
'[class*="dialogueCardList"]',
|
|
567
|
-
'[class*="dialogue_card_item"]',
|
|
568
|
-
'[class*="chatViewer"]',
|
|
569
|
-
'[class*="flowBox"]',
|
|
570
|
-
'[class*="roleSystem"]',
|
|
571
|
-
'[class*="mdRenderContainer"]',
|
|
572
|
-
'[class*="agent-markdown"]',
|
|
573
|
-
'[class*="markdown"]',
|
|
574
|
-
'[class*="content"]',
|
|
575
|
-
'[class*="chat"]',
|
|
576
|
-
'[class*="message"]'
|
|
577
|
-
];
|
|
578
|
-
for (const sel of selectors) {
|
|
579
|
-
result[`selector:${sel}`] = document.querySelectorAll(sel).length;
|
|
580
|
-
}
|
|
581
|
-
const answerBox = document.querySelector('[class*="answerBox"]');
|
|
582
|
-
if (answerBox) {
|
|
583
|
-
result["answerBox"] = {
|
|
584
|
-
textLen: answerBox.innerText?.trim().length || 0,
|
|
585
|
-
preview: (answerBox.innerText?.trim() || "").substring(0, 200),
|
|
586
|
-
classes: answerBox.className?.toString().substring(0, 100) || ""
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
result["bodyText"] = document.body.innerText?.substring(0, 1e3) || "";
|
|
590
|
-
const longTextEls = [];
|
|
591
|
-
const mainContent = document.querySelector('[class*="chatViewer"]') || document.querySelector('[class*="chat"]') || document.body;
|
|
592
|
-
for (const el of Array.from(mainContent.querySelectorAll("div, section, article"))) {
|
|
593
|
-
const text = el.innerText?.trim() || "";
|
|
594
|
-
if (text.length > 50 && text.length < 1e4) {
|
|
595
|
-
longTextEls.push({
|
|
596
|
-
tag: el.tagName,
|
|
597
|
-
classes: el.className?.toString().split(" ").slice(0, 3).join(" ") || "",
|
|
598
|
-
textLen: text.length,
|
|
599
|
-
preview: text.substring(0, 80)
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
result["longTextElements"] = longTextEls.sort((a, b) => b.textLen - a.textLen).slice(0, 10);
|
|
604
|
-
return result;
|
|
605
|
-
});
|
|
606
|
-
log(verbose, "=== \u9875\u9762\u8BCA\u65AD ===");
|
|
607
|
-
for (const [key, value] of Object.entries(pageDump)) {
|
|
608
|
-
if (key === "bodyText" || key === "longTextElements") continue;
|
|
609
|
-
log(verbose, ` ${key}: ${JSON.stringify(value)}`);
|
|
610
|
-
}
|
|
611
|
-
if (pageDump["longTextElements"] && pageDump["longTextElements"].length > 0) {
|
|
612
|
-
log(verbose, " \u957F\u6587\u672C\u5143\u7D20 TOP 10:");
|
|
613
|
-
for (const el of pageDump["longTextElements"]) {
|
|
614
|
-
log(verbose, ` <${el.tag}> .${el.classes} (${el.textLen}\u5B57): ${el.preview}...`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (pageDump["bodyText"]) {
|
|
618
|
-
log(verbose, ` \u9875\u9762\u6587\u672C(\u524D1000\u5B57): ${pageDump["bodyText"].substring(0, 1e3)}`);
|
|
619
|
-
}
|
|
317
|
+
await page.waitForTimeout(1e3);
|
|
620
318
|
}
|
|
621
319
|
async function extractReply(page, question, verbose = false) {
|
|
622
320
|
log(verbose, "\u63D0\u53D6 AI \u56DE\u590D\u5185\u5BB9...");
|
|
623
|
-
await saveDebugScreenshot(page, "before-extract", verbose);
|
|
624
321
|
const reply = await page.evaluate((userQuestion) => {
|
|
625
322
|
const debugInfo = [];
|
|
626
323
|
const answerBox = document.querySelector('[class*="answerBox"]');
|
|
@@ -628,19 +325,12 @@ async function extractReply(page, question, verbose = false) {
|
|
|
628
325
|
if (answerBox) {
|
|
629
326
|
const clone = answerBox.cloneNode(true);
|
|
630
327
|
const processItems = clone.querySelectorAll('[class*="processItem"], [class*="processContent"]');
|
|
631
|
-
for (const item of processItems)
|
|
632
|
-
item.remove();
|
|
633
|
-
}
|
|
328
|
+
for (const item of processItems) item.remove();
|
|
634
329
|
const toolMessages = clone.querySelectorAll('[class*="toolMessage"]');
|
|
635
|
-
for (const msg of toolMessages)
|
|
636
|
-
msg.remove();
|
|
637
|
-
}
|
|
330
|
+
for (const msg of toolMessages) msg.remove();
|
|
638
331
|
const thinkHeaders = clone.querySelectorAll('[class*="headerMask"], [class*="topHeader"]');
|
|
639
|
-
for (const h of thinkHeaders)
|
|
640
|
-
h.remove();
|
|
641
|
-
}
|
|
332
|
+
for (const h of thinkHeaders) h.remove();
|
|
642
333
|
const text = clone.innerText?.trim() || "";
|
|
643
|
-
debugInfo.push(`answerBox after removing thinking: ${text.length} chars`);
|
|
644
334
|
const cleanedText = text.replace(/^参考\d+个网页\s*/, "").replace(/^深度思考已完成\s*/, "").replace(/^思考完成[::]\s*/, "").replace(/^准备输出结果\s*/, "").trim();
|
|
645
335
|
if (cleanedText.length > 0) {
|
|
646
336
|
debugInfo.push(`method1 success: ${cleanedText.length} chars`);
|
|
@@ -652,12 +342,10 @@ async function extractReply(page, question, verbose = false) {
|
|
|
652
342
|
for (const container of allMdContainers) {
|
|
653
343
|
const parent = container.parentElement;
|
|
654
344
|
const parentClass = parent?.className?.toString() || "";
|
|
655
|
-
if (parentClass.includes("toolMessage") || parentClass.includes("process"))
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
345
|
+
if (parentClass.includes("toolMessage") || parentClass.includes("process")) continue;
|
|
658
346
|
const text = container.innerText?.trim() || "";
|
|
659
347
|
if (text.length > 10) {
|
|
660
|
-
debugInfo.push(`method2 success: ${text.length} chars
|
|
348
|
+
debugInfo.push(`method2 success: ${text.length} chars`);
|
|
661
349
|
return JSON.stringify({ text, debug: debugInfo });
|
|
662
350
|
}
|
|
663
351
|
}
|
|
@@ -676,21 +364,6 @@ async function extractReply(page, question, verbose = false) {
|
|
|
676
364
|
}
|
|
677
365
|
}
|
|
678
366
|
}
|
|
679
|
-
const flowBoxes = document.querySelectorAll('[class*="flowBox"]');
|
|
680
|
-
debugInfo.push(`flowBoxes count: ${flowBoxes.length}`);
|
|
681
|
-
for (let i = flowBoxes.length - 1; i >= 0; i--) {
|
|
682
|
-
const box = flowBoxes[i];
|
|
683
|
-
const boxClass = box.className?.toString() || "";
|
|
684
|
-
if (boxClass.includes("questionBox")) continue;
|
|
685
|
-
const clone = box.cloneNode(true);
|
|
686
|
-
const thinkEls = clone.querySelectorAll('[class*="processItem"], [class*="processContent"], [class*="toolMessage"]');
|
|
687
|
-
for (const el of thinkEls) el.remove();
|
|
688
|
-
const text = clone.innerText?.trim() || "";
|
|
689
|
-
if (text.length > 10) {
|
|
690
|
-
debugInfo.push(`method4 success: flowBox[${i}] ${text.length} chars`);
|
|
691
|
-
return JSON.stringify({ text, debug: debugInfo });
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
367
|
const fullText = document.body.innerText;
|
|
695
368
|
const lines = fullText.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
696
369
|
const uiWords = /* @__PURE__ */ new Set([
|
|
@@ -707,27 +380,15 @@ async function extractReply(page, question, verbose = false) {
|
|
|
707
380
|
"\u672A\u767B\u5F55",
|
|
708
381
|
"\u767B\u5F55",
|
|
709
382
|
"\u5185\u5BB9\u7531AI\u751F\u6210\uFF0C\u4EC5\u4F9B\u53C2\u8003\uFF0C\u8BF7\u4ED4\u7EC6\u7504\u522B",
|
|
710
|
-
"\u53C2\u8003"
|
|
711
|
-
"\u6DF1\u5EA6\u5206\u6790\u9700\u6C42\u5E76\u89E3\u7B54",
|
|
712
|
-
"\u901A\u7528\u5199\u4F5C",
|
|
713
|
-
"\u9605\u8BFB\u5206\u6790",
|
|
714
|
-
"\u7F51\u9875\u5DE5\u574A",
|
|
715
|
-
"\u667A\u80FD\u7FFB\u8BD1",
|
|
716
|
-
"\u4EE3\u7801\u7F16\u7A0B"
|
|
383
|
+
"\u53C2\u8003"
|
|
717
384
|
]);
|
|
718
385
|
const thinkingKeywords = [
|
|
719
386
|
"\u6DF1\u5EA6\u601D\u8003\u5DF2\u5B8C\u6210",
|
|
720
387
|
"\u601D\u8003\u5B8C\u6210",
|
|
721
|
-
"\u51C6\u5907\u8F93\u51FA\u7ED3\u679C"
|
|
722
|
-
"picaole\u5728\u8BE2\u95EE",
|
|
723
|
-
"picaole\u95EE\u4E86\u4E00\u4E2A",
|
|
724
|
-
"picaole\u5728\u641C\u7D22",
|
|
725
|
-
"\u76F4\u63A5\u56DE\u7B54\u5373\u53EF",
|
|
726
|
-
"\u4E0D\u9700\u8981\u590D\u6742\u5206\u6790",
|
|
727
|
-
"\u6309\u7167\u56DE\u7B54\u65B9\u5F0F"
|
|
388
|
+
"\u51C6\u5907\u8F93\u51FA\u7ED3\u679C"
|
|
728
389
|
];
|
|
729
390
|
const userQIdx = lines.findIndex((l) => l === userQuestion || l.includes(userQuestion));
|
|
730
|
-
debugInfo.push(`userQ idx
|
|
391
|
+
debugInfo.push(`userQ idx: ${userQIdx}, lines: ${lines.length}`);
|
|
731
392
|
if (userQIdx >= 0) {
|
|
732
393
|
const replyLines = [];
|
|
733
394
|
for (let i = userQIdx + 1; i < lines.length; i++) {
|
|
@@ -736,34 +397,15 @@ async function extractReply(page, question, verbose = false) {
|
|
|
736
397
|
if (thinkingKeywords.some((kw) => line.includes(kw))) continue;
|
|
737
398
|
if (line.startsWith("\u641C\u7D22") || line.includes("\u7BC7\u8D44\u6599")) continue;
|
|
738
399
|
if (line.match(/^参考\d+个网页/)) continue;
|
|
739
|
-
if (line.length > 0)
|
|
740
|
-
replyLines.push(line);
|
|
741
|
-
}
|
|
400
|
+
if (line.length > 0) replyLines.push(line);
|
|
742
401
|
if (line === "\u5FEB\u901F" || line === "\u66F4\u591A" || line.includes("\u5185\u5BB9\u7531AI\u751F\u6210")) break;
|
|
743
402
|
}
|
|
744
403
|
if (replyLines.length > 0) {
|
|
745
|
-
debugInfo.push(`
|
|
404
|
+
debugInfo.push(`method4 success: ${replyLines.length} lines`);
|
|
746
405
|
return JSON.stringify({ text: replyLines.join("\n"), debug: debugInfo });
|
|
747
406
|
}
|
|
748
407
|
}
|
|
749
|
-
|
|
750
|
-
const blockElements = document.querySelectorAll("div, section, article, p");
|
|
751
|
-
for (const el of blockElements) {
|
|
752
|
-
const elText = el.innerText?.trim() || "";
|
|
753
|
-
if (elText.length > 50 && elText.length < 1e4) {
|
|
754
|
-
if (uiWords.has(elText)) continue;
|
|
755
|
-
if (thinkingKeywords.some((kw) => elText.includes(kw))) continue;
|
|
756
|
-
allTextBlocks.push({ text: elText, length: elText.length });
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
allTextBlocks.sort((a, b) => b.length - a.length);
|
|
760
|
-
if (allTextBlocks.length > 0) {
|
|
761
|
-
debugInfo.push(`method6 fallback: longest text block ${allTextBlocks[0].length} chars`);
|
|
762
|
-
return JSON.stringify({ text: allTextBlocks[0].text, debug: debugInfo });
|
|
763
|
-
}
|
|
764
|
-
debugInfo.push(`ALL METHODS FAILED`);
|
|
765
|
-
debugInfo.push(`body text length: ${fullText.length}`);
|
|
766
|
-
debugInfo.push(`body text first 500: ${fullText.substring(0, 500)}`);
|
|
408
|
+
debugInfo.push("ALL METHODS FAILED");
|
|
767
409
|
return JSON.stringify({ text: "", debug: debugInfo });
|
|
768
410
|
}, question);
|
|
769
411
|
let parsed;
|
|
@@ -779,92 +421,39 @@ async function extractReply(page, question, verbose = false) {
|
|
|
779
421
|
log(verbose, `\u63D0\u53D6\u6210\u529F\uFF0C\u56DE\u590D\u957F\u5EA6: ${parsed.text.length} \u5B57\u7B26`);
|
|
780
422
|
return parsed.text;
|
|
781
423
|
}
|
|
782
|
-
log(verbose, "\u63D0\u53D6\u5931\u8D25\uFF0C\u6240\u6709\u65B9\u6CD5\u5747\u672A\u627E\u5230\u56DE\u590D\u5185\u5BB9");
|
|
783
424
|
throw new YiyanAgentError("TIMEOUT", "Failed to extract reply content");
|
|
784
425
|
}
|
|
785
426
|
|
|
786
427
|
// src/agent.ts
|
|
787
|
-
var
|
|
788
|
-
os3.homedir(),
|
|
789
|
-
".yiyan-browser-agent"
|
|
790
|
-
);
|
|
428
|
+
var DEFAULT_SESSION_DIR = path.join(os.homedir(), ".yiyan-browser-agent", "session");
|
|
791
429
|
var YiyanAgent = class {
|
|
792
430
|
options;
|
|
793
|
-
|
|
431
|
+
sessionDir;
|
|
794
432
|
verbose;
|
|
795
|
-
savedChromePath;
|
|
796
433
|
constructor(options) {
|
|
797
434
|
this.options = {
|
|
798
435
|
...DEFAULT_OPTIONS,
|
|
799
436
|
...options
|
|
800
437
|
};
|
|
801
|
-
this.
|
|
438
|
+
this.sessionDir = this.options.profileDir || DEFAULT_SESSION_DIR;
|
|
802
439
|
this.verbose = options?.verbose ?? false;
|
|
803
|
-
const config = readConfig();
|
|
804
|
-
this.savedChromePath = config.chromePath || null;
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* 保存 Chrome 路径到配置文件
|
|
808
|
-
*/
|
|
809
|
-
saveChromePath(chromePath) {
|
|
810
|
-
saveConfig({ chromePath });
|
|
811
|
-
this.savedChromePath = chromePath;
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* 获取有效的 Chrome 路径
|
|
815
|
-
* 优先级:用户指定 > 配置文件保存 > 自动探测
|
|
816
|
-
*/
|
|
817
|
-
getEffectiveChromePath(userPath) {
|
|
818
|
-
const platform = os3.platform();
|
|
819
|
-
if (userPath) {
|
|
820
|
-
return userPath;
|
|
821
|
-
}
|
|
822
|
-
if (this.savedChromePath) {
|
|
823
|
-
return this.savedChromePath;
|
|
824
|
-
}
|
|
825
|
-
return getChromeExecutablePath(platform);
|
|
826
440
|
}
|
|
827
441
|
/**
|
|
828
442
|
* 发送问题并获取答案
|
|
829
443
|
* @param question 问题文本
|
|
830
|
-
* @param headful 是否使用有头浏览器(可见窗口),默认为 false
|
|
831
|
-
*
|
|
832
|
-
* 策略:先尝试 headless(不弹窗),如果失败(验证码/未登录)自动回退到有头模式
|
|
444
|
+
* @param headful 是否使用有头浏览器(可见窗口),默认为 false(headless)
|
|
833
445
|
*/
|
|
834
446
|
async ask(question, headful = false) {
|
|
835
|
-
|
|
836
|
-
return this.executeAsk(question, true);
|
|
837
|
-
}
|
|
838
|
-
try {
|
|
839
|
-
return await this.executeAsk(question, false);
|
|
840
|
-
} catch (error) {
|
|
841
|
-
if (error instanceof YiyanAgentError && (error.type === "CAPTCHA" || error.type === "TIMEOUT")) {
|
|
842
|
-
if (this.verbose) {
|
|
843
|
-
process.stderr.write("[yiyan-agent] headless \u6A21\u5F0F\u5931\u8D25\uFF0C\u81EA\u52A8\u5207\u6362\u5230\u6709\u5934\u6A21\u5F0F...\n");
|
|
844
|
-
}
|
|
845
|
-
return this.executeAsk(question, true);
|
|
846
|
-
}
|
|
847
|
-
throw error;
|
|
848
|
-
}
|
|
447
|
+
return this.executeAsk(question, headful);
|
|
849
448
|
}
|
|
850
449
|
/**
|
|
851
450
|
* 执行单次问答
|
|
852
451
|
*/
|
|
853
452
|
async executeAsk(question, headful) {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
if (!chromePath) {
|
|
857
|
-
throw new YiyanAgentError(
|
|
858
|
-
"BROWSER_LAUNCH",
|
|
859
|
-
`Chrome not found on ${platform}. Please install Chrome or use --chrome-path to specify a custom path.`
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
const profilePath = path3.join(this.profileDir, "chrome-profile");
|
|
863
|
-
const { browser, cdpBrowser, page, chromeProcess } = await launchBrowser({
|
|
864
|
-
chromePath,
|
|
865
|
-
profilePath,
|
|
453
|
+
const { context, page } = await launchBrowser({
|
|
454
|
+
sessionDir: this.sessionDir,
|
|
866
455
|
headless: !headful,
|
|
867
|
-
timeout:
|
|
456
|
+
timeout: this.options.timeout,
|
|
868
457
|
verbose: this.verbose
|
|
869
458
|
});
|
|
870
459
|
try {
|
|
@@ -885,7 +474,7 @@ var YiyanAgent = class {
|
|
|
885
474
|
const reply = await extractReply(page, question, this.verbose);
|
|
886
475
|
return reply;
|
|
887
476
|
} finally {
|
|
888
|
-
await closeBrowser(
|
|
477
|
+
await closeBrowser(context, this.verbose);
|
|
889
478
|
}
|
|
890
479
|
}
|
|
891
480
|
/**
|
|
@@ -893,59 +482,61 @@ var YiyanAgent = class {
|
|
|
893
482
|
* 登录成功后,后续的 ask 调用将自动使用登录状态
|
|
894
483
|
*/
|
|
895
484
|
async login() {
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
if (!chromePath) {
|
|
899
|
-
throw new YiyanAgentError(
|
|
900
|
-
"BROWSER_LAUNCH",
|
|
901
|
-
`Chrome not found on ${platform}. Please install Chrome or use --chrome-path to specify a custom path.`
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
const profilePath = path3.join(this.profileDir, "chrome-profile");
|
|
905
|
-
const { browser, cdpBrowser, page, chromeProcess } = await launchBrowser({
|
|
906
|
-
chromePath,
|
|
907
|
-
profilePath,
|
|
485
|
+
const { context, page } = await launchBrowser({
|
|
486
|
+
sessionDir: this.sessionDir,
|
|
908
487
|
headless: false,
|
|
909
488
|
timeout: 6e4,
|
|
910
489
|
verbose: this.verbose
|
|
911
490
|
});
|
|
912
491
|
try {
|
|
913
|
-
await page
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
process.stderr.write("
|
|
919
|
-
process.stderr.write("[yiyan-agent] \
|
|
920
|
-
|
|
921
|
-
process.stderr.write("[yiyan-agent]
|
|
922
|
-
const readline = await import("readline");
|
|
923
|
-
await new Promise((resolve) => {
|
|
924
|
-
const rl = readline.createInterface({
|
|
925
|
-
input: process.stdin,
|
|
926
|
-
output: process.stderr
|
|
927
|
-
});
|
|
928
|
-
rl.question("\u6309 Enter \u7EE7\u7EED...", () => {
|
|
929
|
-
rl.close();
|
|
930
|
-
resolve();
|
|
931
|
-
});
|
|
932
|
-
});
|
|
933
|
-
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");
|
|
934
501
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
935
502
|
} finally {
|
|
936
|
-
await closeBrowser(
|
|
503
|
+
await closeBrowser(context, this.verbose);
|
|
937
504
|
}
|
|
938
505
|
}
|
|
939
|
-
|
|
940
|
-
|
|
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
|
+
});
|
|
941
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* 清除保存的 session
|
|
524
|
+
*/
|
|
942
525
|
async reset() {
|
|
943
|
-
await
|
|
526
|
+
const fs2 = await import("fs/promises");
|
|
527
|
+
try {
|
|
528
|
+
await fs2.rm(this.sessionDir, { recursive: true, force: true });
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
944
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* 检查状态
|
|
534
|
+
*/
|
|
945
535
|
status() {
|
|
536
|
+
const fs2 = __require("fs");
|
|
946
537
|
return {
|
|
947
|
-
loggedIn:
|
|
948
|
-
|
|
538
|
+
loggedIn: fs2.existsSync(this.sessionDir),
|
|
539
|
+
sessionPath: this.sessionDir
|
|
949
540
|
};
|
|
950
541
|
}
|
|
951
542
|
};
|
|
@@ -956,49 +547,39 @@ function printHelp() {
|
|
|
956
547
|
yiyan-agent - \u6587\u5FC3\u4E00\u8A00\u6D4F\u89C8\u5668\u4EE3\u7406 CLI
|
|
957
548
|
|
|
958
549
|
\u7528\u6CD5:
|
|
959
|
-
yiyan-agent login
|
|
960
|
-
yiyan-agent ask "\u95EE\u9898" [--timeout ms] [--
|
|
961
|
-
yiyan-agent status
|
|
962
|
-
yiyan-agent reset
|
|
963
|
-
yiyan-agent config \u663E\u793A\u5F53\u524D\u914D\u7F6E
|
|
550
|
+
yiyan-agent login \u9996\u6B21\u767B\u5F55\u6587\u5FC3\u4E00\u8A00\uFF08\u4F1A\u6253\u5F00\u6D4F\u89C8\u5668\u7A97\u53E3\uFF09
|
|
551
|
+
yiyan-agent ask "\u95EE\u9898" [--timeout ms] [--headful] [--verbose]
|
|
552
|
+
yiyan-agent status \u68C0\u67E5\u767B\u5F55\u72B6\u6001
|
|
553
|
+
yiyan-agent reset \u6E05\u9664\u4FDD\u5B58\u7684 session
|
|
964
554
|
|
|
965
555
|
\u547D\u4EE4:
|
|
966
556
|
login \u6253\u5F00\u6D4F\u89C8\u5668\u624B\u52A8\u767B\u5F55\u6587\u5FC3\u4E00\u8A00\uFF08\u9996\u6B21\u4F7F\u7528\u5FC5\u987B\u5148\u767B\u5F55\uFF09
|
|
967
557
|
ask \u53D1\u9001\u95EE\u9898\u5E76\u83B7\u53D6\u7B54\u6848\uFF08\u9ED8\u8BA4\u65E0\u5934\u6A21\u5F0F\uFF0C\u4E0D\u5F39\u7A97\uFF1B\u767B\u5F55\u540E\u76F4\u63A5\u4F7F\u7528\uFF09
|
|
968
558
|
status \u68C0\u67E5\u767B\u5F55\u72B6\u6001
|
|
969
|
-
reset \u6E05\u9664\u4FDD\u5B58\u7684
|
|
970
|
-
config \u663E\u793A\u5F53\u524D\u914D\u7F6E
|
|
559
|
+
reset \u6E05\u9664\u4FDD\u5B58\u7684 session
|
|
971
560
|
|
|
972
561
|
\u9009\u9879:
|
|
973
|
-
--timeout <ms>
|
|
974
|
-
--
|
|
975
|
-
--
|
|
976
|
-
--
|
|
977
|
-
--chrome-path <path> \u6307\u5B9A Chrome \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84\uFF08\u4F1A\u4FDD\u5B58\u914D\u7F6E\uFF0C\u540E\u7EED\u81EA\u52A8\u4F7F\u7528\uFF09
|
|
978
|
-
--help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
|
|
562
|
+
--timeout <ms> \u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4 120000
|
|
563
|
+
--headful \u4F7F\u7528\u6709\u5934\u6D4F\u89C8\u5668\uFF08\u53EF\u89C1\u7A97\u53E3\uFF0C\u7528\u4E8E\u624B\u52A8\u8FC7\u9A8C\u8BC1\u7801\uFF09
|
|
564
|
+
--verbose \u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7\uFF08\u8C03\u8BD5\u7528\uFF09
|
|
565
|
+
--help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
|
|
979
566
|
|
|
980
567
|
\u793A\u4F8B:
|
|
981
568
|
yiyan-agent login # \u9996\u6B21\u4F7F\u7528\uFF1A\u767B\u5F55\u6587\u5FC3\u4E00\u8A00
|
|
982
|
-
yiyan-agent login --chrome-path "/usr/bin/google-chrome-stable" # Linux \u6307\u5B9A Chrome \u8DEF\u5F84
|
|
983
569
|
yiyan-agent ask "\u4EC0\u4E48\u662F TypeScript\uFF1F" # \u63D0\u95EE\uFF08\u9759\u9ED8\u6A21\u5F0F\uFF09
|
|
984
570
|
yiyan-agent ask "\u89E3\u91CA Promise" --verbose # \u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7
|
|
985
571
|
yiyan-agent ask "30+30=" --headful # \u6709\u5934\u6A21\u5F0F\uFF08\u53EF\u624B\u52A8\u8FC7\u9A8C\u8BC1\u7801\uFF09
|
|
986
572
|
yiyan-agent status
|
|
987
573
|
yiyan-agent reset
|
|
988
|
-
yiyan-agent config # \u663E\u793A\u4FDD\u5B58\u7684 Chrome \u8DEF\u5F84\u7B49\u914D\u7F6E
|
|
989
574
|
|
|
990
575
|
\u6D41\u7A0B:
|
|
991
576
|
1. \u5148\u8FD0\u884C yiyan-agent login \u767B\u5F55\uFF08\u53EA\u9700\u4E00\u6B21\uFF0C\u767B\u5F55\u72B6\u6001\u4F1A\u4FDD\u5B58\uFF09
|
|
992
577
|
2. \u4E4B\u540E\u76F4\u63A5 yiyan-agent ask "\u95EE\u9898" \u5373\u53EF\uFF0C\u65E0\u9700\u518D\u767B\u5F55
|
|
993
578
|
3. \u5982\u679C\u9047\u5230\u9A8C\u8BC1\u7801\uFF0C\u52A0 --headful \u5207\u6362\u6709\u5934\u6A21\u5F0F\u624B\u52A8\u5904\u7406
|
|
994
579
|
|
|
995
|
-
|
|
996
|
-
\
|
|
997
|
-
|
|
998
|
-
\u5E38\u89C1\u8DEF\u5F84\uFF1A
|
|
999
|
-
/usr/bin/google-chrome-stable (Debian/Ubuntu apt \u5B89\u88C5)
|
|
1000
|
-
/usr/bin/chromium-browser (\u67D0\u4E9B Ubuntu \u7248\u672C)
|
|
1001
|
-
/snap/bin/google-chrome (Ubuntu snap \u5B89\u88C5)
|
|
580
|
+
\u63D0\u793A:
|
|
581
|
+
Chromium \u4F1A\u81EA\u52A8\u4E0B\u8F7D\uFF08\u7EA6150MB\uFF09\uFF0C\u65E0\u9700\u624B\u52A8\u5B89\u88C5 Chrome
|
|
582
|
+
Session \u4FDD\u5B58\u5728 ~/.yiyan-browser-agent/session \u76EE\u5F55
|
|
1002
583
|
`);
|
|
1003
584
|
}
|
|
1004
585
|
function parseCliArgs(args) {
|
|
@@ -1027,33 +608,18 @@ function parseCliArgs(args) {
|
|
|
1027
608
|
if (arg === "login") {
|
|
1028
609
|
result.command = "login";
|
|
1029
610
|
}
|
|
1030
|
-
if (arg === "config") {
|
|
1031
|
-
result.command = "config";
|
|
1032
|
-
}
|
|
1033
611
|
if (arg === "--timeout") {
|
|
1034
612
|
if (i + 1 < args.length) {
|
|
1035
613
|
result.timeout = parseInt(args[i + 1], 10);
|
|
1036
614
|
i++;
|
|
1037
615
|
}
|
|
1038
616
|
}
|
|
1039
|
-
if (arg === "--retry") {
|
|
1040
|
-
if (i + 1 < args.length) {
|
|
1041
|
-
result.retry = parseInt(args[i + 1], 10);
|
|
1042
|
-
i++;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
617
|
if (arg === "--headful") {
|
|
1046
618
|
result.headful = true;
|
|
1047
619
|
}
|
|
1048
620
|
if (arg === "--verbose") {
|
|
1049
621
|
result.verbose = true;
|
|
1050
622
|
}
|
|
1051
|
-
if (arg === "--chrome-path") {
|
|
1052
|
-
if (i + 1 < args.length) {
|
|
1053
|
-
result.chromePath = args[i + 1];
|
|
1054
|
-
i++;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
623
|
}
|
|
1058
624
|
return result;
|
|
1059
625
|
}
|
|
@@ -1070,39 +636,13 @@ async function runCli(args) {
|
|
|
1070
636
|
if (parsed.timeout) {
|
|
1071
637
|
options.timeout = parsed.timeout;
|
|
1072
638
|
}
|
|
1073
|
-
if (parsed.retry) {
|
|
1074
|
-
options.retryCount = parsed.retry;
|
|
1075
|
-
}
|
|
1076
639
|
if (parsed.verbose) {
|
|
1077
640
|
options.verbose = true;
|
|
1078
641
|
}
|
|
1079
|
-
if (parsed.chromePath) {
|
|
1080
|
-
options.chromePath = parsed.chromePath;
|
|
1081
|
-
}
|
|
1082
642
|
const agent = new YiyanAgent(options);
|
|
1083
|
-
if (parsed.command === "config") {
|
|
1084
|
-
const status = agent.status();
|
|
1085
|
-
const configInfo = {
|
|
1086
|
-
...status,
|
|
1087
|
-
savedChromePath: parsed.chromePath || void 0
|
|
1088
|
-
};
|
|
1089
|
-
console.log(formatCliOutput({
|
|
1090
|
-
success: true,
|
|
1091
|
-
question: "config",
|
|
1092
|
-
answer: JSON.stringify(configInfo, null, 2),
|
|
1093
|
-
duration: 0
|
|
1094
|
-
}));
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
643
|
if (parsed.command === "login") {
|
|
1098
|
-
if (parsed.verbose) {
|
|
1099
|
-
console.log("Opening browser for login... Please login to Yiyan (\u6587\u5FC3\u4E00\u8A00) in the browser window.");
|
|
1100
|
-
}
|
|
1101
644
|
try {
|
|
1102
645
|
await agent.login();
|
|
1103
|
-
if (parsed.chromePath) {
|
|
1104
|
-
agent.saveChromePath(parsed.chromePath);
|
|
1105
|
-
}
|
|
1106
646
|
console.log(formatCliOutput({
|
|
1107
647
|
success: true,
|
|
1108
648
|
question: "login",
|
|
@@ -1135,7 +675,7 @@ async function runCli(args) {
|
|
|
1135
675
|
console.log(formatCliOutput({
|
|
1136
676
|
success: true,
|
|
1137
677
|
question: "reset",
|
|
1138
|
-
answer: "
|
|
678
|
+
answer: "Session cleared successfully",
|
|
1139
679
|
duration: 0
|
|
1140
680
|
}));
|
|
1141
681
|
return;
|