yiyan-browser-agent 1.0.18 → 1.0.20
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 +2 -2
- package/src/browser-manager.js +95 -39
- package/src/index.js +14 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yiyan-browser-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/readfor/yiyan-browser-agent"
|
|
42
|
+
"url": "git+https://github.com/readfor/yiyan-browser-agent.git"
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://github.com/readfor/yiyan-browser-agent#readme"
|
|
45
45
|
}
|
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
|
@@ -6,7 +6,8 @@ const path = require('path');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
const logger = require('./logger');
|
|
9
|
-
const YiyanAgent
|
|
9
|
+
const YiyanAgent = require('./agent');
|
|
10
|
+
const BrowserManager = require('./browser-manager');
|
|
10
11
|
|
|
11
12
|
// ─────────────────────────────────────────────
|
|
12
13
|
// Parse CLI arguments
|
|
@@ -150,25 +151,24 @@ async function main() {
|
|
|
150
151
|
const agent = new YiyanAgent({ saveLog: opts.saveLog });
|
|
151
152
|
|
|
152
153
|
// ── Graceful shutdown handler ──────────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
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 {}
|
|
158
158
|
process.exit(code);
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
process.on('SIGINT', () => shutdown(0
|
|
162
|
-
process.on('SIGTERM', () => shutdown(0
|
|
161
|
+
process.on('SIGINT', () => shutdown(0));
|
|
162
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
163
163
|
process.on('uncaughtException', async err => {
|
|
164
164
|
logger.error(`Uncaught error: ${err.message}`);
|
|
165
165
|
if (config.DEBUG) console.error(err.stack);
|
|
166
|
-
await shutdown(1
|
|
166
|
+
await shutdown(1);
|
|
167
167
|
});
|
|
168
168
|
process.on('unhandledRejection', async reason => {
|
|
169
169
|
logger.error(`Unhandled rejection: ${reason}`);
|
|
170
170
|
if (config.DEBUG) console.error(reason);
|
|
171
|
-
await shutdown(1
|
|
171
|
+
await shutdown(1);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
// ── Calibrate mode ─────────────────────────────────────────────────────────
|
|
@@ -200,12 +200,13 @@ async function main() {
|
|
|
200
200
|
try {
|
|
201
201
|
if (opts.interactive) {
|
|
202
202
|
await agent.runInteractive();
|
|
203
|
-
await shutdown(0, true);
|
|
203
|
+
await shutdown(0, true);
|
|
204
204
|
} else {
|
|
205
205
|
const result = await agent.run(opts.task);
|
|
206
206
|
console.log(JSON.stringify(result, null, 2));
|
|
207
|
-
//
|
|
208
|
-
|
|
207
|
+
// Success: don't close browser, next process can connect to it
|
|
208
|
+
// Error: close browser
|
|
209
|
+
process.exit(result.status === 'error' ? 1 : 0);
|
|
209
210
|
}
|
|
210
211
|
} catch (err) {
|
|
211
212
|
console.log(JSON.stringify({
|