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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yiyan-browser-agent",
3
- "version": "1.0.18",
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
  }
@@ -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
@@ -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 = require('./agent');
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
- const shutdown = async (code = 0, closeBrowser = true) => {
154
- if (closeBrowser) {
155
- logger.info('\nShutting down...');
156
- try { await agent.shutdown(); } catch {}
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, true));
162
- process.on('SIGTERM', () => shutdown(0, true));
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, true); // Error: close browser
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, true); // Error: close browser
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); // Interactive mode exit: close browser
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
- // Single task mode: keep browser open if success, close on error
208
- await shutdown(0, result.status === 'error');
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({