yiyan-browser-agent 1.0.19 → 1.0.21
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 +0 -9
- package/src/browser-manager.js +95 -39
- package/src/index.js +11 -39
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -51,8 +51,6 @@ class YiyanAgent {
|
|
|
51
51
|
try {
|
|
52
52
|
await this.browser.sendMessage(task);
|
|
53
53
|
} catch (err) {
|
|
54
|
-
// Input error - mark for restart
|
|
55
|
-
BrowserManager.markError();
|
|
56
54
|
throw err;
|
|
57
55
|
}
|
|
58
56
|
|
|
@@ -121,13 +119,6 @@ class YiyanAgent {
|
|
|
121
119
|
status = 'incomplete';
|
|
122
120
|
}
|
|
123
121
|
|
|
124
|
-
// Mark browser status for reuse
|
|
125
|
-
if (status === 'success') {
|
|
126
|
-
BrowserManager.markSuccess(); // Keep browser open for next task
|
|
127
|
-
} else {
|
|
128
|
-
BrowserManager.markError(); // Restart browser next time
|
|
129
|
-
}
|
|
130
|
-
|
|
131
122
|
this._running = false;
|
|
132
123
|
|
|
133
124
|
// Return JSON
|
package/src/browser-manager.js
CHANGED
|
@@ -1,83 +1,139 @@
|
|
|
1
1
|
// src/browser-manager.js — Singleton browser instance manager for reuse
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { chromium } = require('playwright');
|
|
4
5
|
const YiyanBrowser = require('./browser');
|
|
5
6
|
const logger = require('./logger');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
6
10
|
|
|
7
|
-
//
|
|
11
|
+
// CDP port file path
|
|
12
|
+
const CDP_PORT_FILE = path.join(os.homedir(), '.yiyan-agent', 'cdp-port.json');
|
|
13
|
+
const CDP_PORT = 9222;
|
|
14
|
+
|
|
15
|
+
// Global instance for current process
|
|
8
16
|
let _instance = null;
|
|
9
|
-
let _initialized = false;
|
|
10
|
-
let _hasError = false;
|
|
11
17
|
|
|
12
18
|
class BrowserManager {
|
|
13
19
|
/**
|
|
14
|
-
* Get
|
|
15
|
-
* If previous run had error, will restart the browser
|
|
20
|
+
* Get browser instance - connect to existing or launch new
|
|
16
21
|
*/
|
|
17
22
|
static async getInstance() {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
_instance = null;
|
|
25
|
-
_initialized = false;
|
|
26
|
-
_hasError = false;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Create new instance if needed
|
|
30
|
-
if (!_instance) {
|
|
31
|
-
_instance = new YiyanBrowser();
|
|
32
|
-
await _instance.launch();
|
|
33
|
-
_initialized = true;
|
|
34
|
-
logger.success('Browser initialized (will be reused)');
|
|
23
|
+
// Try to connect to existing browser first
|
|
24
|
+
const existing = await this._tryConnectExisting();
|
|
25
|
+
if (existing) {
|
|
26
|
+
logger.success('Connected to existing browser (reused)');
|
|
27
|
+
_instance = existing;
|
|
28
|
+
return _instance;
|
|
35
29
|
}
|
|
36
30
|
|
|
31
|
+
// Launch new browser with CDP port
|
|
32
|
+
logger.info('Launching new browser...');
|
|
33
|
+
_instance = await this._launchWithCDP();
|
|
37
34
|
return _instance;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
|
-
*
|
|
38
|
+
* Try to connect to existing browser via CDP
|
|
42
39
|
*/
|
|
43
|
-
static
|
|
44
|
-
|
|
40
|
+
static async _tryConnectExisting() {
|
|
41
|
+
try {
|
|
42
|
+
// Check if CDP port file exists
|
|
43
|
+
if (!fs.existsSync(CDP_PORT_FILE)) return null;
|
|
44
|
+
|
|
45
|
+
const portInfo = JSON.parse(fs.readFileSync(CDP_PORT_FILE, 'utf8'));
|
|
46
|
+
const browserURL = `http://localhost:${portInfo.port || CDP_PORT}`;
|
|
47
|
+
|
|
48
|
+
// Try to connect
|
|
49
|
+
const context = await chromium.connectOverCDP(browserURL, {
|
|
50
|
+
timeout: 3000
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create wrapper instance
|
|
54
|
+
const wrapper = new YiyanBrowser();
|
|
55
|
+
wrapper.context = context;
|
|
56
|
+
wrapper.page = context.pages()[0] || await context.newPage();
|
|
57
|
+
wrapper._closed = false;
|
|
58
|
+
wrapper._connected = true; // Mark as connected, not owned
|
|
59
|
+
|
|
60
|
+
return wrapper;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// Connection failed, remove stale port file
|
|
63
|
+
try { fs.unlinkSync(CDP_PORT_FILE); } catch {}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
/**
|
|
48
|
-
*
|
|
69
|
+
* Launch new browser with CDP port for future connections
|
|
49
70
|
*/
|
|
50
|
-
static
|
|
51
|
-
|
|
71
|
+
static async _launchWithCDP() {
|
|
72
|
+
const wrapper = new YiyanBrowser();
|
|
73
|
+
|
|
74
|
+
// Launch browser with CDP port
|
|
75
|
+
const sessionDir = path.join(os.homedir(), '.yiyan-agent', 'session');
|
|
76
|
+
const context = await chromium.launchPersistentContext(sessionDir, {
|
|
77
|
+
headless: false,
|
|
78
|
+
viewport: { width: 1280, height: 900 },
|
|
79
|
+
args: [
|
|
80
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
81
|
+
'--disable-blink-features=AutomationControlled',
|
|
82
|
+
'--no-first-run',
|
|
83
|
+
'--no-sandbox',
|
|
84
|
+
],
|
|
85
|
+
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
wrapper.context = context;
|
|
89
|
+
wrapper.page = context.pages()[0] || await context.newPage();
|
|
90
|
+
wrapper._closed = false;
|
|
91
|
+
wrapper._connected = false; // Owned by this process
|
|
92
|
+
|
|
93
|
+
// Save CDP port info
|
|
94
|
+
fs.writeFileSync(CDP_PORT_FILE, JSON.stringify({
|
|
95
|
+
port: CDP_PORT,
|
|
96
|
+
launchedAt: Date.now()
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
// Navigate to Yiyan
|
|
100
|
+
await wrapper._navigate('https://yiyan.baidu.com/');
|
|
101
|
+
|
|
102
|
+
return wrapper;
|
|
52
103
|
}
|
|
53
104
|
|
|
54
105
|
/**
|
|
55
|
-
*
|
|
106
|
+
* New chat session
|
|
56
107
|
*/
|
|
57
|
-
static
|
|
58
|
-
|
|
108
|
+
static async newChat() {
|
|
109
|
+
if (_instance) {
|
|
110
|
+
await _instance.newChat();
|
|
111
|
+
}
|
|
59
112
|
}
|
|
60
113
|
|
|
61
114
|
/**
|
|
62
|
-
*
|
|
115
|
+
* Close browser (only if we own it)
|
|
63
116
|
*/
|
|
64
117
|
static async close() {
|
|
65
|
-
if (_instance) {
|
|
118
|
+
if (_instance && !_instance._connected) {
|
|
119
|
+
// We own the browser, close it
|
|
66
120
|
try {
|
|
67
121
|
await _instance.close();
|
|
122
|
+
fs.unlinkSync(CDP_PORT_FILE);
|
|
68
123
|
} catch {}
|
|
69
|
-
_instance = null;
|
|
70
|
-
_initialized = false;
|
|
71
|
-
_hasError = false;
|
|
72
124
|
}
|
|
125
|
+
// If connected to existing browser, don't close it
|
|
126
|
+
_instance = null;
|
|
73
127
|
}
|
|
74
128
|
|
|
75
129
|
/**
|
|
76
|
-
*
|
|
130
|
+
* Force close (for cleanup on error)
|
|
77
131
|
*/
|
|
78
|
-
static async
|
|
132
|
+
static async forceClose() {
|
|
79
133
|
if (_instance) {
|
|
80
|
-
await _instance.
|
|
134
|
+
try { await _instance.close(); } catch {}
|
|
135
|
+
try { fs.unlinkSync(CDP_PORT_FILE); } catch {}
|
|
136
|
+
_instance = null;
|
|
81
137
|
}
|
|
82
138
|
}
|
|
83
139
|
}
|
package/src/index.js
CHANGED
|
@@ -151,25 +151,24 @@ async function main() {
|
|
|
151
151
|
const agent = new YiyanAgent({ saveLog: opts.saveLog });
|
|
152
152
|
|
|
153
153
|
// ── Graceful shutdown handler ──────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
154
|
+
// Only close browser on explicit exit (Ctrl+C, error)
|
|
155
|
+
const shutdown = async (code = 0) => {
|
|
156
|
+
logger.info('\nShutting down...');
|
|
157
|
+
try { await BrowserManager.forceClose(); } catch {}
|
|
159
158
|
process.exit(code);
|
|
160
159
|
};
|
|
161
160
|
|
|
162
|
-
process.on('SIGINT', () => shutdown(0
|
|
163
|
-
process.on('SIGTERM', () => shutdown(0
|
|
161
|
+
process.on('SIGINT', () => shutdown(0));
|
|
162
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
164
163
|
process.on('uncaughtException', async err => {
|
|
165
164
|
logger.error(`Uncaught error: ${err.message}`);
|
|
166
165
|
if (config.DEBUG) console.error(err.stack);
|
|
167
|
-
await shutdown(1
|
|
166
|
+
await shutdown(1);
|
|
168
167
|
});
|
|
169
168
|
process.on('unhandledRejection', async reason => {
|
|
170
169
|
logger.error(`Unhandled rejection: ${reason}`);
|
|
171
170
|
if (config.DEBUG) console.error(reason);
|
|
172
|
-
await shutdown(1
|
|
171
|
+
await shutdown(1);
|
|
173
172
|
});
|
|
174
173
|
|
|
175
174
|
// ── Calibrate mode ─────────────────────────────────────────────────────────
|
|
@@ -205,36 +204,9 @@ async function main() {
|
|
|
205
204
|
} else {
|
|
206
205
|
const result = await agent.run(opts.task);
|
|
207
206
|
console.log(JSON.stringify(result, null, 2));
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
logger.info('Browser kept open. Enter next task or Ctrl+C to exit...');
|
|
212
|
-
const readline = require('readline');
|
|
213
|
-
const rl = readline.createInterface({
|
|
214
|
-
input : process.stdin,
|
|
215
|
-
output : process.stdout,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Wait for next task
|
|
219
|
-
while (true) {
|
|
220
|
-
const nextTask = await new Promise(resolve => rl.question('', resolve));
|
|
221
|
-
const trimmed = nextTask.trim();
|
|
222
|
-
|
|
223
|
-
if (!trimmed) continue;
|
|
224
|
-
if (['exit', 'quit', 'q'].includes(trimmed.toLowerCase())) break;
|
|
225
|
-
|
|
226
|
-
// Run next task with same browser
|
|
227
|
-
agent.conversation = new ConversationManager();
|
|
228
|
-
await BrowserManager.newChat();
|
|
229
|
-
const nextResult = await agent.run(trimmed);
|
|
230
|
-
console.log(JSON.stringify(nextResult, null, 2));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
rl.close();
|
|
234
|
-
await shutdown(0, true);
|
|
235
|
-
} else {
|
|
236
|
-
await shutdown(0, true); // Error: close browser
|
|
237
|
-
}
|
|
207
|
+
// Success: don't close browser, next process can connect to it
|
|
208
|
+
// Error: close browser
|
|
209
|
+
process.exit(result.status === 'error' ? 1 : 0);
|
|
238
210
|
}
|
|
239
211
|
} catch (err) {
|
|
240
212
|
console.log(JSON.stringify({
|