yiyan-browser-agent 1.5.3 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yiyan-browser-agent",
3
- "version": "1.5.3",
3
+ "version": "1.6.1",
4
4
  "description": "AI coding agent powered by Yiyan (文心一言) via browser automation — no API key needed",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
@@ -24,6 +24,10 @@ const defaults = {
24
24
  // Output
25
25
  MAX_OUTPUT_LENGTH : 8_000,
26
26
  DEBUG : false,
27
+
28
+ // CrashHandler - 崩溃恢复配置
29
+ MAX_RETRIES : 3, // 连续崩溃最大重试次数
30
+ COOLDOWN_MS : 10000, // 冷却期 (毫秒),防止无限重启
27
31
  };
28
32
 
29
33
  // ─────────────────────────────────────────────
package/src/server.js CHANGED
@@ -6,6 +6,7 @@ const http = require('http');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
+ const config = require('./config');
9
10
  const TaskQueue = require('./task-queue');
10
11
 
11
12
  const DEFAULT_PORT = 9527;
@@ -202,18 +203,31 @@ class AgentServer {
202
203
 
203
204
  logger.info(`[Remote Task] ${taskText}`);
204
205
 
206
+ // 使用 crashHandler.wrap 包装整个执行流程(包括 newChat)
207
+ const crashHandler = this.agent.browser.crashHandler;
208
+
205
209
  try {
206
- // 开启新对话
207
- if (newChat) {
208
- await this.agent.browser.newChat();
210
+ if (crashHandler) {
211
+ const result = await crashHandler.wrap(
212
+ async () => {
213
+ if (newChat) {
214
+ await this.agent.browser.newChat();
215
+ }
216
+ return await this.agent.run(taskText);
217
+ },
218
+ 'executeTask',
219
+ config.RESPONSE_TIMEOUT * 2 // 给恢复留出时间
220
+ );
221
+ this.taskQueue.completeCurrentTask(result);
222
+ } else {
223
+ // 降级:无 crashHandler
224
+ if (newChat) {
225
+ await this.agent.browser.newChat();
226
+ }
227
+ const result = await this.agent.run(taskText);
228
+ this.taskQueue.completeCurrentTask(result);
209
229
  }
210
230
 
211
- // 执行任务
212
- const result = await this.agent.run(taskText);
213
-
214
- // 完成任务(通知队列,触发 resolve)
215
- this.taskQueue.completeCurrentTask(result);
216
-
217
231
  } catch (err) {
218
232
  logger.error(`[Remote Task Error] ${err.message}`);
219
233
  this.taskQueue.errorCurrentTask(err);
@@ -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;