@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,294 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import { writeFileSync, mkdirSync, existsSync, cpSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { platform } from 'os';
6
+ import ctx from '../context.js';
7
+ import { getConfigDir } from '../service.js';
8
+ import { sendToServer } from './buffer.js';
9
+ import { stopAgentHeartbeat } from './heartbeat.js';
10
+
11
+ // Shared cleanup logic for restart/upgrade
12
+ function cleanupAndExit(exitCode) {
13
+ setTimeout(() => {
14
+ for (const [, term] of ctx.terminals) {
15
+ if (term.pty) { try { term.pty.kill(); } catch {} }
16
+ if (term.timer) clearTimeout(term.timer);
17
+ }
18
+ ctx.terminals.clear();
19
+ for (const [, state] of ctx.conversations) {
20
+ if (state.abortController) state.abortController.abort();
21
+ if (state.inputStream) state.inputStream.done();
22
+ }
23
+ ctx.conversations.clear();
24
+ stopAgentHeartbeat();
25
+ if (ctx.ws) {
26
+ ctx.ws.removeAllListeners('close');
27
+ ctx.ws.close();
28
+ }
29
+ clearTimeout(ctx.reconnectTimer);
30
+ console.log(`[Agent] Cleanup done, exiting with code ${exitCode}...`);
31
+ process.exit(exitCode);
32
+ }, 500);
33
+ }
34
+
35
+ export function handleRestartAgent() {
36
+ console.log('[Agent] Restart requested, shutting down for PM2/systemd restart...');
37
+ sendToServer({ type: 'restart_agent_ack' });
38
+ cleanupAndExit(1);
39
+ }
40
+
41
+ export async function handleUpgradeAgent() {
42
+ console.log('[Agent] Upgrade requested, checking for updates...');
43
+ try {
44
+ const pkgName = ctx.pkgName || '@yeaft/webchat-agent';
45
+ // Check latest version (async to avoid blocking heartbeat)
46
+ const latestVersion = await new Promise((resolve, reject) => {
47
+ execFile('npm', ['view', pkgName, 'version'], { stdio: 'pipe', shell: true }, (err, stdout) => {
48
+ if (err) reject(err); else resolve(stdout.toString().trim());
49
+ });
50
+ });
51
+ if (latestVersion === ctx.agentVersion) {
52
+ console.log(`[Agent] Already at latest version (${ctx.agentVersion}), skipping upgrade.`);
53
+ sendToServer({ type: 'upgrade_agent_ack', success: true, alreadyLatest: true, version: ctx.agentVersion });
54
+ return;
55
+ }
56
+ console.log(`[Agent] Upgrading from ${ctx.agentVersion} to latest (${latestVersion})...`);
57
+
58
+ // 检测安装方式:npm install 的路径包含 node_modules,源码运行则不包含
59
+ const scriptPath = (process.argv[1] || '').replace(/\\/g, '/');
60
+ const nmIndex = scriptPath.lastIndexOf('/node_modules/');
61
+ const isNpmInstall = nmIndex !== -1;
62
+
63
+ if (!isNpmInstall) {
64
+ // 源码运行不支持远程升级(代码在 git repo 中,需要手动 git pull)
65
+ console.log('[Agent] Source-based install detected, remote upgrade not supported.');
66
+ sendToServer({ type: 'upgrade_agent_ack', success: false, error: 'Source-based install: please use git pull to upgrade' });
67
+ return;
68
+ }
69
+
70
+ // 提取 node_modules 的父目录
71
+ const installDir = scriptPath.substring(0, nmIndex);
72
+
73
+ // 判断全局安装 vs 局部安装
74
+ const isGlobalInstall = await new Promise((resolve) => {
75
+ execFile('npm', ['prefix', '-g'], { shell: true }, (err, stdout) => {
76
+ if (err) { resolve(false); return; }
77
+ const globalPrefix = stdout.toString().trim().replace(/\\/g, '/');
78
+ resolve(installDir === globalPrefix || installDir === globalPrefix + '/lib');
79
+ });
80
+ });
81
+
82
+ const isWindows = platform() === 'win32';
83
+
84
+ if (isWindows) {
85
+ spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion);
86
+ } else {
87
+ spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion);
88
+ }
89
+
90
+ // 清理并退出,让升级脚本接管
91
+ cleanupAndExit(0);
92
+ } catch (e) {
93
+ console.error('[Agent] Upgrade failed:', e.message);
94
+ sendToServer({ type: 'upgrade_agent_ack', success: false, error: e.message });
95
+ }
96
+ }
97
+
98
+ function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion) {
99
+ const pid = process.pid;
100
+ const configDir = getConfigDir();
101
+ mkdirSync(configDir, { recursive: true });
102
+ const logDir = join(configDir, 'logs');
103
+ mkdirSync(logDir, { recursive: true });
104
+ const batPath = join(configDir, 'upgrade.bat');
105
+ const logPath = join(logDir, 'upgrade.log');
106
+ const isPm2 = !!process.env.pm_id;
107
+ const installDirWin = installDir.replace(/\//g, '\\');
108
+
109
+ // Copy upgrade-worker-template.js to config dir (runs as CJS there, away from ESM context)
110
+ const thisDir = dirname(fileURLToPath(import.meta.url));
111
+ const workerSrc = join(thisDir, 'upgrade-worker-template.js');
112
+ const workerDst = join(configDir, 'upgrade-worker.js');
113
+ cpSync(workerSrc, workerDst);
114
+
115
+ // Determine the target package directory inside node_modules
116
+ const pkgDir = join(installDir, 'node_modules', ...pkgName.split('/')).replace(/\//g, '\\');
117
+
118
+ const batLines = [
119
+ '@echo off',
120
+ 'setlocal',
121
+ `set PID=${pid}`,
122
+ `set PKG=${pkgName}@latest`,
123
+ `set INSTALL_DIR=${installDirWin}`,
124
+ `set PKG_DIR=${pkgDir}`,
125
+ `set LOGFILE=${logPath}`,
126
+ `set WORKER=${workerDst}`,
127
+ `set MAX_WAIT=30`,
128
+ `set COUNT=0`,
129
+ '',
130
+ ':: Change to temp dir to avoid EBUSY on cwd',
131
+ 'cd /d "%TEMP%"',
132
+ '',
133
+ ':: Redirect all output to log file',
134
+ 'echo [Upgrade] Started at %date% %time% > "%LOGFILE%"',
135
+ ];
136
+
137
+ batLines.push(
138
+ ':WAIT_LOOP',
139
+ 'tasklist /FI "PID eq %PID%" 2>NUL | find /I "%PID%" >NUL',
140
+ 'if errorlevel 1 goto PID_EXITED',
141
+ 'set /A COUNT+=1',
142
+ 'if %COUNT% GEQ %MAX_WAIT% (',
143
+ ' echo [Upgrade] Timeout waiting for PID %PID% to exit after 60s >> "%LOGFILE%"',
144
+ ' goto PID_EXITED',
145
+ ')',
146
+ 'ping -n 3 127.0.0.1 >NUL',
147
+ 'goto WAIT_LOOP',
148
+ ':PID_EXITED',
149
+ );
150
+
151
+ if (isPm2) {
152
+ batLines.push(
153
+ 'echo [Upgrade] Stopping pm2 to prevent autorestart... >> "%LOGFILE%"',
154
+ 'call pm2 stop yeaft-agent >> "%LOGFILE%" 2>&1',
155
+ ':: Wait for pm2 to fully terminate the process and release file locks',
156
+ 'ping -n 6 127.0.0.1 >NUL',
157
+ );
158
+ }
159
+
160
+ // Use Node.js worker for file-level upgrade (avoids EBUSY on directory rename)
161
+ batLines.push(
162
+ 'echo [Upgrade] Running upgrade worker... >> "%LOGFILE%"',
163
+ 'node "%WORKER%" "%PKG%" "%PKG_DIR%" "%LOGFILE%"',
164
+ 'if not "%errorlevel%"=="0" (',
165
+ ' echo [Upgrade] Worker failed with exit code %errorlevel% >> "%LOGFILE%"',
166
+ ' goto CLEANUP',
167
+ ')',
168
+ 'echo [Upgrade] Worker completed successfully >> "%LOGFILE%"',
169
+ );
170
+
171
+ batLines.push(':CLEANUP');
172
+
173
+ if (isPm2) {
174
+ batLines.push(
175
+ 'echo [Upgrade] Starting agent via pm2... >> "%LOGFILE%"',
176
+ 'call pm2 start yeaft-agent >> "%LOGFILE%" 2>&1',
177
+ );
178
+ }
179
+
180
+ // Clean up worker and bat script
181
+ batLines.push(
182
+ `del /F /Q "${workerDst}" 2>NUL`,
183
+ `del /F /Q "${batPath}"`,
184
+ );
185
+
186
+ writeFileSync(batPath, batLines.join('\r\n'));
187
+ const child = spawn('cmd.exe', ['/c', batPath], {
188
+ detached: true,
189
+ stdio: 'ignore',
190
+ windowsHide: true
191
+ });
192
+ child.unref();
193
+ console.log(`[Agent] Spawned upgrade script (PID wait for ${pid}, pm2=${isPm2}, dir=${installDir}): ${batPath}`);
194
+ sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
195
+ }
196
+
197
+ function spawnUnixUpgradeScript(pkgName, installDir, isGlobalInstall, latestVersion) {
198
+ const pid = process.pid;
199
+ const configDir = getConfigDir();
200
+ mkdirSync(configDir, { recursive: true });
201
+ const shPath = join(configDir, 'upgrade.sh');
202
+ const isSystemd = existsSync(join(process.env.HOME || '', '.config', 'systemd', 'user', 'yeaft-agent.service'));
203
+ const isLaunchd = platform() === 'darwin' && existsSync(join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist'));
204
+ const cwd = isGlobalInstall ? undefined : installDir;
205
+
206
+ const shLines = [
207
+ '#!/bin/bash',
208
+ `PID=${pid}`,
209
+ `PKG="${pkgName}@latest"`,
210
+ `LOGFILE="${join(configDir, 'logs', 'upgrade.log')}"`,
211
+ `export PATH="${process.env.PATH}"`,
212
+ '',
213
+ '# Redirect all output to log file',
214
+ 'exec > "$LOGFILE" 2>&1',
215
+ 'echo "[Upgrade] Started at $(date)"',
216
+ '',
217
+ ...(cwd ? [`INSTALL_DIR="${cwd}"`] : []),
218
+ '',
219
+ '# Wait for current process to exit',
220
+ 'COUNT=0',
221
+ 'while kill -0 $PID 2>/dev/null; do',
222
+ ' COUNT=$((COUNT+1))',
223
+ ' if [ $COUNT -ge 30 ]; then',
224
+ ' echo "[Upgrade] Timeout waiting for PID $PID to exit"',
225
+ ' break',
226
+ ' fi',
227
+ ' sleep 2',
228
+ 'done',
229
+ '',
230
+ ];
231
+
232
+ // 停止服务管理器的自动重启
233
+ if (isSystemd) {
234
+ shLines.push(
235
+ '# Stop systemd service to prevent restart loop',
236
+ 'systemctl --user stop yeaft-agent 2>/dev/null',
237
+ 'sleep 1',
238
+ '',
239
+ );
240
+ } else if (isLaunchd) {
241
+ const plistPath = join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
242
+ shLines.push(
243
+ '# Unload launchd service to prevent restart loop',
244
+ `launchctl unload "${plistPath}" 2>/dev/null`,
245
+ 'sleep 1',
246
+ '',
247
+ );
248
+ }
249
+
250
+ // npm install
251
+ const npmCmd = isGlobalInstall
252
+ ? `npm install -g "$PKG"`
253
+ : `cd "$INSTALL_DIR" && npm install "$PKG"`;
254
+
255
+ shLines.push(
256
+ 'echo "[Upgrade] Installing $PKG..."',
257
+ npmCmd,
258
+ 'EXIT_CODE=$?',
259
+ 'if [ $EXIT_CODE -ne 0 ]; then',
260
+ ' echo "[Upgrade] npm install failed with exit code $EXIT_CODE"',
261
+ 'else',
262
+ ' echo "[Upgrade] Successfully installed $PKG"',
263
+ 'fi',
264
+ '',
265
+ );
266
+
267
+ // 重新启动服务
268
+ if (isSystemd) {
269
+ shLines.push(
270
+ '# Restart systemd service',
271
+ 'systemctl --user start yeaft-agent',
272
+ 'echo "[Upgrade] Service restarted via systemd"',
273
+ );
274
+ } else if (isLaunchd) {
275
+ const plistPath = join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
276
+ shLines.push(
277
+ '# Reload launchd service',
278
+ `launchctl load "${plistPath}"`,
279
+ 'echo "[Upgrade] Service restarted via launchd"',
280
+ );
281
+ }
282
+
283
+ // 清理脚本自身
284
+ shLines.push('', `rm -f "${shPath}"`);
285
+
286
+ writeFileSync(shPath, shLines.join('\n'), { mode: 0o755 });
287
+ const child = spawn('bash', [shPath], {
288
+ detached: true,
289
+ stdio: 'ignore',
290
+ });
291
+ child.unref();
292
+ console.log(`[Agent] Spawned upgrade script: ${shPath}`);
293
+ sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
294
+ }