@yeaft/webchat-agent 0.0.234 → 0.0.236
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/buffer.js +87 -0
- package/connection/heartbeat.js +47 -0
- package/connection/index.js +89 -0
- package/connection/message-router.js +271 -0
- package/connection/upgrade-worker-template.js +103 -0
- package/connection/upgrade.js +294 -0
- package/crew/control.js +364 -0
- package/crew/human-interaction.js +115 -0
- package/crew/persistence.js +287 -0
- package/crew/role-management.js +131 -0
- package/crew/role-output.js +315 -0
- package/crew/role-query.js +309 -0
- package/crew/routing.js +194 -0
- package/crew/session.js +474 -0
- package/crew/shared-dir.js +116 -0
- package/crew/task-files.js +370 -0
- package/crew/ui-messages.js +246 -0
- package/crew/worktree.js +130 -0
- package/package.json +6 -2
- package/service/config.js +133 -0
- package/service/index.js +99 -0
- package/service/linux.js +111 -0
- package/service/macos.js +137 -0
- package/service/windows.js +181 -0
- package/workbench/file-ops.js +436 -0
- package/workbench/file-search.js +66 -0
- package/workbench/git-ops.js +313 -0
- package/workbench/transfer.js +99 -0
- package/workbench/utils.js +41 -0
|
@@ -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
|
+
}
|