@yeaft/webchat-agent 0.0.233 → 0.0.235

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.
@@ -0,0 +1,87 @@
1
+ import WebSocket from 'ws';
2
+ import ctx from '../context.js';
3
+ import { encrypt, decrypt, isEncrypted } from '../encryption.js';
4
+
5
+ // 需要在断连期间缓冲的消息类型(Claude 输出相关的关键消息)
6
+ export const BUFFERABLE_TYPES = new Set([
7
+ 'claude_output', 'turn_completed', 'conversation_closed',
8
+ 'session_id_update', 'compact_status', 'slash_commands_update',
9
+ 'background_task_started', 'background_task_output',
10
+ 'crew_output', 'crew_status', 'crew_turn_completed',
11
+ 'crew_session_created', 'crew_session_restored', 'crew_human_needed',
12
+ 'crew_role_added', 'crew_role_removed',
13
+ 'crew_role_compact', 'crew_context_usage'
14
+ ]);
15
+
16
+ // Send message to server (with encryption if available)
17
+ // 断连时对关键消息类型进行缓冲,重连后自动 flush
18
+ export async function sendToServer(msg) {
19
+ if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) {
20
+ // 缓冲关键消息
21
+ if (BUFFERABLE_TYPES.has(msg.type)) {
22
+ if (ctx.messageBuffer.length < ctx.messageBufferMaxSize) {
23
+ ctx.messageBuffer.push(msg);
24
+ console.log(`[WS] Buffered message: ${msg.type} (queue: ${ctx.messageBuffer.length})`);
25
+ } else {
26
+ // Buffer full: drop oldest non-status messages to make room
27
+ const dropIdx = ctx.messageBuffer.findIndex(m => m.type !== 'crew_status' && m.type !== 'turn_completed');
28
+ if (dropIdx >= 0) {
29
+ ctx.messageBuffer.splice(dropIdx, 1);
30
+ ctx.messageBuffer.push(msg);
31
+ console.warn(`[WS] Buffer full, dropped oldest to make room for: ${msg.type}`);
32
+ } else {
33
+ console.warn(`[WS] Buffer full (${ctx.messageBufferMaxSize}), dropping: ${msg.type}`);
34
+ }
35
+ }
36
+ } else {
37
+ console.warn(`[WS] Cannot send message, WebSocket not open: ${msg.type}`);
38
+ }
39
+ return;
40
+ }
41
+
42
+ try {
43
+ if (ctx.sessionKey) {
44
+ const encrypted = await encrypt(msg, ctx.sessionKey);
45
+ ctx.ws.send(JSON.stringify(encrypted));
46
+ } else {
47
+ ctx.ws.send(JSON.stringify(msg));
48
+ }
49
+ } catch (e) {
50
+ console.error(`[WS] Error sending message ${msg.type}:`, e.message);
51
+ // 发送失败也缓冲
52
+ if (BUFFERABLE_TYPES.has(msg.type) && ctx.messageBuffer.length < ctx.messageBufferMaxSize) {
53
+ ctx.messageBuffer.push(msg);
54
+ console.log(`[WS] Send failed, buffered: ${msg.type}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ // Flush 断连期间缓冲的消息
60
+ export async function flushMessageBuffer() {
61
+ if (ctx.messageBuffer.length === 0) return;
62
+
63
+ const buffered = ctx.messageBuffer.splice(0);
64
+ console.log(`[WS] Flushing ${buffered.length} buffered messages...`);
65
+
66
+ for (const msg of buffered) {
67
+ await sendToServer(msg);
68
+ }
69
+
70
+ console.log(`[WS] Flush complete`);
71
+ }
72
+
73
+ // Parse incoming message (decrypt if encrypted)
74
+ export async function parseMessage(data) {
75
+ try {
76
+ const parsed = JSON.parse(data.toString());
77
+
78
+ if (ctx.sessionKey && isEncrypted(parsed)) {
79
+ return await decrypt(parsed, ctx.sessionKey);
80
+ }
81
+
82
+ return parsed;
83
+ } catch (e) {
84
+ console.error('Failed to parse message:', e);
85
+ return null;
86
+ }
87
+ }
@@ -0,0 +1,47 @@
1
+ import WebSocket from 'ws';
2
+ import ctx from '../context.js';
3
+
4
+ export function startAgentHeartbeat() {
5
+ stopAgentHeartbeat();
6
+ ctx.lastPongAt = Date.now();
7
+
8
+ // 监听 pong 帧
9
+ if (ctx.ws) {
10
+ ctx.ws.on('pong', () => {
11
+ ctx.lastPongAt = Date.now();
12
+ });
13
+ }
14
+
15
+ ctx.agentHeartbeatTimer = setInterval(() => {
16
+ if (!ctx.ws || ctx.ws.readyState !== WebSocket.OPEN) return;
17
+
18
+ // 检查上次 pong 是否超时
19
+ const sincePong = Date.now() - ctx.lastPongAt;
20
+ if (sincePong > 45000) {
21
+ console.warn(`[Heartbeat] No pong for ${Math.round(sincePong / 1000)}s, reconnecting...`);
22
+ ctx.ws.terminate();
23
+ return;
24
+ }
25
+
26
+ try {
27
+ ctx.ws.ping();
28
+ } catch (e) {
29
+ console.warn('[Heartbeat] Failed to send ping:', e.message);
30
+ }
31
+ }, 25000);
32
+ }
33
+
34
+ export function stopAgentHeartbeat() {
35
+ if (ctx.agentHeartbeatTimer) {
36
+ clearInterval(ctx.agentHeartbeatTimer);
37
+ ctx.agentHeartbeatTimer = null;
38
+ }
39
+ }
40
+
41
+ export function scheduleReconnect(connectFn) {
42
+ clearTimeout(ctx.reconnectTimer);
43
+ ctx.reconnectTimer = setTimeout(() => {
44
+ console.log('Attempting to reconnect...');
45
+ connectFn();
46
+ }, ctx.CONFIG.reconnectInterval);
47
+ }
@@ -0,0 +1,89 @@
1
+ import WebSocket from 'ws';
2
+ import ctx from '../context.js';
3
+ import { sendToServer, parseMessage } from './buffer.js';
4
+ import { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './heartbeat.js';
5
+ import { handleMessage } from './message-router.js';
6
+
7
+ export function connect() {
8
+ // Don't include secret in URL - it will be sent via WebSocket message after connection
9
+ // 使用 agentName 作为唯一标识(不再使用随机 UUID)
10
+ const params = new URLSearchParams({
11
+ type: 'agent',
12
+ id: ctx.CONFIG.agentName, // 直接用名称作为 ID
13
+ name: ctx.CONFIG.agentName,
14
+ workDir: ctx.CONFIG.workDir,
15
+ capabilities: ctx.agentCapabilities.join(',')
16
+ });
17
+
18
+ const url = `${ctx.CONFIG.serverUrl}?${params.toString()}`;
19
+ console.log(`Connecting to server: ${ctx.CONFIG.serverUrl}`);
20
+ if (ctx.CONFIG.disallowedTools.length > 0) {
21
+ console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
22
+ }
23
+
24
+ ctx.ws = new WebSocket(url);
25
+
26
+ ctx.ws.on('open', () => {
27
+ console.log('Connected to server, waiting for auth challenge...');
28
+ clearTimeout(ctx.reconnectTimer);
29
+ // 启动 agent 端心跳: 每 25 秒发一次 ping 帧
30
+ startAgentHeartbeat();
31
+ });
32
+
33
+ ctx.ws.on('message', async (data) => {
34
+ // 收到任何消息都说明连接活着
35
+ ctx.lastPongAt = Date.now();
36
+
37
+ // Check for auth_required message (unencrypted)
38
+ try {
39
+ const msg = JSON.parse(data.toString());
40
+ if (msg.type === 'auth_required' && msg.tempId) {
41
+ console.log('Received auth challenge, sending credentials...');
42
+ ctx.pendingAuthTempId = msg.tempId;
43
+ // Send authentication via WebSocket (not URL)
44
+ ctx.ws.send(JSON.stringify({
45
+ type: 'auth',
46
+ tempId: msg.tempId,
47
+ secret: ctx.CONFIG.agentSecret,
48
+ capabilities: ctx.agentCapabilities,
49
+ version: ctx.agentVersion
50
+ }));
51
+ return;
52
+ }
53
+ } catch (e) {
54
+ // Not JSON or parse error - continue to normal handling
55
+ }
56
+
57
+ const msg = await parseMessage(data);
58
+ if (msg) {
59
+ handleMessage(msg);
60
+ }
61
+ });
62
+
63
+ ctx.ws.on('close', (code, reason) => {
64
+ console.log(`Disconnected from server: ${code} ${reason}`);
65
+ ctx.sessionKey = null;
66
+ ctx.pendingAuthTempId = null;
67
+ stopAgentHeartbeat();
68
+
69
+ if (code === 1008) {
70
+ console.error('Authentication failed. Check AGENT_SECRET configuration.');
71
+ return;
72
+ }
73
+
74
+ scheduleReconnect(connect);
75
+ });
76
+
77
+ ctx.ws.on('error', (err) => {
78
+ console.error('WebSocket error:', err.message);
79
+ });
80
+ }
81
+
82
+ // 注册 sendToServer 到 ctx 供其他模块使用
83
+ ctx.sendToServer = sendToServer;
84
+
85
+ // Re-export submodule functions for backward compatibility
86
+ export { sendToServer, flushMessageBuffer, parseMessage, BUFFERABLE_TYPES } from './buffer.js';
87
+ export { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './heartbeat.js';
88
+ export { handleMessage } from './message-router.js';
89
+ export { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
@@ -0,0 +1,271 @@
1
+ import ctx from '../context.js';
2
+ import { decodeKey } from '../encryption.js';
3
+ import { handleTerminalCreate, handleTerminalInput, handleTerminalResize, handleTerminalClose } from '../terminal.js';
4
+ import { handleProxyHttpRequest, handleProxyWsOpen, handleProxyWsMessage, handleProxyWsClose } from '../proxy.js';
5
+ import {
6
+ handleReadFile, handleWriteFile, handleListDirectory,
7
+ handleGitStatus, handleGitDiff, handleGitAdd, handleGitReset, handleGitRestore, handleGitCommit, handleGitPush,
8
+ handleFileSearch, handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir, handleTransferFiles
9
+ } from '../workbench.js';
10
+ import { handleListHistorySessions, handleListFolders } from '../history.js';
11
+ import {
12
+ createConversation, resumeConversation, deleteConversation,
13
+ handleRefreshConversation, handleCancelExecution,
14
+ handleUserInput, handleUpdateConversationSettings, handleAskUserAnswer,
15
+ sendConversationList
16
+ } from '../conversation.js';
17
+ import {
18
+ createCrewSession, handleCrewHumanInput, handleCrewControl,
19
+ addRoleToSession, removeRoleFromSession,
20
+ handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex,
21
+ handleLoadCrewHistory
22
+ } from '../crew.js';
23
+ import { sendToServer, flushMessageBuffer } from './buffer.js';
24
+ import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
25
+
26
+ export async function handleMessage(msg) {
27
+ switch (msg.type) {
28
+ case 'registered':
29
+ if (msg.sessionKey) {
30
+ ctx.sessionKey = decodeKey(msg.sessionKey);
31
+ console.log('Encryption enabled');
32
+ }
33
+
34
+ // 只保存基本配置(不再保存 agentId,因为现在用 agentName 作为 ID)
35
+ ctx.saveConfig({
36
+ serverUrl: ctx.CONFIG.serverUrl,
37
+ agentName: ctx.CONFIG.agentName,
38
+ workDir: ctx.CONFIG.workDir,
39
+ reconnectInterval: ctx.CONFIG.reconnectInterval
40
+ // 不保存 agentSecret 到配置文件(安全考虑)
41
+ });
42
+ console.log(`Registered as agent: ${msg.agentId} (name: ${ctx.CONFIG.agentName})`);
43
+
44
+ // Check server-pushed upgrade notification
45
+ if (msg.upgradeAvailable) {
46
+ console.log(`\n Update available: ${ctx.agentVersion} → ${msg.upgradeAvailable}`);
47
+ console.log(` Run "yeaft-agent upgrade" to update\n`);
48
+ }
49
+
50
+ sendConversationList();
51
+
52
+ // ★ Flush 断连期间缓冲的消息
53
+ await flushMessageBuffer();
54
+
55
+ // ★ Phase 1: 通知 server 同步完成
56
+ sendToServer({ type: 'agent_sync_complete' });
57
+ break;
58
+
59
+ case 'create_conversation':
60
+ await createConversation(msg);
61
+ break;
62
+
63
+ case 'resume_conversation':
64
+ await resumeConversation(msg);
65
+ break;
66
+
67
+ case 'delete_conversation':
68
+ deleteConversation(msg);
69
+ break;
70
+
71
+ case 'get_conversations':
72
+ sendConversationList();
73
+ break;
74
+
75
+ case 'list_history_sessions':
76
+ await handleListHistorySessions(msg);
77
+ break;
78
+
79
+ case 'list_folders':
80
+ await handleListFolders(msg);
81
+ break;
82
+
83
+ case 'transfer_files':
84
+ await handleTransferFiles(msg);
85
+ break;
86
+
87
+ case 'execute':
88
+ await handleUserInput(msg);
89
+ break;
90
+
91
+ case 'cancel_execution':
92
+ await handleCancelExecution(msg);
93
+ break;
94
+
95
+ // clear_queue 和 cancel_queued_message 已移至 server 端管理 (Phase 3.6)
96
+
97
+ case 'refresh_conversation':
98
+ await handleRefreshConversation(msg);
99
+ break;
100
+
101
+ // Terminal (PTY) messages
102
+ case 'terminal_create':
103
+ await handleTerminalCreate(msg);
104
+ break;
105
+
106
+ case 'terminal_input':
107
+ handleTerminalInput(msg);
108
+ break;
109
+
110
+ case 'terminal_resize':
111
+ handleTerminalResize(msg);
112
+ break;
113
+
114
+ case 'terminal_close':
115
+ handleTerminalClose(msg);
116
+ break;
117
+
118
+ // File operation messages
119
+ case 'read_file':
120
+ await handleReadFile(msg);
121
+ break;
122
+
123
+ case 'write_file':
124
+ await handleWriteFile(msg);
125
+ break;
126
+
127
+ case 'list_directory':
128
+ await handleListDirectory(msg);
129
+ break;
130
+
131
+ case 'git_status':
132
+ await handleGitStatus(msg);
133
+ break;
134
+
135
+ case 'git_diff':
136
+ await handleGitDiff(msg);
137
+ break;
138
+
139
+ case 'git_add':
140
+ await handleGitAdd(msg);
141
+ break;
142
+
143
+ case 'git_reset':
144
+ await handleGitReset(msg);
145
+ break;
146
+
147
+ case 'git_restore':
148
+ await handleGitRestore(msg);
149
+ break;
150
+
151
+ case 'git_commit':
152
+ await handleGitCommit(msg);
153
+ break;
154
+
155
+ case 'git_push':
156
+ await handleGitPush(msg);
157
+ break;
158
+
159
+ case 'file_search':
160
+ await handleFileSearch(msg);
161
+ break;
162
+
163
+ case 'create_file':
164
+ await handleCreateFile(msg);
165
+ break;
166
+
167
+ case 'delete_files':
168
+ await handleDeleteFiles(msg);
169
+ break;
170
+
171
+ case 'move_files':
172
+ await handleMoveFiles(msg);
173
+ break;
174
+
175
+ case 'copy_files':
176
+ await handleCopyFiles(msg);
177
+ break;
178
+
179
+ case 'upload_to_dir':
180
+ await handleUploadToDir(msg);
181
+ break;
182
+
183
+ case 'update_conversation_settings':
184
+ handleUpdateConversationSettings(msg);
185
+ break;
186
+
187
+ case 'ask_user_answer':
188
+ handleAskUserAnswer(msg);
189
+ break;
190
+
191
+ // Crew (multi-agent) messages
192
+ case 'create_crew_session':
193
+ await createCrewSession(msg);
194
+ break;
195
+
196
+ case 'crew_human_input':
197
+ await handleCrewHumanInput(msg);
198
+ break;
199
+
200
+ case 'crew_control':
201
+ await handleCrewControl(msg);
202
+ break;
203
+
204
+ case 'crew_add_role':
205
+ await addRoleToSession(msg);
206
+ break;
207
+
208
+ case 'crew_remove_role':
209
+ await removeRoleFromSession(msg);
210
+ break;
211
+
212
+ case 'list_crew_sessions':
213
+ await handleListCrewSessions(msg);
214
+ break;
215
+
216
+ case 'check_crew_exists':
217
+ await handleCheckCrewExists(msg);
218
+ break;
219
+
220
+ case 'delete_crew_dir':
221
+ await handleDeleteCrewDir(msg);
222
+ break;
223
+
224
+ case 'resume_crew_session':
225
+ await resumeCrewSession(msg);
226
+ break;
227
+
228
+ case 'delete_crew_session':
229
+ await removeFromCrewIndex(msg.sessionId);
230
+ (await import('../conversation.js')).sendConversationList();
231
+ break;
232
+
233
+ case 'update_crew_session':
234
+ await (await import('../crew.js')).handleUpdateCrewSession(msg);
235
+ break;
236
+
237
+ case 'crew_load_history':
238
+ await handleLoadCrewHistory(msg);
239
+ break;
240
+
241
+ // Port proxy
242
+ case 'proxy_request':
243
+ handleProxyHttpRequest(msg);
244
+ break;
245
+
246
+ case 'proxy_ws_open':
247
+ handleProxyWsOpen(msg);
248
+ break;
249
+
250
+ case 'proxy_ws_message':
251
+ handleProxyWsMessage(msg);
252
+ break;
253
+
254
+ case 'proxy_ws_close':
255
+ handleProxyWsClose(msg);
256
+ break;
257
+
258
+ case 'proxy_update_ports':
259
+ ctx.proxyPorts = msg.ports || [];
260
+ sendToServer({ type: 'proxy_ports_update', ports: ctx.proxyPorts });
261
+ break;
262
+
263
+ case 'restart_agent':
264
+ handleRestartAgent();
265
+ break;
266
+
267
+ case 'upgrade_agent':
268
+ await handleUpgradeAgent();
269
+ break;
270
+ }
271
+ }
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const zlib = require('zlib');
5
+ const { execFileSync } = require('child_process');
6
+ const os = require('os');
7
+
8
+ const PKG = process.argv[2];
9
+ const TARGET = process.argv[3];
10
+ const LOGFILE = process.argv[4];
11
+
12
+ function log(msg) {
13
+ const line = '[Upgrade-Worker] ' + msg;
14
+ console.log(line);
15
+ try { fs.appendFileSync(LOGFILE, line + '\n'); } catch {}
16
+ }
17
+
18
+ function parseTar(buf) {
19
+ const files = [];
20
+ let offset = 0;
21
+ while (offset < buf.length - 512) {
22
+ const header = buf.slice(offset, offset + 512);
23
+ if (header.every(b => b === 0)) break;
24
+ const name = header.slice(0, 100).toString('utf8').replace(/\0.*/, '');
25
+ const sizeStr = header.slice(124, 136).toString('utf8').replace(/\0.*/, '').trim();
26
+ const size = parseInt(sizeStr, 8) || 0;
27
+ const typeFlag = header[156];
28
+ offset += 512;
29
+ if (size > 0) {
30
+ const data = buf.slice(offset, offset + size);
31
+ const relPath = name.replace(/^package\//, '');
32
+ if (typeFlag === 48 || typeFlag === 0) {
33
+ files.push({ path: relPath, data });
34
+ }
35
+ offset += Math.ceil(size / 512) * 512;
36
+ }
37
+ }
38
+ return files;
39
+ }
40
+
41
+ function rmDirContents(dir, keep) {
42
+ if (!fs.existsSync(dir)) return;
43
+ for (const entry of fs.readdirSync(dir)) {
44
+ if (keep && keep.includes(entry)) continue;
45
+ const full = path.join(dir, entry);
46
+ const stat = fs.statSync(full, { throwIfNoEntry: false });
47
+ if (!stat) continue;
48
+ if (stat.isDirectory()) {
49
+ fs.rmSync(full, { recursive: true, force: true });
50
+ } else {
51
+ fs.unlinkSync(full);
52
+ }
53
+ }
54
+ }
55
+
56
+ try {
57
+ log('Starting upgrade: ' + PKG + ' -> ' + TARGET);
58
+
59
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'yeaft-upgrade-'));
60
+ log('Temp dir: ' + tmpDir);
61
+
62
+ const packOutput = execFileSync('npm', ['pack', PKG, '--pack-destination', tmpDir], {
63
+ shell: true, encoding: 'utf8', cwd: tmpDir, timeout: 120000
64
+ }).trim();
65
+ const tgzName = packOutput.split('\n').pop().trim();
66
+ const tgzPath = path.join(tmpDir, tgzName);
67
+ log('Downloaded: ' + tgzPath);
68
+
69
+ const gzBuf = fs.readFileSync(tgzPath);
70
+ const tarBuf = zlib.gunzipSync(gzBuf);
71
+ const files = parseTar(tarBuf);
72
+ log('Extracted ' + files.length + ' files from archive');
73
+
74
+ log('Removing old files from: ' + TARGET);
75
+ rmDirContents(TARGET, ['node_modules']);
76
+
77
+ for (const f of files) {
78
+ const dest = path.join(TARGET, f.path);
79
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
80
+ fs.writeFileSync(dest, f.data);
81
+ }
82
+ log('Copied ' + files.length + ' files to target');
83
+
84
+ log('Installing dependencies...');
85
+ try {
86
+ execFileSync('npm', ['install', '--omit=dev'], {
87
+ shell: true, cwd: TARGET, encoding: 'utf8', timeout: 120000
88
+ });
89
+ log('Dependencies installed');
90
+ } catch (depErr) {
91
+ log('WARN: npm install deps failed: ' + depErr.message);
92
+ }
93
+
94
+ const newPkg = JSON.parse(fs.readFileSync(path.join(TARGET, 'package.json'), 'utf8'));
95
+ log('Upgrade complete. New version: ' + newPkg.version);
96
+
97
+ fs.rmSync(tmpDir, { recursive: true, force: true });
98
+ process.exit(0);
99
+ } catch (err) {
100
+ log('FATAL: ' + err.message);
101
+ log(err.stack || '');
102
+ process.exit(1);
103
+ }