yiyan-browser-agent 1.5.1 → 1.5.2

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.5.1",
3
+ "version": "1.5.2",
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/server.js CHANGED
@@ -6,6 +6,7 @@ const http = require('http');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
+ const TaskQueue = require('./task-queue');
9
10
 
10
11
  const DEFAULT_PORT = 9527;
11
12
  const LOCK_FILE = path.join(os.homedir(), '.yiyan-agent', 'server.lock');
@@ -15,6 +16,11 @@ class AgentServer {
15
16
  this.agent = agent;
16
17
  this.port = port;
17
18
  this.server = null;
19
+
20
+ // 创建任务队列,设置任务准备好时的回调
21
+ this.taskQueue = new TaskQueue((task) => {
22
+ this._executeTask(task);
23
+ });
18
24
  }
19
25
 
20
26
  async start() {
@@ -39,6 +45,9 @@ class AgentServer {
39
45
  }
40
46
 
41
47
  async stop() {
48
+ // 清空队列
49
+ this.taskQueue.clearQueue();
50
+
42
51
  if (this.server) {
43
52
  await new Promise((resolve) => {
44
53
  this.server.close(resolve);
@@ -98,60 +107,146 @@ class AgentServer {
98
107
 
99
108
  async _handleRequest(req, res) {
100
109
  const logger = require('./logger');
110
+ const url = new URL(req.url, `http://localhost:${this.port}`);
111
+
112
+ // ── API 路由 ─────────────────────────────────────────────
113
+
114
+ // GET /status - 获取队列状态
115
+ if (req.method === 'GET' && url.pathname === '/status') {
116
+ this._handleStatus(req, res);
117
+ return;
118
+ }
119
+
120
+ // GET /queue - 获取队列详情
121
+ if (req.method === 'GET' && url.pathname === '/queue') {
122
+ this._handleQueue(req, res);
123
+ return;
124
+ }
125
+
126
+ // GET /task/:id - 获取单个任务状态
127
+ if (req.method === 'GET' && url.pathname.startsWith('/task/')) {
128
+ const requestId = url.pathname.slice(6);
129
+ this._handleTaskStatus(req, res, requestId);
130
+ return;
131
+ }
132
+
133
+ // POST /task - 提交任务
134
+ if (req.method === 'POST' && url.pathname === '/task') {
135
+ this._handleTaskSubmit(req, res);
136
+ return;
137
+ }
101
138
 
102
- // 只接受 POST /task
103
- if (req.method !== 'POST') {
104
- res.writeHead(405, { 'Content-Type': 'application/json' });
105
- res.end(JSON.stringify({ status: 'error', answer: 'Method not allowed, use POST' }));
139
+ // GET / - 简单介绍
140
+ if (req.method === 'GET' && url.pathname === '/') {
141
+ res.writeHead(200, { 'Content-Type': 'application/json' });
142
+ res.end(JSON.stringify({
143
+ name: 'yiyan-agent',
144
+ version: '1.5.2',
145
+ endpoints: {
146
+ 'POST /task': '提交任务',
147
+ 'GET /status': '获取队列状态',
148
+ 'GET /queue': '获取队列详情',
149
+ 'GET /task/:id': '获取任务状态',
150
+ }
151
+ }));
106
152
  return;
107
153
  }
108
154
 
109
- // 读取请求体
155
+ // 其他请求 - 404
156
+ res.writeHead(404, { 'Content-Type': 'application/json' });
157
+ res.end(JSON.stringify({ status: 'error', answer: 'Not found' }));
158
+ }
159
+
160
+ // ── POST /task - 提交任务 ─────────────────────────────────────
161
+ _handleTaskSubmit(req, res) {
162
+ const logger = require('./logger');
110
163
  let body = '';
164
+
111
165
  req.on('data', chunk => { body += chunk.toString(); });
112
- req.on('end', async () => {
166
+
167
+ req.on('end', () => {
113
168
  try {
114
169
  const request = JSON.parse(body);
115
- const result = await this._executeTask(request);
116
- res.writeHead(200, { 'Content-Type': 'application/json' });
117
- res.end(JSON.stringify(result));
170
+ const { task, newChat = true } = request;
171
+
172
+ if (!task) {
173
+ res.writeHead(400, { 'Content-Type': 'application/json' });
174
+ res.end(JSON.stringify({ status: 'error', answer: 'No task provided' }));
175
+ return;
176
+ }
177
+
178
+ // 添加任务到队列,等待任务完成后自动调用 resolve 返回结果
179
+ this.taskQueue.addTask(request, (result) => {
180
+ res.writeHead(200, { 'Content-Type': 'application/json' });
181
+ res.end(JSON.stringify(result));
182
+ });
183
+
118
184
  } catch (err) {
119
185
  logger.error(`[HTTP Error] ${err.message}`);
120
186
  res.writeHead(400, { 'Content-Type': 'application/json' });
121
187
  res.end(JSON.stringify({ status: 'error', answer: `Bad request: ${err.message}` }));
122
188
  }
123
189
  });
124
- }
125
-
126
- async _executeTask(request) {
127
- const { task, newChat = true } = request;
128
190
 
129
- if (!task) {
130
- return { status: 'error', answer: 'No task provided' };
131
- }
191
+ req.on('error', (err) => {
192
+ res.writeHead(500, { 'Content-Type': 'application/json' });
193
+ res.end(JSON.stringify({ status: 'error', answer: `Request error: ${err.message}` }));
194
+ });
195
+ }
132
196
 
197
+ // ── 执行任务(由队列回调触发)─────────────────────────────────────
198
+ async _executeTask(task) {
199
+ const { data } = task;
200
+ const { task: taskText, newChat = true } = data;
133
201
  const logger = require('./logger');
134
- logger.info(`[Remote Task] ${task}`);
202
+
203
+ logger.info(`[Remote Task] ${taskText}`);
135
204
 
136
205
  try {
137
- // 可选:开启新对话
206
+ // 开启新对话
138
207
  if (newChat) {
139
208
  await this.agent.browser.newChat();
140
209
  }
141
210
 
142
- const result = await this.agent.run(task);
143
- logger.info(`[Remote Task Done] ${result.status} (${result.duration}ms)`);
144
- return result;
211
+ // 执行任务
212
+ const result = await this.agent.run(taskText);
213
+
214
+ // 完成任务(通知队列,触发 resolve)
215
+ this.taskQueue.completeCurrentTask(result);
216
+
145
217
  } catch (err) {
146
218
  logger.error(`[Remote Task Error] ${err.message}`);
147
- return {
148
- question: task,
149
- answer: `Error: ${err.message}`,
150
- duration: 0,
151
- status: 'error'
152
- };
219
+ this.taskQueue.errorCurrentTask(err);
153
220
  }
154
221
  }
222
+
223
+ // ── GET /status - 获取队列状态 ─────────────────────────────────────
224
+ _handleStatus(req, res) {
225
+ const status = this.taskQueue.getQueueStatus();
226
+ res.writeHead(200, { 'Content-Type': 'application/json' });
227
+ res.end(JSON.stringify(status, null, 2));
228
+ }
229
+
230
+ // ── GET /queue - 获取队列详情 ─────────────────────────────────────
231
+ _handleQueue(req, res) {
232
+ const status = this.taskQueue.getQueueStatus();
233
+ res.writeHead(200, { 'Content-Type': 'application/json' });
234
+ res.end(JSON.stringify(status, null, 2));
235
+ }
236
+
237
+ // ── GET /task/:id - 获取任务状态 ─────────────────────────────────────
238
+ _handleTaskStatus(req, res, requestId) {
239
+ const status = this.taskQueue.getTaskStatus(requestId);
240
+
241
+ if (!status) {
242
+ res.writeHead(404, { 'Content-Type': 'application/json' });
243
+ res.end(JSON.stringify({ status: 'error', answer: 'Task not found' }));
244
+ return;
245
+ }
246
+
247
+ res.writeHead(200, { 'Content-Type': 'application/json' });
248
+ res.end(JSON.stringify(status, null, 2));
249
+ }
155
250
  }
156
251
 
157
252
  module.exports = { AgentServer, DEFAULT_PORT, LOCK_FILE };
@@ -0,0 +1,296 @@
1
+ // src/task-queue.js — Task queue for handling concurrent requests
2
+ 'use strict';
3
+
4
+ const logger = require('./logger');
5
+
6
+ /**
7
+ * TaskQueue - 任务队列管理器
8
+ *
9
+ * 解决问题:
10
+ * 1. 多个客户端并发请求时,输入框竞争导致任务混淆
11
+ * 2. 响应返回给错误的客户端
12
+ *
13
+ * 解决方案:
14
+ * 1. 所有任务进入队列,串行处理
15
+ * 2. 每个任务绑定唯一ID,追踪响应归属
16
+ * 3. 提供状态查询API
17
+ */
18
+ class TaskQueue {
19
+ constructor(onTaskReady = null) {
20
+ this.queue = []; // 待处理任务队列
21
+ this.currentTask = null; // 当前正在处理的任务
22
+ this.isProcessing = false; // 是否正在处理
23
+ this.completedTasks = []; // 已完成任务(最近10个)
24
+ this.onTaskReady = onTaskReady; // 任务准备好执行时的回调
25
+ this.stats = {
26
+ totalProcessed: 0,
27
+ averageProcessTime: 0,
28
+ totalProcessTime: 0,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * 添加任务到队列
34
+ * @param {object} taskData - 任务数据 { task, newChat, provider }
35
+ * @param {function} resolve - Promise的resolve函数
36
+ * @returns {object} - { requestId, queuePosition, estimatedWait }
37
+ */
38
+ addTask(taskData, resolve) {
39
+ const requestId = this._generateRequestId();
40
+
41
+ const task = {
42
+ id: requestId,
43
+ data: taskData,
44
+ resolve,
45
+ status: 'pending',
46
+ addedAt: Date.now(),
47
+ startedAt: null,
48
+ completedAt: null,
49
+ };
50
+
51
+ this.queue.push(task);
52
+
53
+ const queuePosition = this.queue.length;
54
+ const estimatedWait = this._estimateWaitTime(queuePosition);
55
+
56
+ logger.info(`[Queue] 新任务 ${this._shortId(requestId)}: "${taskData.task.slice(0, 30)}..." (队列位置: ${queuePosition})`);
57
+
58
+ // 如果没有正在处理的任务,立即开始处理
59
+ if (!this.isProcessing) {
60
+ this._processNext();
61
+ }
62
+
63
+ return {
64
+ requestId,
65
+ queuePosition,
66
+ estimatedWait,
67
+ status: 'pending',
68
+ };
69
+ }
70
+
71
+ /**
72
+ * 处理下一个任务
73
+ */
74
+ _processNext() {
75
+ if (this.queue.length === 0) {
76
+ this.isProcessing = false;
77
+ this.currentTask = null;
78
+ logger.dim('[Queue] 队列已空,等待新任务...');
79
+ return;
80
+ }
81
+
82
+ this.isProcessing = true;
83
+ this.currentTask = this.queue.shift();
84
+ this.currentTask.status = 'processing';
85
+ this.currentTask.startedAt = Date.now();
86
+
87
+ const waitTime = this.currentTask.startedAt - this.currentTask.addedAt;
88
+ logger.info(`[Queue] 开始处理 ${this._shortId(this.currentTask.id)}: "${this.currentTask.data.task.slice(0, 30)}..." (等待了 ${waitTime}ms)`);
89
+
90
+ // 调用外部执行回调
91
+ if (this.onTaskReady) {
92
+ this.onTaskReady(this.currentTask);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 完成当前任务(由外部调用,传入执行结果)
98
+ * @param {object} result - 执行结果
99
+ */
100
+ completeCurrentTask(result) {
101
+ if (!this.currentTask) return;
102
+
103
+ this.currentTask.completedAt = Date.now();
104
+ this.currentTask.status = 'completed';
105
+
106
+ const processTime = this.currentTask.completedAt - this.currentTask.startedAt;
107
+ result.requestId = this.currentTask.id;
108
+ result.processTime = processTime;
109
+ result.waitTime = this.currentTask.startedAt - this.currentTask.addedAt;
110
+
111
+ // 更新统计
112
+ this._updateStats(processTime);
113
+
114
+ // 保存到已完成列表(保留最近10个)
115
+ this.completedTasks.unshift({
116
+ id: this.currentTask.id,
117
+ task: this.currentTask.data.task,
118
+ processTime,
119
+ status: result.status,
120
+ completedAt: this.currentTask.completedAt,
121
+ });
122
+ if (this.completedTasks.length > 10) {
123
+ this.completedTasks.pop();
124
+ }
125
+
126
+ // 返回结果给客户端
127
+ this.currentTask.resolve(result);
128
+
129
+ logger.info(`[Queue] ✓ 完成 ${this._shortId(this.currentTask.id)} (${processTime}ms)`);
130
+
131
+ // 处理下一个
132
+ this.currentTask = null;
133
+ this._processNext();
134
+ }
135
+
136
+ /**
137
+ * 当前任务出错
138
+ * @param {Error} err - 错误对象
139
+ */
140
+ errorCurrentTask(err) {
141
+ if (!this.currentTask) return;
142
+
143
+ this.currentTask.completedAt = Date.now();
144
+ this.currentTask.status = 'error';
145
+
146
+ const result = {
147
+ question: this.currentTask.data.task,
148
+ answer: `Error: ${err.message}`,
149
+ status: 'error',
150
+ requestId: this.currentTask.id,
151
+ processTime: this.currentTask.completedAt - this.currentTask.startedAt,
152
+ };
153
+
154
+ this.currentTask.resolve(result);
155
+
156
+ logger.error(`[Queue] ✗ 错误 ${this._shortId(this.currentTask.id)}: ${err.message}`);
157
+
158
+ this.currentTask = null;
159
+ this._processNext();
160
+ }
161
+
162
+ /**
163
+ * 获取任务状态
164
+ * @param {string} requestId - 任务ID
165
+ * @returns {object|null} - 任务状态
166
+ */
167
+ getTaskStatus(requestId) {
168
+ // 检查当前任务
169
+ if (this.currentTask && this.currentTask.id === requestId) {
170
+ return {
171
+ id: requestId,
172
+ task: this.currentTask.data.task,
173
+ status: 'processing',
174
+ addedAt: this.currentTask.addedAt,
175
+ startedAt: this.currentTask.startedAt,
176
+ elapsed: Date.now() - this.currentTask.startedAt,
177
+ };
178
+ }
179
+
180
+ // 检查队列中的任务
181
+ const pending = this.queue.find(t => t.id === requestId);
182
+ if (pending) {
183
+ return {
184
+ id: requestId,
185
+ task: pending.data.task,
186
+ status: 'pending',
187
+ addedAt: pending.addedAt,
188
+ queuePosition: this.queue.indexOf(pending) + 1,
189
+ waitTime: Date.now() - pending.addedAt,
190
+ };
191
+ }
192
+
193
+ // 检查已完成任务
194
+ const completed = this.completedTasks.find(t => t.id === requestId);
195
+ if (completed) {
196
+ return {
197
+ id: requestId,
198
+ task: completed.task,
199
+ status: 'completed',
200
+ processTime: completed.processTime,
201
+ completedAt: completed.completedAt,
202
+ };
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ /**
209
+ * 获取队列整体状态
210
+ * @returns {object} - 队列状态
211
+ */
212
+ getQueueStatus() {
213
+ return {
214
+ queueLength: this.queue.length,
215
+ isProcessing: this.isProcessing,
216
+ currentTask: this.currentTask ? {
217
+ id: this.shortId(this.currentTask.id),
218
+ task: this.currentTask.data.task.slice(0, 50),
219
+ status: this.currentTask.status,
220
+ elapsed: Date.now() - this.currentTask.startedAt,
221
+ } : null,
222
+ pendingTasks: this.queue.map(t => ({
223
+ id: this.shortId(t.id),
224
+ task: t.data.task.slice(0, 50),
225
+ waitTime: Date.now() - t.addedAt,
226
+ })),
227
+ recentCompleted: this.completedTasks.slice(0, 5).map(t => ({
228
+ id: this.shortId(t.id),
229
+ task: t.task.slice(0, 50),
230
+ processTime: t.processTime,
231
+ status: t.status,
232
+ })),
233
+ stats: {
234
+ totalProcessed: this.stats.totalProcessed,
235
+ averageProcessTime: Math.round(this.stats.averageProcessTime),
236
+ },
237
+ };
238
+ }
239
+
240
+ /**
241
+ * 生成唯一请求ID
242
+ */
243
+ _generateRequestId() {
244
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
245
+ }
246
+
247
+ /**
248
+ * 简短ID(用于显示)
249
+ */
250
+ _shortId(id) {
251
+ if (!id) return 'unknown';
252
+ const parts = id.split('-');
253
+ return parts[1] || id.slice(-6);
254
+ }
255
+
256
+ shortId(id) {
257
+ return this._shortId(id);
258
+ }
259
+
260
+ /**
261
+ * 估算等待时间
262
+ */
263
+ _estimateWaitTime(position) {
264
+ if (position === 0) return 0;
265
+ // 使用历史平均处理时间估算
266
+ const avgTime = this.stats.averageProcessTime || 5000;
267
+ return Math.round(avgTime * position);
268
+ }
269
+
270
+ /**
271
+ * 更新统计信息
272
+ */
273
+ _updateStats(processTime) {
274
+ this.stats.totalProcessed++;
275
+ this.stats.totalProcessTime += processTime;
276
+ this.stats.averageProcessTime = this.stats.totalProcessTime / this.stats.totalProcessed;
277
+ }
278
+
279
+ /**
280
+ * 清空队列(紧急情况)
281
+ */
282
+ clearQueue() {
283
+ // 通知所有等待的任务
284
+ for (const task of this.queue) {
285
+ task.resolve({
286
+ status: 'error',
287
+ answer: 'Queue cleared',
288
+ requestId: task.id,
289
+ });
290
+ }
291
+ this.queue = [];
292
+ logger.warn('[Queue] 队列已清空');
293
+ }
294
+ }
295
+
296
+ module.exports = TaskQueue;