@yeaft/webchat-agent 0.0.20 → 0.0.21

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.
Files changed (2) hide show
  1. package/connection.js +122 -9
  2. package/package.json +1 -1
package/connection.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import WebSocket from 'ws';
2
- import { execSync, execFile } from 'child_process';
2
+ import { execSync, execFile, spawn } from 'child_process';
3
+ import { writeFileSync, mkdirSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { platform } from 'os';
3
6
  import ctx from './context.js';
7
+ import { getConfigDir } from './service.js';
4
8
  import { encrypt, decrypt, isEncrypted, decodeKey } from './encryption.js';
5
9
  import { handleTerminalCreate, handleTerminalInput, handleTerminalResize, handleTerminalClose } from './terminal.js';
6
10
  import { handleProxyHttpRequest, handleProxyWsOpen, handleProxyWsMessage, handleProxyWsClose } from './proxy.js';
@@ -321,15 +325,124 @@ async function handleMessage(msg) {
321
325
  return;
322
326
  }
323
327
  console.log(`[Agent] Upgrading from ${ctx.agentVersion} to ${latestVersion}...`);
324
- // Use async execFile to avoid blocking event loop (heartbeat must keep running)
325
- await new Promise((resolve, reject) => {
326
- execFile('npm', ['install', '-g', `${pkgName}@latest`], { stdio: 'pipe', shell: true }, (err) => {
327
- if (err) reject(err); else resolve();
328
+
329
+ // 检测安装方式:npm install 的路径包含 node_modules,源码运行则不包含
330
+ const scriptPath = (process.argv[1] || '').replace(/\\/g, '/');
331
+ const nmIndex = scriptPath.lastIndexOf('/node_modules/');
332
+ const isNpmInstall = nmIndex !== -1;
333
+
334
+ if (!isNpmInstall) {
335
+ // 源码运行不支持远程升级(代码在 git repo 中,需要手动 git pull)
336
+ console.log('[Agent] Source-based install detected, remote upgrade not supported.');
337
+ sendToServer({ type: 'upgrade_agent_ack', success: false, error: 'Source-based install: please use git pull to upgrade' });
338
+ return;
339
+ }
340
+
341
+ // 提取 node_modules 的父目录(即 npm install 执行时的项目目录或全局 prefix)
342
+ // 例如 /usr/lib/node_modules/@yeaft/webchat-agent/cli.js → /usr/lib
343
+ // 例如 C:/Users/x/myproject/node_modules/@yeaft/webchat-agent/cli.js → C:/Users/x/myproject
344
+ const installDir = scriptPath.substring(0, nmIndex);
345
+
346
+ // 判断全局安装 vs 局部安装:全局安装的 installDir 就是 npm global prefix
347
+ const isGlobalInstall = await new Promise((resolve) => {
348
+ execFile('npm', ['prefix', '-g'], { shell: true }, (err, stdout) => {
349
+ if (err) { resolve(false); return; }
350
+ const globalPrefix = stdout.toString().trim().replace(/\\/g, '/');
351
+ resolve(installDir === globalPrefix);
328
352
  });
329
353
  });
330
- console.log('[Agent] Upgrade successful, restarting...');
331
- sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion });
332
- // Restart after upgrade (same as restart_agent)
354
+
355
+ const isWindows = platform() === 'win32';
356
+ // 全局安装用 npm install -g,局部安装在 installDir 下 npm install
357
+ const npmArgs = isGlobalInstall
358
+ ? ['install', '-g', `${pkgName}@${latestVersion}`]
359
+ : ['install', `${pkgName}@${latestVersion}`];
360
+
361
+ if (isWindows) {
362
+ // Windows: 进程持有文件锁,npm install 无法覆盖正在运行的模块文件 (EBUSY)
363
+ // 策略:生成 detached bat 脚本,等当前进程退出后再 npm install
364
+ const pid = process.pid;
365
+ const configDir = getConfigDir();
366
+ mkdirSync(configDir, { recursive: true });
367
+ const batPath = join(configDir, 'upgrade.bat');
368
+ const isPm2 = !!process.env.pm_id;
369
+ const installDirWin = installDir.replace(/\//g, '\\');
370
+
371
+ const batLines = [
372
+ '@echo off',
373
+ 'setlocal',
374
+ `set PID=${pid}`,
375
+ `set PKG=${pkgName}@${latestVersion}`,
376
+ `set INSTALL_DIR=${installDirWin}`,
377
+ `set MAX_WAIT=30`,
378
+ `set COUNT=0`,
379
+ ':WAIT_LOOP',
380
+ 'tasklist /FI "PID eq %PID%" 2>NUL | find /I "%PID%" >NUL',
381
+ 'if errorlevel 1 goto PID_EXITED',
382
+ 'set /A COUNT+=1',
383
+ 'if %COUNT% GEQ %MAX_WAIT% (',
384
+ ' echo [Upgrade] Timeout waiting for PID %PID% to exit after 60s',
385
+ ' goto PID_EXITED',
386
+ ')',
387
+ 'timeout /T 2 /NOBREAK >NUL',
388
+ 'goto WAIT_LOOP',
389
+ ':PID_EXITED',
390
+ ];
391
+
392
+ if (isPm2) {
393
+ // pm2 可能已自动重启旧代码(exit 1 触发),先 stop 释放文件锁
394
+ batLines.push(
395
+ 'echo [Upgrade] Stopping pm2 agent to release file locks...',
396
+ 'call pm2 stop yeaft-agent 2>NUL',
397
+ 'timeout /T 3 /NOBREAK >NUL',
398
+ );
399
+ }
400
+
401
+ const npmBatCmd = isGlobalInstall
402
+ ? 'call npm install -g %PKG%'
403
+ : 'cd /d "%INSTALL_DIR%" && call npm install %PKG%';
404
+
405
+ batLines.push(
406
+ 'echo [Upgrade] Installing %PKG%...',
407
+ npmBatCmd,
408
+ 'if errorlevel 1 (',
409
+ ' echo [Upgrade] npm install failed with exit code %errorlevel%',
410
+ ') else (',
411
+ ' echo [Upgrade] Successfully installed %PKG%',
412
+ ')',
413
+ );
414
+
415
+ if (isPm2) {
416
+ batLines.push(
417
+ 'echo [Upgrade] Starting agent via pm2...',
418
+ 'call pm2 start yeaft-agent',
419
+ );
420
+ }
421
+
422
+ batLines.push(`del /F /Q "${batPath}"`);
423
+
424
+ writeFileSync(batPath, batLines.join('\r\n'));
425
+ const child = spawn('cmd.exe', ['/c', batPath], {
426
+ detached: true,
427
+ stdio: 'ignore',
428
+ windowsHide: true
429
+ });
430
+ child.unref();
431
+ console.log(`[Agent] Spawned upgrade script (PID wait for ${pid}, pm2=${isPm2}, dir=${installDir}): ${batPath}`);
432
+ sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion, pendingRestart: true });
433
+ } else {
434
+ // Linux/macOS: rename 不受文件锁影响,直接升级
435
+ const cwd = isGlobalInstall ? undefined : installDir;
436
+ await new Promise((resolve, reject) => {
437
+ execFile('npm', npmArgs, { cwd, stdio: 'pipe', shell: true }, (err) => {
438
+ if (err) reject(err); else resolve();
439
+ });
440
+ });
441
+ console.log('[Agent] Upgrade successful, restarting...');
442
+ sendToServer({ type: 'upgrade_agent_ack', success: true, version: latestVersion });
443
+ }
444
+
445
+ // 清理并退出
333
446
  setTimeout(() => {
334
447
  for (const [, term] of ctx.terminals) {
335
448
  if (term.pty) { try { term.pty.kill(); } catch {} }
@@ -347,7 +460,7 @@ async function handleMessage(msg) {
347
460
  ctx.ws.close();
348
461
  }
349
462
  clearTimeout(ctx.reconnectTimer);
350
- console.log('[Agent] Cleanup done, exiting for auto-restart...');
463
+ console.log('[Agent] Cleanup done, exiting...');
351
464
  process.exit(1);
352
465
  }, 500);
353
466
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",