yiyan-browser-agent 1.5.3 → 1.6.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/package.json +1 -1
- package/src/agent.js +25 -0
- package/src/browser.js +7 -0
- package/src/config.js +4 -0
- package/src/stability/crash-handler.js +261 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -27,6 +27,31 @@ class YiyanAgent {
|
|
|
27
27
|
async run(task) {
|
|
28
28
|
const startTime = Date.now();
|
|
29
29
|
|
|
30
|
+
// 使用崩溃处理器包装整个执行流程
|
|
31
|
+
if (this.browser.crashHandler) {
|
|
32
|
+
return this.browser.crashHandler.wrap(
|
|
33
|
+
async () => {
|
|
34
|
+
logger.info('Sending task to Yiyan...');
|
|
35
|
+
await this.browser.sendMessage(task);
|
|
36
|
+
|
|
37
|
+
logger.info('Waiting for response...');
|
|
38
|
+
const answer = await this.browser.waitForResponse();
|
|
39
|
+
|
|
40
|
+
const duration = Date.now() - startTime;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
question: task,
|
|
44
|
+
answer: answer || 'No response',
|
|
45
|
+
duration,
|
|
46
|
+
status: answer ? 'success' : 'error'
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
'run_task',
|
|
50
|
+
config.RESPONSE_TIMEOUT
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 降级:无崩溃处理器时使用原有逻辑
|
|
30
55
|
logger.info('Sending task to Yiyan...');
|
|
31
56
|
await this.browser.sendMessage(task);
|
|
32
57
|
|
package/src/browser.js
CHANGED
|
@@ -6,6 +6,7 @@ const { chromium } = require('playwright');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
const logger = require('./logger');
|
|
9
|
+
const CrashHandler = require('./stability/crash-handler');
|
|
9
10
|
|
|
10
11
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
12
|
// Selector banks — ordered by likelihood, with fallbacks
|
|
@@ -85,6 +86,7 @@ class YiyanBrowser {
|
|
|
85
86
|
this.context = null;
|
|
86
87
|
this.page = null;
|
|
87
88
|
this._closed = false;
|
|
89
|
+
this.crashHandler = null; // 崩溃处理器
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -122,12 +124,17 @@ class YiyanBrowser {
|
|
|
122
124
|
await this._navigate(config.YIYAN_URL);
|
|
123
125
|
await this._ensureLoggedIn();
|
|
124
126
|
|
|
127
|
+
// 注册崩溃监听
|
|
128
|
+
this.crashHandler = new CrashHandler(this);
|
|
129
|
+
this.crashHandler.attach();
|
|
130
|
+
|
|
125
131
|
logger.success('Browser ready!');
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
async close() {
|
|
129
135
|
if (this._closed) return;
|
|
130
136
|
this._closed = true;
|
|
137
|
+
// 崩溃处理器不再需要监听
|
|
131
138
|
try { await this.context?.close(); } catch {}
|
|
132
139
|
}
|
|
133
140
|
|
package/src/config.js
CHANGED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// src/stability/crash-handler.js — Simplified crash detection and recovery
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CrashHandler - 简化版崩溃处理器
|
|
9
|
+
*
|
|
10
|
+
* 功能:
|
|
11
|
+
* 1. 监听浏览器崩溃事件
|
|
12
|
+
* 2. 自动恢复(新建页面或重启浏览器)
|
|
13
|
+
* 3. 操作重试机制
|
|
14
|
+
* 4. 冷却期防止无限重启
|
|
15
|
+
*/
|
|
16
|
+
class CrashHandler {
|
|
17
|
+
constructor(browser) {
|
|
18
|
+
this.browser = browser;
|
|
19
|
+
this.crashCount = 0; // 连续崩溃计数
|
|
20
|
+
this.lastCrashTime = 0; // 上次崩溃时间
|
|
21
|
+
this.maxRetries = config.MAX_RETRIES || 3;
|
|
22
|
+
this.cooldownMs = config.COOLDOWN_MS || 10000;
|
|
23
|
+
this._recoveryPromise = null; // 正在进行的恢复
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── 注册崩溃监听 ──
|
|
27
|
+
attach() {
|
|
28
|
+
if (!this.browser.context || !this.browser.page) {
|
|
29
|
+
logger.warn('[CrashHandler] Browser not ready, cannot attach listeners');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 浏览器 context 关闭
|
|
34
|
+
this.browser.context.on('close', () => {
|
|
35
|
+
this._onCrash('context_closed');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 页面崩溃
|
|
39
|
+
this.browser.page.on('crash', () => {
|
|
40
|
+
this._onCrash('page_crash');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 页面断开
|
|
44
|
+
this.browser.page.on('disconnected', () => {
|
|
45
|
+
this._onCrash('disconnected');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
logger.dim('[CrashHandler] Listeners attached');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── 崩溃事件处理 ──
|
|
52
|
+
_onCrash(type) {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
|
|
55
|
+
// 冷却期检查:短时间内重复崩溃不处理
|
|
56
|
+
if (now - this.lastCrashTime < this.cooldownMs) {
|
|
57
|
+
logger.warn(`[CrashHandler] ${type} ignored (cooldown active, ${Math.round((this.cooldownMs - (now - this.lastCrashTime)) / 1000)}s remaining)`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.crashCount++;
|
|
62
|
+
this.lastCrashTime = now;
|
|
63
|
+
|
|
64
|
+
logger.error(`[CrashHandler] Crash detected: ${type} (count: ${this.crashCount}/${this.maxRetries})`);
|
|
65
|
+
|
|
66
|
+
// 超过最大重试次数
|
|
67
|
+
if (this.crashCount > this.maxRetries) {
|
|
68
|
+
logger.error(`[CrashHandler] Max retries (${this.maxRetries}) exceeded, stopping auto-recovery`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 触发恢复(异步,不阻塞)
|
|
73
|
+
this._recover(type).catch(err => {
|
|
74
|
+
logger.error(`[CrashHandler] Recovery error: ${err.message}`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 恢复逻辑 ──
|
|
79
|
+
async _recover(type) {
|
|
80
|
+
// 防止并发恢复
|
|
81
|
+
if (this._recoveryPromise) {
|
|
82
|
+
logger.dim('[CrashHandler] Recovery already in progress, waiting...');
|
|
83
|
+
return this._recoveryPromise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this._recoveryPromise = this._doRecovery(type);
|
|
87
|
+
try {
|
|
88
|
+
await this._recoveryPromise;
|
|
89
|
+
} finally {
|
|
90
|
+
this._recoveryPromise = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async _doRecovery(type) {
|
|
95
|
+
logger.info(`[CrashHandler] Starting recovery for: ${type}`);
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (type === 'page_crash') {
|
|
100
|
+
// 页面崩溃 → 新建页面
|
|
101
|
+
logger.dim('[CrashHandler] Strategy: create new page');
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await this.browser.page.close();
|
|
105
|
+
} catch {
|
|
106
|
+
// 页面可能已经关闭
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.browser.page = await this.browser.context.newPage();
|
|
110
|
+
await this.browser.page.goto(config.YIYAN_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
111
|
+
await this.browser.page.waitForTimeout(500);
|
|
112
|
+
|
|
113
|
+
} else {
|
|
114
|
+
// 其他崩溃 → 完全重启浏览器
|
|
115
|
+
logger.dim('[CrashHandler] Strategy: full browser restart');
|
|
116
|
+
|
|
117
|
+
// 先关闭
|
|
118
|
+
try {
|
|
119
|
+
await this.browser.close();
|
|
120
|
+
} catch {
|
|
121
|
+
// 可能已经关闭
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 重置状态
|
|
125
|
+
this.browser._closed = false;
|
|
126
|
+
this.browser.context = null;
|
|
127
|
+
this.browser.page = null;
|
|
128
|
+
|
|
129
|
+
// 等待一下再重启
|
|
130
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
131
|
+
|
|
132
|
+
// 重新启动
|
|
133
|
+
await this.browser.launch();
|
|
134
|
+
|
|
135
|
+
// 重新注册监听
|
|
136
|
+
this.attach();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 恢复成功,重置崩溃计数
|
|
140
|
+
this.crashCount = 0;
|
|
141
|
+
const duration = Date.now() - startTime;
|
|
142
|
+
logger.success(`[CrashHandler] Recovery successful (${duration}ms)`);
|
|
143
|
+
|
|
144
|
+
return { success: true, duration };
|
|
145
|
+
|
|
146
|
+
} catch (err) {
|
|
147
|
+
logger.error(`[CrashHandler] Recovery failed: ${err.message}`);
|
|
148
|
+
return { success: false, error: err.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 操作包装器(带重试)──
|
|
153
|
+
async wrap(operation, operationName = 'operation', timeoutMs = null) {
|
|
154
|
+
const maxTimeout = timeoutMs || config.RESPONSE_TIMEOUT || 180000;
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
let attempt = 0;
|
|
157
|
+
|
|
158
|
+
while (Date.now() - startTime < maxTimeout) {
|
|
159
|
+
attempt++;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// 检查浏览器状态
|
|
163
|
+
if (!this.browser.page || this.browser.page.isClosed()) {
|
|
164
|
+
logger.warn(`[CrashHandler] Browser disconnected before ${operationName}`);
|
|
165
|
+
await this._recover('disconnected');
|
|
166
|
+
|
|
167
|
+
if (!this.browser.page || this.browser.page.isClosed()) {
|
|
168
|
+
throw new Error('Browser recovery failed');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 执行操作
|
|
173
|
+
const result = await operation();
|
|
174
|
+
|
|
175
|
+
// 成功,重置崩溃计数
|
|
176
|
+
if (attempt > 1) {
|
|
177
|
+
this.crashCount = 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const elapsed = Date.now() - startTime;
|
|
184
|
+
const msg = err.message?.toLowerCase() || '';
|
|
185
|
+
|
|
186
|
+
logger.warn(`[CrashHandler] ${operationName} failed (attempt ${attempt}): ${err.message}`);
|
|
187
|
+
|
|
188
|
+
// 判断是否值得重试
|
|
189
|
+
const shouldRetry =
|
|
190
|
+
elapsed < maxTimeout - 5000 && // 还有足够时间
|
|
191
|
+
this.crashCount <= this.maxRetries && // 未超过最大崩溃次数
|
|
192
|
+
(
|
|
193
|
+
msg.includes('disconnected') ||
|
|
194
|
+
msg.includes('crash') ||
|
|
195
|
+
msg.includes('closed') ||
|
|
196
|
+
msg.includes('timeout') ||
|
|
197
|
+
msg.includes('network') ||
|
|
198
|
+
msg.includes('socket') ||
|
|
199
|
+
msg.includes('navigation') ||
|
|
200
|
+
err.name === 'TimeoutError'
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!shouldRetry) {
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 等待后重试
|
|
208
|
+
const retryDelay = Math.min(2000 * attempt, 10000); // 递增延迟,最多10秒
|
|
209
|
+
logger.dim(`[CrashHandler] Retrying ${operationName} in ${retryDelay}ms...`);
|
|
210
|
+
await new Promise(r => setTimeout(r, retryDelay));
|
|
211
|
+
|
|
212
|
+
// 尝试恢复浏览器
|
|
213
|
+
if (msg.includes('disconnected') || msg.includes('crash') || msg.includes('closed')) {
|
|
214
|
+
await this._recover('disconnected');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new Error(`${operationName} timed out after ${Math.round((Date.now() - startTime) / 1000)}s`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── 健康检查 ──
|
|
223
|
+
async healthCheck() {
|
|
224
|
+
if (!this.browser.page) {
|
|
225
|
+
return { healthy: false, reason: 'no_page' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.browser.page.isClosed()) {
|
|
229
|
+
return { healthy: false, reason: 'page_closed' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// 尝试执行简单 JS 检测页面是否响应
|
|
234
|
+
await this.browser.page.evaluate(() => Date.now());
|
|
235
|
+
return { healthy: true };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return { healthy: false, reason: err.message };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 状态报告 ──
|
|
242
|
+
getStatus() {
|
|
243
|
+
return {
|
|
244
|
+
crashCount: this.crashCount,
|
|
245
|
+
lastCrashTime: this.lastCrashTime,
|
|
246
|
+
maxRetries: this.maxRetries,
|
|
247
|
+
cooldownMs: this.cooldownMs,
|
|
248
|
+
cooldownRemaining: Math.max(0, this.cooldownMs - (Date.now() - this.lastCrashTime)),
|
|
249
|
+
recovering: this._recoveryPromise !== null,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── 重置崩溃计数(手动干预后调用)──
|
|
254
|
+
reset() {
|
|
255
|
+
this.crashCount = 0;
|
|
256
|
+
this.lastCrashTime = 0;
|
|
257
|
+
logger.info('[CrashHandler] Crash count reset');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = CrashHandler;
|