yiyan-browser-agent 1.4.7 → 1.4.9

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/README.md CHANGED
@@ -108,6 +108,22 @@ yiyan-agent --interactive
108
108
  # Run on a specific project
109
109
  ya --dir ~/projects/my-app "refactor all callbacks to async/await"
110
110
 
111
+ ### Process Communication (v1.4.8+)
112
+
113
+ When interactive mode (`-i`) is running, other `yiyan-agent` processes automatically detect and forward tasks to it, avoiding repeated browser startup:
114
+
115
+ ```bash
116
+ # Terminal 1: Start interactive mode (acts as server)
117
+ yiyan-agent -i
118
+ # → Server listening on port 9527
119
+
120
+ # Terminal 2: Send task (forwarded to server, no new browser)
121
+ yiyan-agent "北京天气,15个字"
122
+ # → Found running server, forwarding task...
123
+ ```
124
+
125
+ Lock file: `~/.yiyan-agent/server.lock`
126
+
111
127
  # Debug mode (shows what Yiyan is actually outputting)
112
128
  ya --debug "build a calculator"
113
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yiyan-browser-agent",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
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
@@ -4,11 +4,13 @@
4
4
  const config = require('./config');
5
5
  const logger = require('./logger');
6
6
  const YiyanBrowser = require('./browser');
7
+ const { AgentServer } = require('./server');
7
8
 
8
9
  class YiyanAgent {
9
10
  constructor(options = {}) {
10
11
  this.browser = new YiyanBrowser();
11
12
  this.options = options;
13
+ this.server = null; // TCP server for accepting remote tasks
12
14
  }
13
15
 
14
16
  async init() {
@@ -16,6 +18,9 @@ class YiyanAgent {
16
18
  }
17
19
 
18
20
  async shutdown() {
21
+ if (this.server) {
22
+ await this.server.stop();
23
+ }
19
24
  await this.browser.close();
20
25
  }
21
26
 
@@ -39,10 +44,22 @@ class YiyanAgent {
39
44
  }
40
45
 
41
46
  async runInteractive() {
47
+ // 启动 TCP 服务,接收其他进程的任务
48
+ this.server = new AgentServer(this);
49
+ try {
50
+ await this.server.start();
51
+ logger.info(`Server listening on port ${this.server.port} — other processes can send tasks here`);
52
+ } catch (err) {
53
+ logger.warn(`Failed to start server: ${err.message}`);
54
+ // 继续运行,只是不能接收远程任务
55
+ }
56
+
42
57
  const readline = require('readline');
43
58
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
44
59
  const ask = () => new Promise(resolve => rl.question('', resolve));
45
60
 
61
+ logger.info('Interactive mode — type task and press Enter (exit/quit/q to quit)');
62
+
46
63
  while (true) {
47
64
  const task = (await ask()).trim();
48
65
  if (!task) continue;
package/src/client.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // src/client.js — TCP client for sending tasks to running server
3
+ 'use strict';
4
+
5
+ const net = require('net');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const { DEFAULT_PORT, LOCK_FILE } = require('./server');
11
+
12
+ class AgentClient {
13
+ constructor() {
14
+ this.port = DEFAULT_PORT;
15
+ }
16
+
17
+ // 检查是否有服务器运行
18
+ isServerAvailable() {
19
+ if (fs.existsSync(LOCK_FILE)) {
20
+ try {
21
+ const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8'));
22
+ // 检查进程是否活着
23
+ try {
24
+ process.kill(lock.pid, 0);
25
+ this.port = lock.port || DEFAULT_PORT;
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ return false;
35
+ }
36
+
37
+ // 发送任务到服务器
38
+ async sendTask(task, options = {}) {
39
+ return new Promise((resolve, reject) => {
40
+ const socket = net.connect(this.port, 'localhost');
41
+ const timeout = options.timeout || 180000; // 3分钟
42
+
43
+ let data = '';
44
+ let timer = setTimeout(() => {
45
+ socket.destroy();
46
+ reject(new Error('Connection timeout'));
47
+ }, timeout);
48
+
49
+ socket.on('connect', () => {
50
+ clearTimeout(timer);
51
+ // 发送任务
52
+ const request = JSON.stringify({
53
+ task,
54
+ newChat: options.newChat !== false
55
+ });
56
+ socket.end(request);
57
+ });
58
+
59
+ socket.on('data', (chunk) => {
60
+ data += chunk.toString();
61
+ });
62
+
63
+ socket.on('end', () => {
64
+ try {
65
+ const result = JSON.parse(data);
66
+ resolve(result);
67
+ } catch (err) {
68
+ reject(new Error(`Invalid response: ${data}`));
69
+ }
70
+ });
71
+
72
+ socket.on('error', (err) => {
73
+ clearTimeout(timer);
74
+ reject(err);
75
+ });
76
+ });
77
+ }
78
+ }
79
+
80
+ module.exports = AgentClient;
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ const fs = require('fs');
7
7
  const config = require('./config');
8
8
  const logger = require('./logger');
9
9
  const YiyanAgent = require('./agent');
10
+ const AgentClient = require('./client');
10
11
 
11
12
  function parseArgs(argv) {
12
13
  const args = argv.slice(2);
@@ -32,13 +33,19 @@ yiyan-agent — AI Agent via Yiyan Browser
32
33
 
33
34
  USAGE
34
35
  yiyan-agent "任务" # 执行任务,只输出JSON
35
- yiyan-agent -i # 交互模式(用于登录)
36
+ yiyan-agent -i # 交互模式(启动服务器,接收远程任务)
36
37
 
37
38
  OPTIONS
38
- -i, --interactive 交互模式
39
+ -i, --interactive 交互模式(启动 TCP 服务,端口 9527)
39
40
  --show-browser 显示浏览器
40
41
  --debug 调试模式
41
42
  -h, --help 帮助
43
+
44
+ 进程间通信
45
+ 当交互模式运行时,其他 yiyan-agent 进程会自动检测并转发任务,
46
+ 避免每次重新启动浏览器。
47
+
48
+ 锁文件: ~/.yiyan-agent/server.lock
42
49
  `);
43
50
  }
44
51
 
@@ -101,8 +108,25 @@ async function main() {
101
108
  if (opts.interactive) {
102
109
  await agent.runInteractive();
103
110
  } else {
111
+ // 单任务模式:先检查是否有服务器运行
112
+ const client = new AgentClient();
113
+
114
+ if (client.isServerAvailable()) {
115
+ // 有服务器运行,转发任务
116
+ logger.info(`Found running server, forwarding task...`);
117
+ try {
118
+ const result = await client.sendTask(opts.task);
119
+ console.log(JSON.stringify(result, null, 2));
120
+ // 直接退出,不关闭浏览器(服务器管理)
121
+ process.exit(0);
122
+ } catch (err) {
123
+ // 转发失败,fallback 到本地执行
124
+ logger.warn(`Server connection failed: ${err.message}, running locally...`);
125
+ }
126
+ }
127
+
128
+ // 无服务器或转发失败,本地执行
104
129
  const result = await agent.run(opts.task);
105
- // 单任务:只输出JSON
106
130
  console.log(JSON.stringify(result, null, 2));
107
131
  }
108
132
  } catch (err) {
package/src/server.js ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // src/server.js — TCP server for accepting tasks from other processes
3
+ 'use strict';
4
+
5
+ const net = require('net');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const DEFAULT_PORT = 9527;
11
+ const LOCK_FILE = path.join(os.homedir(), '.yiyan-agent', 'server.lock');
12
+
13
+ class AgentServer {
14
+ constructor(agent, port = DEFAULT_PORT) {
15
+ this.agent = agent;
16
+ this.port = port;
17
+ this.server = null;
18
+ }
19
+
20
+ async start() {
21
+ // 检查是否已有服务器运行
22
+ if (this._isServerRunning()) {
23
+ throw new Error(`Server already running on port ${this.port}`);
24
+ }
25
+
26
+ this.server = net.createServer((socket) => {
27
+ this._handleConnection(socket);
28
+ });
29
+
30
+ await new Promise((resolve, reject) => {
31
+ this.server.listen(this.port, (err) => {
32
+ if (err) reject(err);
33
+ else resolve();
34
+ });
35
+ });
36
+
37
+ // 写锁文件
38
+ this._writeLockFile();
39
+
40
+ return this.port;
41
+ }
42
+
43
+ async stop() {
44
+ if (this.server) {
45
+ await new Promise((resolve) => {
46
+ this.server.close(resolve);
47
+ });
48
+ this.server = null;
49
+ }
50
+ this._removeLockFile();
51
+ }
52
+
53
+ _isServerRunning() {
54
+ if (fs.existsSync(LOCK_FILE)) {
55
+ try {
56
+ const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8'));
57
+ // 检查进程是否还活着
58
+ try {
59
+ process.kill(lock.pid, 0);
60
+ return true;
61
+ } catch {
62
+ // 进程已死,清理锁文件
63
+ this._removeLockFile();
64
+ return false;
65
+ }
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ _writeLockFile() {
74
+ const dir = path.dirname(LOCK_FILE);
75
+ if (!fs.existsSync(dir)) {
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ }
78
+ fs.writeFileSync(LOCK_FILE, JSON.stringify({
79
+ pid: process.pid,
80
+ port: this.port,
81
+ started: Date.now()
82
+ }));
83
+ }
84
+
85
+ _removeLockFile() {
86
+ try {
87
+ if (fs.existsSync(LOCK_FILE)) {
88
+ fs.unlinkSync(LOCK_FILE);
89
+ }
90
+ } catch {}
91
+ }
92
+
93
+ async _handleConnection(socket) {
94
+ let data = '';
95
+
96
+ socket.on('data', (chunk) => {
97
+ data += chunk.toString();
98
+ });
99
+
100
+ socket.on('end', async () => {
101
+ try {
102
+ const request = JSON.parse(data);
103
+ const result = await this._executeTask(request);
104
+ socket.end(JSON.stringify(result));
105
+ } catch (err) {
106
+ socket.end(JSON.stringify({
107
+ status: 'error',
108
+ answer: `Server error: ${err.message}`
109
+ }));
110
+ }
111
+ });
112
+
113
+ socket.on('error', (err) => {
114
+ // 连接错误,忽略
115
+ });
116
+ }
117
+
118
+ async _executeTask(request) {
119
+ const { task, newChat = true } = request;
120
+
121
+ if (!task) {
122
+ return { status: 'error', answer: 'No task provided' };
123
+ }
124
+
125
+ try {
126
+ // 可选:开启新对话
127
+ if (newChat) {
128
+ await this.agent.browser.newChat();
129
+ }
130
+
131
+ const result = await this.agent.run(task);
132
+ return result;
133
+ } catch (err) {
134
+ return {
135
+ question: task,
136
+ answer: `Error: ${err.message}`,
137
+ duration: 0,
138
+ status: 'error'
139
+ };
140
+ }
141
+ }
142
+ }
143
+
144
+ module.exports = { AgentServer, DEFAULT_PORT, LOCK_FILE };