@yeaft/webchat-agent 0.0.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/connection.js ADDED
@@ -0,0 +1,391 @@
1
+ import WebSocket from 'ws';
2
+ import ctx from './context.js';
3
+ import { encrypt, decrypt, isEncrypted, decodeKey } from './encryption.js';
4
+ import { handleTerminalCreate, handleTerminalInput, handleTerminalResize, handleTerminalClose } from './terminal.js';
5
+ import { handleProxyHttpRequest, handleProxyWsOpen, handleProxyWsMessage, handleProxyWsClose } from './proxy.js';
6
+ import {
7
+ handleReadFile, handleWriteFile, handleListDirectory,
8
+ handleGitStatus, handleGitDiff, handleGitAdd, handleGitReset, handleGitRestore, handleGitCommit, handleGitPush,
9
+ handleFileSearch, handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir, handleTransferFiles
10
+ } from './workbench.js';
11
+ import { handleListHistorySessions, handleListFolders } from './history.js';
12
+ import {
13
+ createConversation, resumeConversation, deleteConversation,
14
+ handleRefreshConversation, handleCancelExecution,
15
+ handleUserInput, handleUpdateConversationSettings, handleAskUserAnswer,
16
+ sendConversationList
17
+ } from './conversation.js';
18
+
19
+ // Send message to server (with encryption if available)
20
+ async function sendToServer(msg) {
21
+ if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) {
22
+ console.warn(`[WS] Cannot send message, WebSocket not open (state: ${ctx.ws?.readyState})`);
23
+ return;
24
+ }
25
+
26
+ try {
27
+ if (ctx.sessionKey) {
28
+ const encrypted = await encrypt(msg, ctx.sessionKey);
29
+ ctx.ws.send(JSON.stringify(encrypted));
30
+ console.log(`[WS] Sent encrypted message: ${msg.type}`);
31
+ } else {
32
+ ctx.ws.send(JSON.stringify(msg));
33
+ console.log(`[WS] Sent plain message: ${msg.type}`);
34
+ }
35
+ } catch (e) {
36
+ console.error(`[WS] Error sending message ${msg.type}:`, e.message);
37
+ }
38
+ }
39
+
40
+ // Parse incoming message (decrypt if encrypted)
41
+ async function parseMessage(data) {
42
+ try {
43
+ const parsed = JSON.parse(data.toString());
44
+
45
+ if (ctx.sessionKey && isEncrypted(parsed)) {
46
+ return await decrypt(parsed, ctx.sessionKey);
47
+ }
48
+
49
+ return parsed;
50
+ } catch (e) {
51
+ console.error('Failed to parse message:', e);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ async function handleMessage(msg) {
57
+ switch (msg.type) {
58
+ case 'registered':
59
+ if (msg.sessionKey) {
60
+ ctx.sessionKey = decodeKey(msg.sessionKey);
61
+ console.log('Encryption enabled');
62
+ }
63
+
64
+ // 只保存基本配置(不再保存 agentId,因为现在用 agentName 作为 ID)
65
+ ctx.saveConfig({
66
+ serverUrl: ctx.CONFIG.serverUrl,
67
+ agentName: ctx.CONFIG.agentName,
68
+ workDir: ctx.CONFIG.workDir,
69
+ reconnectInterval: ctx.CONFIG.reconnectInterval
70
+ // 不保存 agentSecret 到配置文件(安全考虑)
71
+ });
72
+ console.log(`Registered as agent: ${msg.agentId} (name: ${ctx.CONFIG.agentName})`);
73
+
74
+ // Check server-pushed upgrade notification
75
+ if (msg.upgradeAvailable) {
76
+ console.log(`\n Update available: ${ctx.agentVersion} → ${msg.upgradeAvailable}`);
77
+ console.log(` Run "yeaft-agent upgrade" to update\n`);
78
+ }
79
+
80
+ sendConversationList();
81
+
82
+ // ★ Phase 1: 通知 server 同步完成
83
+ sendToServer({ type: 'agent_sync_complete' });
84
+ break;
85
+
86
+ case 'create_conversation':
87
+ await createConversation(msg);
88
+ break;
89
+
90
+ case 'resume_conversation':
91
+ await resumeConversation(msg);
92
+ break;
93
+
94
+ case 'delete_conversation':
95
+ deleteConversation(msg);
96
+ break;
97
+
98
+ case 'get_conversations':
99
+ sendConversationList();
100
+ break;
101
+
102
+ case 'list_history_sessions':
103
+ await handleListHistorySessions(msg);
104
+ break;
105
+
106
+ case 'list_folders':
107
+ await handleListFolders(msg);
108
+ break;
109
+
110
+ case 'transfer_files':
111
+ await handleTransferFiles(msg);
112
+ break;
113
+
114
+ case 'execute':
115
+ await handleUserInput(msg);
116
+ break;
117
+
118
+ case 'cancel_execution':
119
+ await handleCancelExecution(msg);
120
+ break;
121
+
122
+ // clear_queue 和 cancel_queued_message 已移至 server 端管理 (Phase 3.6)
123
+
124
+ case 'refresh_conversation':
125
+ await handleRefreshConversation(msg);
126
+ break;
127
+
128
+ // Terminal (PTY) messages
129
+ case 'terminal_create':
130
+ await handleTerminalCreate(msg);
131
+ break;
132
+
133
+ case 'terminal_input':
134
+ handleTerminalInput(msg);
135
+ break;
136
+
137
+ case 'terminal_resize':
138
+ handleTerminalResize(msg);
139
+ break;
140
+
141
+ case 'terminal_close':
142
+ handleTerminalClose(msg);
143
+ break;
144
+
145
+ // File operation messages
146
+ case 'read_file':
147
+ await handleReadFile(msg);
148
+ break;
149
+
150
+ case 'write_file':
151
+ await handleWriteFile(msg);
152
+ break;
153
+
154
+ case 'list_directory':
155
+ await handleListDirectory(msg);
156
+ break;
157
+
158
+ case 'git_status':
159
+ await handleGitStatus(msg);
160
+ break;
161
+
162
+ case 'git_diff':
163
+ await handleGitDiff(msg);
164
+ break;
165
+
166
+ case 'git_add':
167
+ await handleGitAdd(msg);
168
+ break;
169
+
170
+ case 'git_reset':
171
+ await handleGitReset(msg);
172
+ break;
173
+
174
+ case 'git_restore':
175
+ await handleGitRestore(msg);
176
+ break;
177
+
178
+ case 'git_commit':
179
+ await handleGitCommit(msg);
180
+ break;
181
+
182
+ case 'git_push':
183
+ await handleGitPush(msg);
184
+ break;
185
+
186
+ case 'file_search':
187
+ await handleFileSearch(msg);
188
+ break;
189
+
190
+ case 'create_file':
191
+ await handleCreateFile(msg);
192
+ break;
193
+
194
+ case 'delete_files':
195
+ await handleDeleteFiles(msg);
196
+ break;
197
+
198
+ case 'move_files':
199
+ await handleMoveFiles(msg);
200
+ break;
201
+
202
+ case 'copy_files':
203
+ await handleCopyFiles(msg);
204
+ break;
205
+
206
+ case 'upload_to_dir':
207
+ await handleUploadToDir(msg);
208
+ break;
209
+
210
+ case 'update_conversation_settings':
211
+ handleUpdateConversationSettings(msg);
212
+ break;
213
+
214
+ case 'ask_user_answer':
215
+ handleAskUserAnswer(msg);
216
+ break;
217
+
218
+ // Port proxy
219
+ case 'proxy_request':
220
+ handleProxyHttpRequest(msg);
221
+ break;
222
+
223
+ case 'proxy_ws_open':
224
+ handleProxyWsOpen(msg);
225
+ break;
226
+
227
+ case 'proxy_ws_message':
228
+ handleProxyWsMessage(msg);
229
+ break;
230
+
231
+ case 'proxy_ws_close':
232
+ handleProxyWsClose(msg);
233
+ break;
234
+
235
+ case 'proxy_update_ports':
236
+ ctx.proxyPorts = msg.ports || [];
237
+ sendToServer({ type: 'proxy_ports_update', ports: ctx.proxyPorts });
238
+ break;
239
+
240
+ case 'restart_agent':
241
+ console.log('[Agent] Restart requested, shutting down for PM2/systemd restart...');
242
+ sendToServer({ type: 'restart_agent_ack' });
243
+ // 延迟让 ack 消息发出,然后优雅退出
244
+ setTimeout(() => {
245
+ // 清理终端和会话(与 index.js cleanup 相同逻辑)
246
+ for (const [, term] of ctx.terminals) {
247
+ if (term.pty) { try { term.pty.kill(); } catch {} }
248
+ if (term.timer) clearTimeout(term.timer);
249
+ }
250
+ ctx.terminals.clear();
251
+ for (const [, state] of ctx.conversations) {
252
+ if (state.abortController) state.abortController.abort();
253
+ if (state.inputStream) state.inputStream.done();
254
+ }
255
+ ctx.conversations.clear();
256
+ stopAgentHeartbeat();
257
+ if (ctx.ws) {
258
+ // 禁止自动重连,让 process.exit 干净退出
259
+ ctx.ws.removeAllListeners('close');
260
+ ctx.ws.close();
261
+ }
262
+ clearTimeout(ctx.reconnectTimer);
263
+ console.log('[Agent] Cleanup done, exiting with code 1 for auto-restart...');
264
+ process.exit(1);
265
+ }, 500);
266
+ break;
267
+ }
268
+ }
269
+
270
+ export function startAgentHeartbeat() {
271
+ stopAgentHeartbeat();
272
+ ctx.lastPongAt = Date.now();
273
+
274
+ // 监听 pong 帧
275
+ if (ctx.ws) {
276
+ ctx.ws.on('pong', () => {
277
+ ctx.lastPongAt = Date.now();
278
+ });
279
+ }
280
+
281
+ ctx.agentHeartbeatTimer = setInterval(() => {
282
+ if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) return;
283
+
284
+ // 检查上次 pong 是否超时
285
+ const sincePong = Date.now() - ctx.lastPongAt;
286
+ if (sincePong > 45000) {
287
+ console.warn(`[Heartbeat] No pong for ${Math.round(sincePong / 1000)}s, reconnecting...`);
288
+ ctx.ws.terminate();
289
+ return;
290
+ }
291
+
292
+ try {
293
+ ctx.ws.ping();
294
+ } catch (e) {
295
+ console.warn('[Heartbeat] Failed to send ping:', e.message);
296
+ }
297
+ }, 25000);
298
+ }
299
+
300
+ export function stopAgentHeartbeat() {
301
+ if (ctx.agentHeartbeatTimer) {
302
+ clearInterval(ctx.agentHeartbeatTimer);
303
+ ctx.agentHeartbeatTimer = null;
304
+ }
305
+ }
306
+
307
+ export function scheduleReconnect() {
308
+ clearTimeout(ctx.reconnectTimer);
309
+ ctx.reconnectTimer = setTimeout(() => {
310
+ console.log('Attempting to reconnect...');
311
+ connect();
312
+ }, ctx.CONFIG.reconnectInterval);
313
+ }
314
+
315
+ export function connect() {
316
+ // Don't include secret in URL - it will be sent via WebSocket message after connection
317
+ // 使用 agentName 作为唯一标识(不再使用随机 UUID)
318
+ const params = new URLSearchParams({
319
+ type: 'agent',
320
+ id: ctx.CONFIG.agentName, // 直接用名称作为 ID
321
+ name: ctx.CONFIG.agentName,
322
+ workDir: ctx.CONFIG.workDir,
323
+ capabilities: ctx.agentCapabilities.join(',')
324
+ });
325
+
326
+ const url = `${ctx.CONFIG.serverUrl}?${params.toString()}`;
327
+ console.log(`Connecting to server: ${ctx.CONFIG.serverUrl}`);
328
+ if (ctx.CONFIG.disallowedTools.length > 0) {
329
+ console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
330
+ }
331
+
332
+ ctx.ws = new WebSocket(url);
333
+
334
+ ctx.ws.on('open', () => {
335
+ console.log('Connected to server, waiting for auth challenge...');
336
+ clearTimeout(ctx.reconnectTimer);
337
+ // 启动 agent 端心跳: 每 25 秒发一次 ping 帧
338
+ startAgentHeartbeat();
339
+ });
340
+
341
+ ctx.ws.on('message', async (data) => {
342
+ // 收到任何消息都说明连接活着
343
+ ctx.lastPongAt = Date.now();
344
+
345
+ // Check for auth_required message (unencrypted)
346
+ try {
347
+ const msg = JSON.parse(data.toString());
348
+ if (msg.type === 'auth_required' && msg.tempId) {
349
+ console.log('Received auth challenge, sending credentials...');
350
+ ctx.pendingAuthTempId = msg.tempId;
351
+ // Send authentication via WebSocket (not URL)
352
+ ctx.ws.send(JSON.stringify({
353
+ type: 'auth',
354
+ tempId: msg.tempId,
355
+ secret: ctx.CONFIG.agentSecret,
356
+ capabilities: ctx.agentCapabilities,
357
+ version: ctx.agentVersion
358
+ }));
359
+ return;
360
+ }
361
+ } catch (e) {
362
+ // Not JSON or parse error - continue to normal handling
363
+ }
364
+
365
+ const msg = await parseMessage(data);
366
+ if (msg) {
367
+ handleMessage(msg);
368
+ }
369
+ });
370
+
371
+ ctx.ws.on('close', (code, reason) => {
372
+ console.log(`Disconnected from server: ${code} ${reason}`);
373
+ ctx.sessionKey = null;
374
+ ctx.pendingAuthTempId = null;
375
+ stopAgentHeartbeat();
376
+
377
+ if (code === 1008) {
378
+ console.error('Authentication failed. Check AGENT_SECRET configuration.');
379
+ return;
380
+ }
381
+
382
+ scheduleReconnect();
383
+ });
384
+
385
+ ctx.ws.on('error', (err) => {
386
+ console.error('WebSocket error:', err.message);
387
+ });
388
+ }
389
+
390
+ // 注册 sendToServer 到 ctx 供其他模块使用
391
+ ctx.sendToServer = sendToServer;
package/context.js ADDED
@@ -0,0 +1,26 @@
1
+ // 共享上下文对象 - 所有模块通过 import 访问
2
+ // 由 index.js 在启动时初始化
3
+
4
+ export default {
5
+ ws: null,
6
+ sessionKey: null,
7
+ conversations: new Map(),
8
+ terminals: new Map(),
9
+ proxyPorts: [],
10
+ proxyWsSockets: new Map(),
11
+ pendingUserQuestions: new Map(),
12
+ nodePty: null,
13
+ CONFIG: null,
14
+ agentCapabilities: [],
15
+ // Agent 级别的 slash commands 缓存(所有 conversation 共用)
16
+ slashCommands: [],
17
+ // 连接相关
18
+ reconnectTimer: null,
19
+ pendingAuthTempId: null,
20
+ agentHeartbeatTimer: null,
21
+ lastPongAt: 0,
22
+ // 由 connection.js 注册的通信函数
23
+ sendToServer: null,
24
+ // 由 index.js 注册的配置保存函数
25
+ saveConfig: null,
26
+ };