@yeaft/webchat-agent 0.0.232 → 0.0.234

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 CHANGED
@@ -1,777 +1,14 @@
1
- import WebSocket from 'ws';
2
- import { execSync, execFile, spawn } from 'child_process';
3
- import { writeFileSync, mkdirSync, existsSync, cpSync, rmSync } from 'fs';
4
- import { join } from 'path';
5
- import { platform } from 'os';
6
- import ctx from './context.js';
7
- import { getConfigDir } from './service.js';
8
- import { encrypt, decrypt, isEncrypted, decodeKey } from './encryption.js';
9
- import { handleTerminalCreate, handleTerminalInput, handleTerminalResize, handleTerminalClose } from './terminal.js';
10
- import { handleProxyHttpRequest, handleProxyWsOpen, handleProxyWsMessage, handleProxyWsClose } from './proxy.js';
11
- import {
12
- handleReadFile, handleWriteFile, handleListDirectory,
13
- handleGitStatus, handleGitDiff, handleGitAdd, handleGitReset, handleGitRestore, handleGitCommit, handleGitPush,
14
- handleFileSearch, handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir, handleTransferFiles
15
- } from './workbench.js';
16
- import { handleListHistorySessions, handleListFolders } from './history.js';
17
- import {
18
- createConversation, resumeConversation, deleteConversation,
19
- handleRefreshConversation, handleCancelExecution,
20
- handleUserInput, handleUpdateConversationSettings, handleAskUserAnswer,
21
- sendConversationList
22
- } from './conversation.js';
23
- import {
24
- createCrewSession, handleCrewHumanInput, handleCrewControl,
25
- addRoleToSession, removeRoleFromSession,
26
- handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex,
27
- handleLoadCrewHistory
28
- } from './crew.js';
29
-
30
- // 需要在断连期间缓冲的消息类型(Claude 输出相关的关键消息)
31
- const BUFFERABLE_TYPES = new Set([
32
- 'claude_output', 'turn_completed', 'conversation_closed',
33
- 'session_id_update', 'compact_status', 'slash_commands_update',
34
- 'background_task_started', 'background_task_output',
35
- 'crew_output', 'crew_status', 'crew_turn_completed',
36
- 'crew_session_created', 'crew_session_restored', 'crew_human_needed',
37
- 'crew_role_added', 'crew_role_removed',
38
- 'crew_role_compact', 'crew_context_usage'
39
- ]);
40
-
41
- // Send message to server (with encryption if available)
42
- // 断连时对关键消息类型进行缓冲,重连后自动 flush
43
- async function sendToServer(msg) {
44
- if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) {
45
- // 缓冲关键消息
46
- if (BUFFERABLE_TYPES.has(msg.type)) {
47
- if (ctx.messageBuffer.length < ctx.messageBufferMaxSize) {
48
- ctx.messageBuffer.push(msg);
49
- console.log(`[WS] Buffered message: ${msg.type} (queue: ${ctx.messageBuffer.length})`);
50
- } else {
51
- // Buffer full: drop oldest non-status messages to make room
52
- const dropIdx = ctx.messageBuffer.findIndex(m => m.type !== 'crew_status' && m.type !== 'turn_completed');
53
- if (dropIdx >= 0) {
54
- ctx.messageBuffer.splice(dropIdx, 1);
55
- ctx.messageBuffer.push(msg);
56
- console.warn(`[WS] Buffer full, dropped oldest to make room for: ${msg.type}`);
57
- } else {
58
- console.warn(`[WS] Buffer full (${ctx.messageBufferMaxSize}), dropping: ${msg.type}`);
59
- }
60
- }
61
- } else {
62
- console.warn(`[WS] Cannot send message, WebSocket not open: ${msg.type}`);
63
- }
64
- return;
65
- }
66
-
67
- try {
68
- if (ctx.sessionKey) {
69
- const encrypted = await encrypt(msg, ctx.sessionKey);
70
- ctx.ws.send(JSON.stringify(encrypted));
71
- } else {
72
- ctx.ws.send(JSON.stringify(msg));
73
- }
74
- } catch (e) {
75
- console.error(`[WS] Error sending message ${msg.type}:`, e.message);
76
- // 发送失败也缓冲
77
- if (BUFFERABLE_TYPES.has(msg.type) && ctx.messageBuffer.length < ctx.messageBufferMaxSize) {
78
- ctx.messageBuffer.push(msg);
79
- console.log(`[WS] Send failed, buffered: ${msg.type}`);
80
- }
81
- }
82
- }
83
-
84
- // Flush 断连期间缓冲的消息
85
- async function flushMessageBuffer() {
86
- if (ctx.messageBuffer.length === 0) return;
87
-
88
- const buffered = ctx.messageBuffer.splice(0);
89
- console.log(`[WS] Flushing ${buffered.length} buffered messages...`);
90
-
91
- for (const msg of buffered) {
92
- await sendToServer(msg);
93
- }
94
-
95
- console.log(`[WS] Flush complete`);
96
- }
97
-
98
- // Parse incoming message (decrypt if encrypted)
99
- async function parseMessage(data) {
100
- try {
101
- const parsed = JSON.parse(data.toString());
102
-
103
- if (ctx.sessionKey && isEncrypted(parsed)) {
104
- return await decrypt(parsed, ctx.sessionKey);
105
- }
106
-
107
- return parsed;
108
- } catch (e) {
109
- console.error('Failed to parse message:', e);
110
- return null;
111
- }
112
- }
113
-
114
- async function handleMessage(msg) {
115
- switch (msg.type) {
116
- case 'registered':
117
- if (msg.sessionKey) {
118
- ctx.sessionKey = decodeKey(msg.sessionKey);
119
- console.log('Encryption enabled');
120
- }
121
-
122
- // 只保存基本配置(不再保存 agentId,因为现在用 agentName 作为 ID)
123
- ctx.saveConfig({
124
- serverUrl: ctx.CONFIG.serverUrl,
125
- agentName: ctx.CONFIG.agentName,
126
- workDir: ctx.CONFIG.workDir,
127
- reconnectInterval: ctx.CONFIG.reconnectInterval
128
- // 不保存 agentSecret 到配置文件(安全考虑)
129
- });
130
- console.log(`Registered as agent: ${msg.agentId} (name: ${ctx.CONFIG.agentName})`);
131
-
132
- // Check server-pushed upgrade notification
133
- if (msg.upgradeAvailable) {
134
- console.log(`\n Update available: ${ctx.agentVersion} → ${msg.upgradeAvailable}`);
135
- console.log(` Run "yeaft-agent upgrade" to update\n`);
136
- }
137
-
138
- sendConversationList();
139
-
140
- // ★ Flush 断连期间缓冲的消息
141
- await flushMessageBuffer();
142
-
143
- // ★ Phase 1: 通知 server 同步完成
144
- sendToServer({ type: 'agent_sync_complete' });
145
- break;
146
-
147
- case 'create_conversation':
148
- await createConversation(msg);
149
- break;
150
-
151
- case 'resume_conversation':
152
- await resumeConversation(msg);
153
- break;
154
-
155
- case 'delete_conversation':
156
- deleteConversation(msg);
157
- break;
158
-
159
- case 'get_conversations':
160
- sendConversationList();
161
- break;
162
-
163
- case 'list_history_sessions':
164
- await handleListHistorySessions(msg);
165
- break;
166
-
167
- case 'list_folders':
168
- await handleListFolders(msg);
169
- break;
170
-
171
- case 'transfer_files':
172
- await handleTransferFiles(msg);
173
- break;
174
-
175
- case 'execute':
176
- await handleUserInput(msg);
177
- break;
178
-
179
- case 'cancel_execution':
180
- await handleCancelExecution(msg);
181
- break;
182
-
183
- // clear_queue 和 cancel_queued_message 已移至 server 端管理 (Phase 3.6)
184
-
185
- case 'refresh_conversation':
186
- await handleRefreshConversation(msg);
187
- break;
188
-
189
- // Terminal (PTY) messages
190
- case 'terminal_create':
191
- await handleTerminalCreate(msg);
192
- break;
193
-
194
- case 'terminal_input':
195
- handleTerminalInput(msg);
196
- break;
197
-
198
- case 'terminal_resize':
199
- handleTerminalResize(msg);
200
- break;
201
-
202
- case 'terminal_close':
203
- handleTerminalClose(msg);
204
- break;
205
-
206
- // File operation messages
207
- case 'read_file':
208
- await handleReadFile(msg);
209
- break;
210
-
211
- case 'write_file':
212
- await handleWriteFile(msg);
213
- break;
214
-
215
- case 'list_directory':
216
- await handleListDirectory(msg);
217
- break;
218
-
219
- case 'git_status':
220
- await handleGitStatus(msg);
221
- break;
222
-
223
- case 'git_diff':
224
- await handleGitDiff(msg);
225
- break;
226
-
227
- case 'git_add':
228
- await handleGitAdd(msg);
229
- break;
230
-
231
- case 'git_reset':
232
- await handleGitReset(msg);
233
- break;
234
-
235
- case 'git_restore':
236
- await handleGitRestore(msg);
237
- break;
238
-
239
- case 'git_commit':
240
- await handleGitCommit(msg);
241
- break;
242
-
243
- case 'git_push':
244
- await handleGitPush(msg);
245
- break;
246
-
247
- case 'file_search':
248
- await handleFileSearch(msg);
249
- break;
250
-
251
- case 'create_file':
252
- await handleCreateFile(msg);
253
- break;
254
-
255
- case 'delete_files':
256
- await handleDeleteFiles(msg);
257
- break;
258
-
259
- case 'move_files':
260
- await handleMoveFiles(msg);
261
- break;
262
-
263
- case 'copy_files':
264
- await handleCopyFiles(msg);
265
- break;
266
-
267
- case 'upload_to_dir':
268
- await handleUploadToDir(msg);
269
- break;
270
-
271
- case 'update_conversation_settings':
272
- handleUpdateConversationSettings(msg);
273
- break;
274
-
275
- case 'ask_user_answer':
276
- handleAskUserAnswer(msg);
277
- break;
278
-
279
- // Crew (multi-agent) messages
280
- case 'create_crew_session':
281
- await createCrewSession(msg);
282
- break;
283
-
284
- case 'crew_human_input':
285
- await handleCrewHumanInput(msg);
286
- break;
287
-
288
- case 'crew_control':
289
- await handleCrewControl(msg);
290
- break;
291
-
292
- case 'crew_add_role':
293
- await addRoleToSession(msg);
294
- break;
295
-
296
- case 'crew_remove_role':
297
- await removeRoleFromSession(msg);
298
- break;
299
-
300
- case 'list_crew_sessions':
301
- await handleListCrewSessions(msg);
302
- break;
303
-
304
- case 'check_crew_exists':
305
- await handleCheckCrewExists(msg);
306
- break;
307
-
308
- case 'delete_crew_dir':
309
- await handleDeleteCrewDir(msg);
310
- break;
311
-
312
- case 'resume_crew_session':
313
- await resumeCrewSession(msg);
314
- break;
315
-
316
- case 'delete_crew_session':
317
- await removeFromCrewIndex(msg.sessionId);
318
- (await import('./conversation.js')).sendConversationList();
319
- break;
320
-
321
- case 'update_crew_session':
322
- await (await import('./crew.js')).handleUpdateCrewSession(msg);
323
- break;
324
-
325
- case 'crew_load_history':
326
- await handleLoadCrewHistory(msg);
327
- break;
328
-
329
- // Port proxy
330
- case 'proxy_request':
331
- handleProxyHttpRequest(msg);
332
- break;
333
-
334
- case 'proxy_ws_open':
335
- handleProxyWsOpen(msg);
336
- break;
337
-
338
- case 'proxy_ws_message':
339
- handleProxyWsMessage(msg);
340
- break;
341
-
342
- case 'proxy_ws_close':
343
- handleProxyWsClose(msg);
344
- break;
345
-
346
- case 'proxy_update_ports':
347
- ctx.proxyPorts = msg.ports || [];
348
- sendToServer({ type: 'proxy_ports_update', ports: ctx.proxyPorts });
349
- break;
350
-
351
- case 'restart_agent':
352
- console.log('[Agent] Restart requested, shutting down for PM2/systemd restart...');
353
- sendToServer({ type: 'restart_agent_ack' });
354
- // 延迟让 ack 消息发出,然后优雅退出
355
- setTimeout(() => {
356
- // 清理终端和会话(与 index.js cleanup 相同逻辑)
357
- for (const [, term] of ctx.terminals) {
358
- if (term.pty) { try { term.pty.kill(); } catch {} }
359
- if (term.timer) clearTimeout(term.timer);
360
- }
361
- ctx.terminals.clear();
362
- for (const [, state] of ctx.conversations) {
363
- if (state.abortController) state.abortController.abort();
364
- if (state.inputStream) state.inputStream.done();
365
- }
366
- ctx.conversations.clear();
367
- stopAgentHeartbeat();
368
- if (ctx.ws) {
369
- // 禁止自动重连,让 process.exit 干净退出
370
- ctx.ws.removeAllListeners('close');
371
- ctx.ws.close();
372
- }
373
- clearTimeout(ctx.reconnectTimer);
374
- console.log('[Agent] Cleanup done, exiting with code 1 for auto-restart...');
375
- process.exit(1);
376
- }, 500);
377
- break;
378
-
379
- case 'upgrade_agent':
380
- console.log('[Agent] Upgrade requested, checking for updates...');
381
- (async () => {
382
- try {
383
- const pkgName = ctx.pkgName || '@yeaft/webchat-agent';
384
- // Check latest version (async to avoid blocking heartbeat)
385
- const latestVersion = await new Promise((resolve, reject) => {
386
- execFile('npm', ['view', pkgName, 'version'], { stdio: 'pipe', shell: true }, (err, stdout) => {
387
- if (err) reject(err); else resolve(stdout.toString().trim());
388
- });
389
- });
390
- if (latestVersion === ctx.agentVersion) {
391
- console.log(`[Agent] Already at latest version (${ctx.agentVersion}), skipping upgrade.`);
392
- sendToServer({ type: 'upgrade_agent_ack', success: true, alreadyLatest: true, version: ctx.agentVersion });
393
- return;
394
- }
395
- console.log(`[Agent] Upgrading from ${ctx.agentVersion} to latest (${latestVersion})...`);
396
-
397
- // 检测安装方式:npm install 的路径包含 node_modules,源码运行则不包含
398
- const scriptPath = (process.argv[1] || '').replace(/\\/g, '/');
399
- const nmIndex = scriptPath.lastIndexOf('/node_modules/');
400
- const isNpmInstall = nmIndex !== -1;
401
-
402
- if (!isNpmInstall) {
403
- // 源码运行不支持远程升级(代码在 git repo 中,需要手动 git pull)
404
- console.log('[Agent] Source-based install detected, remote upgrade not supported.');
405
- sendToServer({ type: 'upgrade_agent_ack', success: false, error: 'Source-based install: please use git pull to upgrade' });
406
- return;
407
- }
408
-
409
- // 提取 node_modules 的父目录(即 npm install 执行时的项目目录或全局 prefix)
410
- // 例如 /usr/lib/node_modules/@yeaft/webchat-agent/cli.js → /usr/lib
411
- // 例如 C:/Users/x/myproject/node_modules/@yeaft/webchat-agent/cli.js → C:/Users/x/myproject
412
- const installDir = scriptPath.substring(0, nmIndex);
413
-
414
- // 判断全局安装 vs 局部安装
415
- // npm 全局 node_modules: prefix/lib/node_modules (Linux/macOS) 或 prefix/node_modules (Windows)
416
- const isGlobalInstall = await new Promise((resolve) => {
417
- execFile('npm', ['prefix', '-g'], { shell: true }, (err, stdout) => {
418
- if (err) { resolve(false); return; }
419
- const globalPrefix = stdout.toString().trim().replace(/\\/g, '/');
420
- resolve(installDir === globalPrefix || installDir === globalPrefix + '/lib');
421
- });
422
- });
423
-
424
- const isWindows = platform() === 'win32';
425
- // 全局安装用 npm install -g,局部安装在 installDir 下 npm install
426
- // 不指定版本号,直接用 @latest 确保安装最新版
427
- const npmArgs = isGlobalInstall
428
- ? ['install', '-g', `${pkgName}@latest`]
429
- : ['install', `${pkgName}@latest`];
430
-
431
- if (isWindows) {
432
- // Windows: 进程持有文件锁,npm install 无法覆盖正在运行的模块文件 (EBUSY)
433
- // 策略:生成 detached bat 脚本,等当前进程退出后 pm2 stop(防止 autorestart),
434
- // 再 npm install
435
- const pid = process.pid;
436
- const configDir = getConfigDir();
437
- mkdirSync(configDir, { recursive: true });
438
- const logDir = join(configDir, 'logs');
439
- mkdirSync(logDir, { recursive: true });
440
- const batPath = join(configDir, 'upgrade.bat');
441
- const logPath = join(logDir, 'upgrade.log');
442
- const isPm2 = !!process.env.pm_id;
443
- const installDirWin = installDir.replace(/\//g, '\\');
444
-
445
- const batLines = [
446
- '@echo off',
447
- 'setlocal',
448
- `set PID=${pid}`,
449
- `set PKG=${pkgName}@latest`,
450
- `set INSTALL_DIR=${installDirWin}`,
451
- `set LOGFILE=${logPath}`,
452
- `set MAX_WAIT=30`,
453
- `set COUNT=0`,
454
- '',
455
- ':: Redirect all output to log file',
456
- 'echo [Upgrade] Started at %date% %time% > "%LOGFILE%"',
457
- ];
458
-
459
- // 先等原始 agent 进程退出。
460
- // 不能在这之前 pm2 stop,因为 pm2 stop 会发 SIGTERM 杀当前进程,
461
- // 导致 agent 还没来得及发 upgrade_agent_ack 就被杀掉。
462
- // PID 退出后再由 bat 脚本执行 pm2 stop 阻止 autorestart。
463
- batLines.push(
464
- ':WAIT_LOOP',
465
- 'tasklist /FI "PID eq %PID%" 2>NUL | find /I "%PID%" >NUL',
466
- 'if errorlevel 1 goto PID_EXITED',
467
- 'set /A COUNT+=1',
468
- 'if %COUNT% GEQ %MAX_WAIT% (',
469
- ' echo [Upgrade] Timeout waiting for PID %PID% to exit after 60s >> "%LOGFILE%"',
470
- ' goto PID_EXITED',
471
- ')',
472
- 'ping -n 3 127.0.0.1 >NUL',
473
- 'goto WAIT_LOOP',
474
- ':PID_EXITED',
475
- );
476
-
477
- if (isPm2) {
478
- // PID已退出。但如果 agent 端 pm2 stop 失败,PM2 的 autorestart 可能
479
- // 已在 restart_delay(5s) 后启动了新实例。这里 pm2 stop 会停掉新实例,
480
- // 确保 npm install 时无进程锁文件。
481
- batLines.push(
482
- 'echo [Upgrade] Stopping pm2 to prevent autorestart... >> "%LOGFILE%"',
483
- 'call pm2 stop yeaft-agent >> "%LOGFILE%" 2>&1',
484
- ':: Wait for pm2 to fully terminate the process and release file locks',
485
- 'ping -n 6 127.0.0.1 >NUL',
486
- );
487
- }
488
-
489
- const npmBatCmd = isGlobalInstall
490
- ? 'call npm install -g %PKG%'
491
- : 'cd /d "%INSTALL_DIR%" && call npm install %PKG%';
492
-
493
- batLines.push(
494
- 'echo [Upgrade] Installing %PKG%... >> "%LOGFILE%"',
495
- `${npmBatCmd} >> "%LOGFILE%" 2>&1`,
496
- 'if errorlevel 1 (',
497
- ' echo [Upgrade] npm install failed with exit code %errorlevel% >> "%LOGFILE%"',
498
- ') else (',
499
- ' echo [Upgrade] Successfully installed %PKG% >> "%LOGFILE%"',
500
- ')',
501
- );
502
-
503
- batLines.push(':CLEANUP');
504
-
505
- if (isPm2) {
506
- batLines.push(
507
- 'echo [Upgrade] Starting agent via pm2... >> "%LOGFILE%"',
508
- 'call pm2 start yeaft-agent >> "%LOGFILE%" 2>&1',
509
- );
510
- }
511
-
512
- batLines.push(`del /F /Q "${batPath}"`);
513
-
514
- writeFileSync(batPath, batLines.join('\r\n'));
515
- const child = spawn('cmd.exe', ['/c', batPath], {
516
- detached: true,
517
- stdio: 'ignore',
518
- windowsHide: true
519
- });
520
- child.unref();
521
- console.log(`[Agent] Spawned upgrade script (PID wait for ${pid}, pm2=${isPm2}, dir=${installDir}): ${batPath}`);
522
- sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
523
- } else {
524
- // Linux/macOS: 生成 detached shell 脚本,先停止服务再升级再启动
525
- // 避免在升级过程中 systemd 不断重启已删除的旧版本
526
- const pid = process.pid;
527
- const configDir = getConfigDir();
528
- mkdirSync(configDir, { recursive: true });
529
- const shPath = join(configDir, 'upgrade.sh');
530
- const isSystemd = existsSync(join(process.env.HOME || '', '.config', 'systemd', 'user', 'yeaft-agent.service'));
531
- const isLaunchd = platform() === 'darwin' && existsSync(join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist'));
532
- const cwd = isGlobalInstall ? undefined : installDir;
533
-
534
- const shLines = [
535
- '#!/bin/bash',
536
- `PID=${pid}`,
537
- `PKG="${pkgName}@latest"`,
538
- `LOGFILE="${join(configDir, 'logs', 'upgrade.log')}"`,
539
- `export PATH="${process.env.PATH}"`,
540
- '',
541
- '# Redirect all output to log file',
542
- 'exec > "$LOGFILE" 2>&1',
543
- 'echo "[Upgrade] Started at $(date)"',
544
- '',
545
- ...(cwd ? [`INSTALL_DIR="${cwd}"`] : []),
546
- '',
547
- '# Wait for current process to exit',
548
- 'COUNT=0',
549
- 'while kill -0 $PID 2>/dev/null; do',
550
- ' COUNT=$((COUNT+1))',
551
- ' if [ $COUNT -ge 30 ]; then',
552
- ' echo "[Upgrade] Timeout waiting for PID $PID to exit"',
553
- ' break',
554
- ' fi',
555
- ' sleep 2',
556
- 'done',
557
- '',
558
- ];
559
-
560
- // 停止服务管理器的自动重启
561
- if (isSystemd) {
562
- shLines.push(
563
- '# Stop systemd service to prevent restart loop',
564
- 'systemctl --user stop yeaft-agent 2>/dev/null',
565
- 'sleep 1',
566
- '',
567
- );
568
- } else if (isLaunchd) {
569
- const plistPath = join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
570
- shLines.push(
571
- '# Unload launchd service to prevent restart loop',
572
- `launchctl unload "${plistPath}" 2>/dev/null`,
573
- 'sleep 1',
574
- '',
575
- );
576
- }
577
-
578
- // npm install
579
- const npmCmd = isGlobalInstall
580
- ? `npm install -g "$PKG"`
581
- : `cd "$INSTALL_DIR" && npm install "$PKG"`;
582
-
583
- shLines.push(
584
- 'echo "[Upgrade] Installing $PKG..."',
585
- npmCmd,
586
- 'EXIT_CODE=$?',
587
- 'if [ $EXIT_CODE -ne 0 ]; then',
588
- ' echo "[Upgrade] npm install failed with exit code $EXIT_CODE"',
589
- 'else',
590
- ' echo "[Upgrade] Successfully installed $PKG"',
591
- 'fi',
592
- '',
593
- );
594
-
595
- // 重新启动服务
596
- if (isSystemd) {
597
- shLines.push(
598
- '# Restart systemd service',
599
- 'systemctl --user start yeaft-agent',
600
- 'echo "[Upgrade] Service restarted via systemd"',
601
- );
602
- } else if (isLaunchd) {
603
- const plistPath = join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
604
- shLines.push(
605
- '# Reload launchd service',
606
- `launchctl load "${plistPath}"`,
607
- 'echo "[Upgrade] Service restarted via launchd"',
608
- );
609
- }
610
-
611
- // 清理脚本自身
612
- shLines.push('', `rm -f "${shPath}"`);
613
-
614
- writeFileSync(shPath, shLines.join('\n'), { mode: 0o755 });
615
- const child = spawn('bash', [shPath], {
616
- detached: true,
617
- stdio: 'ignore',
618
- });
619
- child.unref();
620
- console.log(`[Agent] Spawned upgrade script: ${shPath}`);
621
- sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
622
- }
623
-
624
- // 清理并退出,让升级脚本接管
625
- // 注意: PM2 环境下不能在进程内调用 pm2 stop(会杀死自身进程,后续代码不会执行)
626
- // 由 bat 脚本在检测到 PID 退出后执行 pm2 stop,阻止 autorestart
627
- setTimeout(() => {
628
- for (const [, term] of ctx.terminals) {
629
- if (term.pty) { try { term.pty.kill(); } catch {} }
630
- if (term.timer) clearTimeout(term.timer);
631
- }
632
- ctx.terminals.clear();
633
- for (const [, state] of ctx.conversations) {
634
- if (state.abortController) state.abortController.abort();
635
- if (state.inputStream) state.inputStream.done();
636
- }
637
- ctx.conversations.clear();
638
- stopAgentHeartbeat();
639
- if (ctx.ws) {
640
- ctx.ws.removeAllListeners('close');
641
- ctx.ws.close();
642
- }
643
- clearTimeout(ctx.reconnectTimer);
644
- console.log('[Agent] Cleanup done, exiting for upgrade...');
645
- process.exit(0);
646
- }, 500);
647
- } catch (e) {
648
- console.error('[Agent] Upgrade failed:', e.message);
649
- sendToServer({ type: 'upgrade_agent_ack', success: false, error: e.message });
650
- }
651
- })();
652
- break;
653
- }
654
- }
655
-
656
- export function startAgentHeartbeat() {
657
- stopAgentHeartbeat();
658
- ctx.lastPongAt = Date.now();
659
-
660
- // 监听 pong 帧
661
- if (ctx.ws) {
662
- ctx.ws.on('pong', () => {
663
- ctx.lastPongAt = Date.now();
664
- });
665
- }
666
-
667
- ctx.agentHeartbeatTimer = setInterval(() => {
668
- if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) return;
669
-
670
- // 检查上次 pong 是否超时
671
- const sincePong = Date.now() - ctx.lastPongAt;
672
- if (sincePong > 45000) {
673
- console.warn(`[Heartbeat] No pong for ${Math.round(sincePong / 1000)}s, reconnecting...`);
674
- ctx.ws.terminate();
675
- return;
676
- }
677
-
678
- try {
679
- ctx.ws.ping();
680
- } catch (e) {
681
- console.warn('[Heartbeat] Failed to send ping:', e.message);
682
- }
683
- }, 25000);
684
- }
685
-
686
- export function stopAgentHeartbeat() {
687
- if (ctx.agentHeartbeatTimer) {
688
- clearInterval(ctx.agentHeartbeatTimer);
689
- ctx.agentHeartbeatTimer = null;
690
- }
691
- }
692
-
693
- export function scheduleReconnect() {
694
- clearTimeout(ctx.reconnectTimer);
695
- ctx.reconnectTimer = setTimeout(() => {
696
- console.log('Attempting to reconnect...');
697
- connect();
698
- }, ctx.CONFIG.reconnectInterval);
699
- }
700
-
701
- export function connect() {
702
- // Don't include secret in URL - it will be sent via WebSocket message after connection
703
- // 使用 agentName 作为唯一标识(不再使用随机 UUID)
704
- const params = new URLSearchParams({
705
- type: 'agent',
706
- id: ctx.CONFIG.agentName, // 直接用名称作为 ID
707
- name: ctx.CONFIG.agentName,
708
- workDir: ctx.CONFIG.workDir,
709
- capabilities: ctx.agentCapabilities.join(',')
710
- });
711
-
712
- const url = `${ctx.CONFIG.serverUrl}?${params.toString()}`;
713
- console.log(`Connecting to server: ${ctx.CONFIG.serverUrl}`);
714
- if (ctx.CONFIG.disallowedTools.length > 0) {
715
- console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
716
- }
717
-
718
- ctx.ws = new WebSocket(url);
719
-
720
- ctx.ws.on('open', () => {
721
- console.log('Connected to server, waiting for auth challenge...');
722
- clearTimeout(ctx.reconnectTimer);
723
- // 启动 agent 端心跳: 每 25 秒发一次 ping 帧
724
- startAgentHeartbeat();
725
- });
726
-
727
- ctx.ws.on('message', async (data) => {
728
- // 收到任何消息都说明连接活着
729
- ctx.lastPongAt = Date.now();
730
-
731
- // Check for auth_required message (unencrypted)
732
- try {
733
- const msg = JSON.parse(data.toString());
734
- if (msg.type === 'auth_required' && msg.tempId) {
735
- console.log('Received auth challenge, sending credentials...');
736
- ctx.pendingAuthTempId = msg.tempId;
737
- // Send authentication via WebSocket (not URL)
738
- ctx.ws.send(JSON.stringify({
739
- type: 'auth',
740
- tempId: msg.tempId,
741
- secret: ctx.CONFIG.agentSecret,
742
- capabilities: ctx.agentCapabilities,
743
- version: ctx.agentVersion
744
- }));
745
- return;
746
- }
747
- } catch (e) {
748
- // Not JSON or parse error - continue to normal handling
749
- }
750
-
751
- const msg = await parseMessage(data);
752
- if (msg) {
753
- handleMessage(msg);
754
- }
755
- });
756
-
757
- ctx.ws.on('close', (code, reason) => {
758
- console.log(`Disconnected from server: ${code} ${reason}`);
759
- ctx.sessionKey = null;
760
- ctx.pendingAuthTempId = null;
761
- stopAgentHeartbeat();
762
-
763
- if (code === 1008) {
764
- console.error('Authentication failed. Check AGENT_SECRET configuration.');
765
- return;
766
- }
767
-
768
- scheduleReconnect();
769
- });
770
-
771
- ctx.ws.on('error', (err) => {
772
- console.error('WebSocket error:', err.message);
773
- });
774
- }
775
-
776
- // 注册 sendToServer 到 ctx 供其他模块使用
777
- ctx.sendToServer = sendToServer;
1
+ // Re-export from connection/ submodules for backward compatibility
2
+ export {
3
+ connect,
4
+ sendToServer,
5
+ flushMessageBuffer,
6
+ parseMessage,
7
+ BUFFERABLE_TYPES,
8
+ startAgentHeartbeat,
9
+ stopAgentHeartbeat,
10
+ scheduleReconnect,
11
+ handleMessage,
12
+ handleRestartAgent,
13
+ handleUpgradeAgent
14
+ } from './connection/index.js';