@myassis/gateway 1.0.26 → 1.0.28

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/dist/main.js CHANGED
@@ -209,38 +209,50 @@ else {
209
209
  app.use(errorHandler_js_1.errorHandler);
210
210
  // 创建 HTTP Server(而非 app.listen,以便挂载 WebSocket)
211
211
  const server = http_1.default.createServer(app);
212
- // 初始化 WebSocket 服务
213
- WebSocketService_js_1.webSocketService.initialize(server);
214
- // 启动任务调度器
215
- TaskSchedulerService_js_1.taskSchedulerService.start();
216
212
  // 端口自动顺延:被占用时自动尝试下一个端口
213
+ const configuredPort = index_js_1.appConfig.port;
214
+ let schedulerStarted = false;
215
+ let webSocketInitialized = false;
217
216
  const startServer = (port) => {
218
- server.listen(port, () => {
219
- // 动态更新 appConfig.port(供 WebSocket 广播实际端口)
220
- index_js_1.appConfig.port = port;
221
- if (port !== index_js_1.appConfig.port) {
222
- logger.info(`Port ${index_js_1.appConfig.port} is in use, fallback to port ${port}`);
223
- }
224
- logger.info(`我的助手 Gateway Service running on port ${port}`);
225
- }).on('error', (err) => {
217
+ const handleListenError = (err) => {
218
+ server.removeListener('error', handleListenError);
226
219
  if (err.code === 'EADDRINUSE') {
227
220
  const nextPort = port + 1;
228
- if (nextPort <= (index_js_1.appConfig.port + 10)) {
221
+ if (nextPort <= (configuredPort + 10)) {
229
222
  logger.warn(`Port ${port} is in use, trying ${nextPort}...`);
230
223
  startServer(nextPort);
231
224
  }
232
225
  else {
233
- logger.error(`All ports from ${index_js_1.appConfig.port} to ${index_js_1.appConfig.port + 10} are in use`);
226
+ logger.error(`All ports from ${configuredPort} to ${configuredPort + 10} are in use`);
234
227
  process.exit(1);
235
228
  }
229
+ return;
236
230
  }
237
- else {
238
- logger.error(`Server error: ${err.message}`);
239
- process.exit(1);
231
+ logger.error(`Server error: ${err.message}`);
232
+ process.exit(1);
233
+ };
234
+ server.once('error', handleListenError);
235
+ server.listen(port, () => {
236
+ server.removeListener('error', handleListenError);
237
+ if (port !== configuredPort) {
238
+ logger.info(`Port ${configuredPort} is in use, fallback to port ${port}`);
239
+ }
240
+ // 动态更新 appConfig.port(供 WebSocket 广播实际端口)
241
+ index_js_1.appConfig.port = port;
242
+ // HTTP Server 监听成功后再初始化 WebSocket,避免端口冲突时 WebSocketServer 误报 EADDRINUSE
243
+ if (!webSocketInitialized) {
244
+ WebSocketService_js_1.webSocketService.initialize(server);
245
+ webSocketInitialized = true;
246
+ }
247
+ // HTTP Server 启动成功后再启动任务调度器,避免端口占用时重复启动后台任务
248
+ if (!schedulerStarted) {
249
+ TaskSchedulerService_js_1.taskSchedulerService.start();
250
+ schedulerStarted = true;
240
251
  }
252
+ logger.info(`我的助手 Gateway Service running on port ${port}`);
241
253
  });
242
254
  };
243
- startServer(index_js_1.appConfig.port);
255
+ startServer(configuredPort);
244
256
  // 优雅关闭
245
257
  process.on('SIGTERM', () => {
246
258
  logger.info('收到 SIGTERM,正在关闭...');
@@ -189,21 +189,21 @@ async function installLinux() {
189
189
  if (installed)
190
190
  await stopService();
191
191
  const execStart = isPkg ? exe : `${exe} ${script}`;
192
- const unitContent = `[Unit]
193
- Description=${exports.SERVICE_DISPLAY_NAME}
194
- After=network.target
195
-
196
- [Service]
197
- Type=simple
198
- User=${process.env.USER || 'root'}
199
- WorkingDirectory=${workDir}
200
- ExecStart=${execStart}
201
- Restart=always
202
- RestartSec=5
203
- Environment=NODE_ENV=production
204
-
205
- [Install]
206
- WantedBy=multi-user.target
192
+ const unitContent = `[Unit]
193
+ Description=${exports.SERVICE_DISPLAY_NAME}
194
+ After=network.target
195
+
196
+ [Service]
197
+ Type=simple
198
+ User=${process.env.USER || 'root'}
199
+ WorkingDirectory=${workDir}
200
+ ExecStart=${execStart}
201
+ Restart=always
202
+ RestartSec=5
203
+ Environment=NODE_ENV=production
204
+
205
+ [Install]
206
+ WantedBy=multi-user.target
207
207
  `;
208
208
  await fs_1.default.promises.writeFile('/tmp/myassis-gateway.service', unitContent, 'utf8');
209
209
  await execAsync('cp /tmp/myassis-gateway.service /etc/systemd/system/myassis-gateway.service', { timeout: 10000 });
@@ -381,7 +381,7 @@ async function updateService() {
381
381
  if (isPackagedExe()) {
382
382
  // pkg 环境:从 COS 下载对应平台的 exe
383
383
  // ❗ 关键:不能在这里 stopService,进程本身即为服务进程,停止后后续代码不执行
384
- // 方案:下载到临时文件后,spawn 一个独立延迟替换进程,本进程直接返回
384
+ // 方案:下载到临时文件后,派发一个独立延迟替换脚本,本进程直接返回
385
385
  const platform = process.platform === 'win32' ? 'win' : 'linux';
386
386
  const isWin = platform === 'win';
387
387
  const downloadUrl = isWin
@@ -404,44 +404,123 @@ async function updateService() {
404
404
  response.data.on('error', reject);
405
405
  });
406
406
  console.log('✅ 下载完成');
407
- // 生成延迟替换脚本,spawn 为独立进程(当前进程 return 后不影响它)
407
+ // 生成延迟替换脚本,Windows 通过 nssm 管理服务并使用 execAsync 派发脚本
408
408
  if (isWin) {
409
+ const nssmPath = getNssmPath();
410
+ if (!nssmPath || !fs_1.default.existsSync(nssmPath)) {
411
+ throw new Error('未找到 nssm.exe,无法执行 Windows 服务更新');
412
+ }
413
+ const escapePowerShellString = (value) => value.replace(/'/g, "''");
414
+ const updateScriptPath = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-update.ps1');
409
415
  const psScript = [
410
- `Start-Sleep -Seconds 5`,
411
- `Stop-Service -Name '${exports.SERVICE_NAME}' -ErrorAction SilentlyContinue`,
412
- `Start-Sleep -Seconds 2`,
413
- `while (Test-Path '${currentExe}') { try { Move-Item '${currentExe}' '${currentExe}.bak' -Force; break } catch { Start-Sleep -Seconds 1 } }`,
414
- `Move-Item '${tmpExe}' '${currentExe}' -Force`,
415
- `Start-Sleep -Seconds 1`,
416
- `Start-Service -Name '${exports.SERVICE_NAME}'`,
417
- `Remove-Item '${currentExe}.bak' -Force -ErrorAction SilentlyContinue`,
418
- `Remove-Item '${tmpExe}' -Force -ErrorAction SilentlyContinue`,
419
- ].join('; ');
420
- const args = ['-NoProfile', '-NonInteractive', '-Command', psScript];
421
- const child = (0, child_process_1.spawn)('powershell.exe', args, {
422
- detached: true,
423
- stdio: 'ignore',
424
- windowsHide: true,
416
+ `$ErrorActionPreference = 'Stop'`,
417
+ `$nssm = '${escapePowerShellString(nssmPath)}'`,
418
+ `$serviceName = '${escapePowerShellString(exports.SERVICE_NAME)}'`,
419
+ `$currentExe = '${escapePowerShellString(currentExe)}'`,
420
+ `$tmpExe = '${escapePowerShellString(tmpExe)}'`,
421
+ `$backupExe = '${escapePowerShellString(`${currentExe}.bak`)}'`,
422
+ `$logPath = Join-Path $env:TEMP 'myassis-gateway-update.log'`,
423
+ `$logDir = Split-Path -Parent $logPath`,
424
+ `if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }`,
425
+ `function Write-UpdateLog { param([string]$Message) Add-Content -Path $logPath -Encoding UTF8 -Value \"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message\" }`,
426
+ `try {`,
427
+ ` Write-UpdateLog 'update script started'`,
428
+ ` Start-Sleep -Seconds 5`,
429
+ ` Write-UpdateLog 'stopping service by nssm'`,
430
+ ` & $nssm stop $serviceName | Out-Null`,
431
+ ` Start-Sleep -Seconds 2`,
432
+ ` Write-UpdateLog 'waiting executable unlock'`,
433
+ ` $moved = $false`,
434
+ ` for ($i = 0; $i -lt 60; $i++) {`,
435
+ ` try {`,
436
+ ` if (Test-Path $backupExe) { Remove-Item $backupExe -Force -ErrorAction SilentlyContinue }`,
437
+ ` Move-Item $currentExe $backupExe -Force`,
438
+ ` $moved = $true`,
439
+ ` break`,
440
+ ` } catch {`,
441
+ ` Start-Sleep -Seconds 1`,
442
+ ` }`,
443
+ ` }`,
444
+ ` if (-not $moved) { throw '等待旧版本可执行文件解锁超时' }`,
445
+ ` Write-UpdateLog 'moving new executable'`,
446
+ ` Move-Item $tmpExe $currentExe -Force`,
447
+ ` Start-Sleep -Seconds 1`,
448
+ ` Write-UpdateLog 'starting service by nssm'`,
449
+ ` & $nssm start $serviceName | Out-Null`,
450
+ ` Remove-Item $backupExe -Force -ErrorAction SilentlyContinue`,
451
+ ` Remove-Item $tmpExe -Force -ErrorAction SilentlyContinue`,
452
+ ` Write-UpdateLog 'update script finished'`,
453
+ `} catch {`,
454
+ ` Write-UpdateLog \"update script failed: $($_.Exception.Message)\"`,
455
+ ` try { & $nssm start $serviceName | Out-Null } catch { Write-UpdateLog \"restart service failed: $($_.Exception.Message)\" }`,
456
+ ` throw`,
457
+ `}`,
458
+ ].join('\r\n');
459
+ fs_1.default.writeFileSync(updateScriptPath, `\uFEFF${psScript}`, 'utf8');
460
+ await execAsync(`cmd.exe /c start \"\" powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"${updateScriptPath}\"`, {
461
+ timeout: 10000,
462
+ windowsHide: true
425
463
  });
426
- child.unref();
427
464
  }
428
465
  else {
429
- // Linux: bash 延迟替换
466
+ // Linux: 生成独立更新脚本,优先通过 systemd-run 派发,避免当前服务停止时更新脚本被一并终止
467
+ const escapeShellString = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
468
+ const updateScriptPath = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-update.sh');
469
+ const updateLogPath = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-update.log');
470
+ const backupExe = `${currentExe}.bak`;
430
471
  const bashScript = [
472
+ '#!/usr/bin/env bash',
473
+ 'set -euo pipefail',
474
+ `SERVICE_NAME=${escapeShellString(exports.SERVICE_NAME)}`,
475
+ `CURRENT_EXE=${escapeShellString(currentExe)}`,
476
+ `TMP_EXE=${escapeShellString(tmpExe)}`,
477
+ `BACKUP_EXE=${escapeShellString(backupExe)}`,
478
+ `LOG_PATH=${escapeShellString(updateLogPath)}`,
479
+ 'mkdir -p "$(dirname "$LOG_PATH")"',
480
+ 'log() { echo "$(date \'+%Y-%m-%d %H:%M:%S\') $*" >> "$LOG_PATH"; }',
481
+ 'log "update script started"',
431
482
  'sleep 5',
432
- `systemctl stop ${exports.SERVICE_NAME} 2>/dev/null || true`,
483
+ 'log "stopping service by systemctl"',
484
+ 'systemctl stop "$SERVICE_NAME" >> "$LOG_PATH" 2>&1 || true',
433
485
  'sleep 2',
434
- `mv -f '${currentExe}' '${currentExe}.bak' 2>/dev/null; true`,
435
- `mv -f '${tmpExe}' '${currentExe}'`,
486
+ 'log "waiting executable unlock"',
487
+ 'moved=0',
488
+ 'for i in $(seq 1 60); do',
489
+ ' if [ -f "$BACKUP_EXE" ]; then rm -f "$BACKUP_EXE" >> "$LOG_PATH" 2>&1 || true; fi',
490
+ ' if mv -f "$CURRENT_EXE" "$BACKUP_EXE" >> "$LOG_PATH" 2>&1; then',
491
+ ' moved=1',
492
+ ' break',
493
+ ' fi',
494
+ ' sleep 1',
495
+ 'done',
496
+ 'if [ "$moved" != "1" ]; then',
497
+ ' log "waiting old executable unlock timeout"',
498
+ ' systemctl start "$SERVICE_NAME" >> "$LOG_PATH" 2>&1 || true',
499
+ ' exit 1',
500
+ 'fi',
501
+ 'log "moving new executable"',
502
+ 'mv -f "$TMP_EXE" "$CURRENT_EXE" >> "$LOG_PATH" 2>&1',
503
+ 'chmod +x "$CURRENT_EXE" >> "$LOG_PATH" 2>&1 || true',
436
504
  'sleep 1',
437
- `systemctl start ${exports.SERVICE_NAME} 2>/dev/null || true`,
438
- `rm -f '${currentExe}.bak' '${tmpExe}'`,
439
- ].join('; ');
440
- const child = (0, child_process_1.spawn)('bash', ['-c', bashScript], {
441
- detached: true,
442
- stdio: 'ignore',
443
- });
444
- child.unref();
505
+ 'log "starting service by systemctl"',
506
+ 'systemctl start "$SERVICE_NAME" >> "$LOG_PATH" 2>&1 || true',
507
+ 'rm -f "$BACKUP_EXE" "$TMP_EXE" >> "$LOG_PATH" 2>&1 || true',
508
+ 'log "update script finished"',
509
+ ].join('\n');
510
+ fs_1.default.writeFileSync(updateScriptPath, bashScript, 'utf8');
511
+ fs_1.default.chmodSync(updateScriptPath, 0o755);
512
+ const escapedUpdateScriptPath = escapeShellString(updateScriptPath);
513
+ try {
514
+ await execAsync(`systemd-run --unit=myassis-gateway-update --description="Myassis Gateway Update" /bin/bash ${escapedUpdateScriptPath}`, {
515
+ timeout: 10000,
516
+ });
517
+ }
518
+ catch (err) {
519
+ logger.warn(`systemd-run 启动更新脚本失败,回退到 nohup setsid: ${err.message}`);
520
+ await execAsync(`nohup setsid /bin/bash ${escapedUpdateScriptPath} >/dev/null 2>&1 < /dev/null &`, {
521
+ timeout: 10000,
522
+ });
523
+ }
445
524
  }
446
525
  console.log('✅ 后台替换进程已启动,服务将在几秒后自动重启');
447
526
  return { success: true, message: `更新成功: ${check.currentVersion} → ${check.latestVersion},服务正在后台替换...` };
@@ -501,6 +501,9 @@ class Session {
501
501
  }
502
502
  };
503
503
  const toolCalls = [];
504
+ // 获取本地工具定义
505
+ const tools = (0, index_js_1.getToolDefinitions)();
506
+ const models = (await dataService_js_1.modelsService.list(token)).data.map(x => (0, models_js_1.toModel)(x));
504
507
  // 递归处理函数(支持多轮工具调用)
505
508
  const processModelResponse = async () => {
506
509
  if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
@@ -547,9 +550,6 @@ class Session {
547
550
  }
548
551
  }
549
552
  }
550
- // 获取本地工具定义
551
- const tools = (0, index_js_1.getToolDefinitions)();
552
- const models = (await dataService_js_1.modelsService.list(token)).data.map(x => (0, models_js_1.toModel)(x));
553
553
  // 使用 LLMClient 进行流式调用
554
554
  if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
555
555
  throw Error('aborted');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {