yiyan-browser-agent 1.0.19 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yiyan-browser-agent",
3
- "version": "1.0.19",
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": {
@@ -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
- // Global singleton instance
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 or create browser instance (singleton)
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
- // If there was an error, restart the browser
19
- if (_hasError && _instance) {
20
- logger.info('Previous error detected, restarting browser...');
21
- try {
22
- await _instance.close();
23
- } catch {}
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
- * Mark that an error occurred (browser should be restarted next time)
38
+ * Try to connect to existing browser via CDP
42
39
  */
43
- static markError() {
44
- _hasError = true;
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
- * Mark that everything is working fine (keep browser open)
69
+ * Launch new browser with CDP port for future connections
49
70
  */
50
- static markSuccess() {
51
- _hasError = false;
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
- * Check if browser is currently initialized
106
+ * New chat session
56
107
  */
57
- static isInitialized() {
58
- return _initialized && _instance && !_instance._closed;
108
+ static async newChat() {
109
+ if (_instance) {
110
+ await _instance.newChat();
111
+ }
59
112
  }
60
113
 
61
114
  /**
62
- * Force close the browser (for shutdown)
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
- * Reset for new chat session
130
+ * Force close (for cleanup on error)
77
131
  */
78
- static async newChat() {
132
+ static async forceClose() {
79
133
  if (_instance) {
80
- await _instance.newChat();
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
- const shutdown = async (code = 0, closeBrowser = true) => {
155
- if (closeBrowser) {
156
- logger.info('\nShutting down...');
157
- try { await agent.shutdown(); } catch {}
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, true));
163
- process.on('SIGTERM', () => shutdown(0, true));
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, true); // Error: close browser
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, true); // Error: close browser
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
- // Success: keep process alive for next task (read from stdin)
210
- if (result.status === 'success') {
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({