@yeaft/webchat-agent 0.0.10 → 0.0.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -44,7 +44,8 @@
44
44
  "workbench.js",
45
45
  "history.js",
46
46
  "encryption.js",
47
- "sdk/"
47
+ "sdk/",
48
+ "scripts/agent-tray.ps1"
48
49
  ],
49
50
  "license": "MIT",
50
51
  "author": "Yeaft",
@@ -0,0 +1,134 @@
1
+ # Claude Agent 托盘管理器
2
+ # 右键托盘图标可以查看日志、重启、停止等
3
+
4
+ Add-Type -AssemblyName System.Windows.Forms
5
+ Add-Type -AssemblyName System.Drawing
6
+
7
+ $PM2AppName = "yeaft-agent"
8
+
9
+ # 创建托盘图标
10
+ $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
11
+ $notifyIcon.Text = "Yeaft Agent"
12
+ $notifyIcon.Visible = $true
13
+
14
+ # 创建 Claude 风格图标(橙色圆形 + 白色 C)
15
+ function Create-ClaudeIcon {
16
+ $size = 32
17
+ $bitmap = New-Object System.Drawing.Bitmap($size, $size)
18
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
19
+ $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
20
+ $graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias
21
+
22
+ # 橙色背景圆形 (Claude 的品牌色)
23
+ $orangeBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(255, 204, 102, 51))
24
+ $graphics.FillEllipse($orangeBrush, 1, 1, $size - 2, $size - 2)
25
+
26
+ # 白色 "C" 字母
27
+ $whiteBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
28
+ $font = New-Object System.Drawing.Font("Arial", 16, [System.Drawing.FontStyle]::Bold)
29
+ # 测量文字大小并精确居中
30
+ $textSize = $graphics.MeasureString("C", $font)
31
+ $x = ($size - $textSize.Width) / 2
32
+ $y = ($size - $textSize.Height) / 2
33
+ $graphics.DrawString("C", $font, $whiteBrush, $x, $y)
34
+
35
+ $graphics.Dispose()
36
+
37
+ # 转换为图标
38
+ $hIcon = $bitmap.GetHicon()
39
+ $icon = [System.Drawing.Icon]::FromHandle($hIcon)
40
+ return $icon
41
+ }
42
+
43
+ $notifyIcon.Icon = Create-ClaudeIcon
44
+
45
+ # 创建右键菜单
46
+ $contextMenu = New-Object System.Windows.Forms.ContextMenuStrip
47
+
48
+ # 查看状态
49
+ $menuStatus = New-Object System.Windows.Forms.ToolStripMenuItem
50
+ $menuStatus.Text = "View Status"
51
+ $menuStatus.Add_Click({
52
+ Start-Process "powershell" -ArgumentList "-NoExit -Command `"pm2 status`""
53
+ })
54
+ $contextMenu.Items.Add($menuStatus)
55
+
56
+ # 查看日志
57
+ $menuLogs = New-Object System.Windows.Forms.ToolStripMenuItem
58
+ $menuLogs.Text = "View Logs"
59
+ $menuLogs.Add_Click({
60
+ Start-Process "powershell" -ArgumentList "-NoExit -Command `"pm2 logs $PM2AppName --lines 100`""
61
+ })
62
+ $contextMenu.Items.Add($menuLogs)
63
+
64
+ # 分隔线
65
+ $contextMenu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
66
+
67
+ # 重启
68
+ $menuRestart = New-Object System.Windows.Forms.ToolStripMenuItem
69
+ $menuRestart.Text = "Restart Agent"
70
+ $menuRestart.Add_Click({
71
+ pm2 restart $PM2AppName
72
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Agent restarted", [System.Windows.Forms.ToolTipIcon]::Info)
73
+ })
74
+ $contextMenu.Items.Add($menuRestart)
75
+
76
+ # 停止
77
+ $menuStop = New-Object System.Windows.Forms.ToolStripMenuItem
78
+ $menuStop.Text = "Stop Agent"
79
+ $menuStop.Add_Click({
80
+ pm2 stop $PM2AppName
81
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Agent stopped", [System.Windows.Forms.ToolTipIcon]::Warning)
82
+ })
83
+ $contextMenu.Items.Add($menuStop)
84
+
85
+ # 启动
86
+ $menuStart = New-Object System.Windows.Forms.ToolStripMenuItem
87
+ $menuStart.Text = "Start Agent"
88
+ $menuStart.Add_Click({
89
+ pm2 start $PM2AppName
90
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Agent started", [System.Windows.Forms.ToolTipIcon]::Info)
91
+ })
92
+ $contextMenu.Items.Add($menuStart)
93
+
94
+ # 分隔线
95
+ $contextMenu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
96
+
97
+ # 打开日志文件夹
98
+ $menuOpenLogs = New-Object System.Windows.Forms.ToolStripMenuItem
99
+ $menuOpenLogs.Text = "Open Logs Folder"
100
+ $menuOpenLogs.Add_Click({
101
+ $logDir = Join-Path $env:APPDATA "yeaft-agent\logs"
102
+ if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
103
+ Start-Process "explorer" -ArgumentList $logDir
104
+ })
105
+ $contextMenu.Items.Add($menuOpenLogs)
106
+
107
+ # 分隔线
108
+ $contextMenu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
109
+
110
+ # 退出(停止 agent + 关闭托盘)
111
+ $menuExit = New-Object System.Windows.Forms.ToolStripMenuItem
112
+ $menuExit.Text = "Exit"
113
+ $menuExit.Add_Click({
114
+ pm2 stop $PM2AppName 2>$null
115
+ $notifyIcon.ShowBalloonTip(1000, "Yeaft Agent", "Agent stopped", [System.Windows.Forms.ToolTipIcon]::Info)
116
+ Start-Sleep -Milliseconds 500
117
+ $notifyIcon.Visible = $false
118
+ $notifyIcon.Dispose()
119
+ [System.Windows.Forms.Application]::Exit()
120
+ })
121
+ $contextMenu.Items.Add($menuExit)
122
+
123
+ $notifyIcon.ContextMenuStrip = $contextMenu
124
+
125
+ # 双击打开日志
126
+ $notifyIcon.Add_DoubleClick({
127
+ Start-Process "powershell" -ArgumentList "-NoExit -Command `"pm2 logs $PM2AppName`""
128
+ })
129
+
130
+ # 显示启动提示
131
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Tray manager started. Right-click for options.", [System.Windows.Forms.ToolTipIcon]::Info)
132
+
133
+ # 保持运行
134
+ [System.Windows.Forms.Application]::Run()
package/service.js CHANGED
@@ -371,80 +371,98 @@ function macLogs() {
371
371
  // ─── Windows (Task Scheduler) ────────────────────────────────
372
372
 
373
373
  const WIN_TASK_NAME = 'YeaftAgent';
374
+ const PM2_APP_NAME = 'yeaft-agent';
374
375
 
375
- function getWinWrapperPath() {
376
- return join(getConfigDir(), 'run.vbs');
376
+ // Legacy paths for cleanup
377
+ function getWinWrapperPath() { return join(getConfigDir(), 'run.vbs'); }
378
+ function getWinBatPath() { return join(getConfigDir(), 'run.bat'); }
379
+
380
+ function ensurePm2() {
381
+ try {
382
+ execSync('pm2 --version', { stdio: 'pipe' });
383
+ } catch {
384
+ console.log('Installing pm2...');
385
+ execSync('npm install -g pm2', { stdio: 'inherit' });
386
+ }
377
387
  }
378
388
 
379
- function getWinBatPath() {
380
- return join(getConfigDir(), 'run.bat');
389
+ function getEcosystemPath() {
390
+ return join(getConfigDir(), 'ecosystem.config.cjs');
381
391
  }
382
392
 
383
- function winInstall(config) {
393
+ function generateEcosystem(config) {
384
394
  const nodePath = getNodePath();
385
395
  const cliPath = getCliPath();
396
+ const cliDir = dirname(cliPath);
386
397
  const logDir = getLogDir();
387
398
 
388
- // Build environment variable settings for the batch file
389
- const envLines = [];
390
- if (config.serverUrl) envLines.push(`set "SERVER_URL=${config.serverUrl}"`);
391
- if (config.agentName) envLines.push(`set "AGENT_NAME=${config.agentName}"`);
392
- if (config.agentSecret) envLines.push(`set "AGENT_SECRET=${config.agentSecret}"`);
393
- if (config.workDir) envLines.push(`set "WORK_DIR=${config.workDir}"`);
399
+ const env = {};
400
+ if (config.serverUrl) env.SERVER_URL = config.serverUrl;
401
+ if (config.agentName) env.AGENT_NAME = config.agentName;
402
+ if (config.agentSecret) env.AGENT_SECRET = config.agentSecret;
403
+ if (config.workDir) env.WORK_DIR = config.workDir;
404
+
405
+ return `module.exports = {
406
+ apps: [{
407
+ name: '${PM2_APP_NAME}',
408
+ script: '${cliPath.replace(/\\/g, '\\\\')}',
409
+ interpreter: '${nodePath.replace(/\\/g, '\\\\')}',
410
+ cwd: '${cliDir.replace(/\\/g, '\\\\')}',
411
+ env: ${JSON.stringify(env, null, 6)},
412
+ autorestart: true,
413
+ watch: false,
414
+ max_restarts: 10,
415
+ restart_delay: 5000,
416
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
417
+ error_file: '${join(logDir, 'error.log').replace(/\\/g, '\\\\')}',
418
+ out_file: '${join(logDir, 'out.log').replace(/\\/g, '\\\\')}',
419
+ merge_logs: true,
420
+ max_memory_restart: '500M',
421
+ }]
422
+ };
423
+ `;
424
+ }
394
425
 
395
- // Create a batch file that sets env vars and starts node (with log redirection)
426
+ function winInstall(config) {
427
+ ensurePm2();
428
+ const logDir = getLogDir();
396
429
  mkdirSync(logDir, { recursive: true });
397
- const logFile = join(logDir, 'out.log');
398
- const cliDir = dirname(cliPath);
399
- const batContent = `@echo off\r\ncd /d "${cliDir}"\r\n${envLines.join('\r\n')}\r\n"${nodePath}" "${cliPath}" >> "${logFile}" 2>&1\r\n`;
400
- const batPath = getWinBatPath();
401
- writeFileSync(batPath, batContent);
402
430
 
403
- // Create VBS wrapper to run hidden (no console window)
404
- const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\r\nWshShell.CurrentDirectory = "${cliDir}"\r\nWshShell.Run """${batPath}""", 0, False\r\n`;
405
- const vbsPath = getWinWrapperPath();
406
- writeFileSync(vbsPath, vbsContent);
431
+ // Generate ecosystem config
432
+ const ecoPath = getEcosystemPath();
433
+ writeFileSync(ecoPath, generateEcosystem(config));
407
434
 
408
- // Remove existing task if any
409
- try { execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f 2>nul`, { stdio: 'pipe' }); } catch {}
435
+ // Stop existing instance if any
436
+ try { execSync(`pm2 delete ${PM2_APP_NAME}`, { stdio: 'pipe' }); } catch {}
410
437
 
411
- // Create scheduled task that runs at logon
412
- // Try schtasks first (highest limited), fall back to Startup folder
413
- let usedStartupFolder = false;
414
- try {
415
- execSync(
416
- `schtasks /create /tn "${WIN_TASK_NAME}" /tr "wscript.exe \\"${vbsPath}\\"" /sc onlogon /rl highest /f`,
417
- { stdio: 'pipe' }
418
- );
419
- } catch {
420
- try {
421
- execSync(
422
- `schtasks /create /tn "${WIN_TASK_NAME}" /tr "wscript.exe \\"${vbsPath}\\"" /sc onlogon /rl limited /f`,
423
- { stdio: 'pipe' }
424
- );
425
- } catch {
426
- // schtasks not available (no admin) — use Startup folder
427
- const startupDir = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
428
- const startupVbs = join(startupDir, `${WIN_TASK_NAME}.vbs`);
429
- writeFileSync(startupVbs, vbsContent);
430
- usedStartupFolder = true;
431
- console.log(' (Using Startup folder for auto-start — no admin required)');
432
- }
438
+ // Start with pm2
439
+ execSync(`pm2 start "${ecoPath}"`, { stdio: 'inherit' });
440
+
441
+ // Save pm2 process list for resurrection
442
+ execSync('pm2 save', { stdio: 'pipe' });
443
+
444
+ // Setup auto-start: create startup script in Windows Startup folder
445
+ // pm2-startup doesn't work well on Windows, use Startup folder approach
446
+ const trayScript = join(__dirname, 'scripts', 'agent-tray.ps1');
447
+ const startupDir = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
448
+ const startupBat = join(startupDir, `${PM2_APP_NAME}.bat`);
449
+ // Resurrect pm2 processes + launch tray icon
450
+ let batContent = `@echo off\r\npm2 resurrect\r\n`;
451
+ if (existsSync(trayScript)) {
452
+ batContent += `start "" powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "${trayScript}"\r\n`;
433
453
  }
454
+ writeFileSync(startupBat, batContent);
434
455
 
435
- // Also start it now
436
- if (usedStartupFolder) {
437
- execSync(`wscript.exe "${vbsPath}"`, { stdio: 'pipe' });
438
- } else {
439
- execSync(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: 'pipe' });
456
+ // Launch tray now
457
+ if (existsSync(trayScript)) {
458
+ spawn('powershell', ['-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', trayScript], {
459
+ detached: true, stdio: 'ignore'
460
+ }).unref();
440
461
  }
441
462
 
442
- console.log('Service installed and started.');
443
- console.log(` Bat: ${batPath}`);
444
- console.log(` VBS: ${vbsPath}`);
445
- console.log(` Log: ${logFile}`);
446
- console.log(` Node: ${nodePath}`);
447
- console.log(` CLI: ${cliPath}`);
463
+ console.log(`\nService installed and started.`);
464
+ console.log(` Ecosystem: ${ecoPath}`);
465
+ console.log(` Startup: ${startupBat}`);
448
466
  console.log(`\nManage with:`);
449
467
  console.log(` yeaft-agent status`);
450
468
  console.log(` yeaft-agent logs`);
@@ -453,14 +471,19 @@ function winInstall(config) {
453
471
  }
454
472
 
455
473
  function winUninstall() {
456
- try { winStop(); } catch {}
457
- try { execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f`, { stdio: 'pipe' }); } catch {}
458
- // Clean up wrapper files
474
+ try { execSync(`pm2 delete ${PM2_APP_NAME}`, { stdio: 'pipe' }); } catch {}
475
+ try { execSync('pm2 save', { stdio: 'pipe' }); } catch {}
476
+ // Clean up ecosystem config
477
+ const ecoPath = getEcosystemPath();
478
+ if (existsSync(ecoPath)) unlinkSync(ecoPath);
479
+ // Clean up Startup bat
480
+ const startupBat = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${PM2_APP_NAME}.bat`);
481
+ if (existsSync(startupBat)) unlinkSync(startupBat);
482
+ // Clean up legacy files
459
483
  const vbsPath = getWinWrapperPath();
460
484
  const batPath = getWinBatPath();
461
485
  if (existsSync(vbsPath)) unlinkSync(vbsPath);
462
486
  if (existsSync(batPath)) unlinkSync(batPath);
463
- // Clean up Startup folder shortcut
464
487
  const startupVbs = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${WIN_TASK_NAME}.vbs`);
465
488
  if (existsSync(startupVbs)) unlinkSync(startupVbs);
466
489
  console.log('Service uninstalled.');
@@ -468,14 +491,12 @@ function winUninstall() {
468
491
 
469
492
  function winStart() {
470
493
  try {
471
- execSync(`schtasks /run /tn "${WIN_TASK_NAME}"`, { stdio: 'pipe' });
472
- console.log('Service started.');
494
+ execSync(`pm2 start ${PM2_APP_NAME}`, { stdio: 'inherit' });
473
495
  } catch {
474
- // No schtasks — try direct launch via VBS
475
- const vbsPath = getWinWrapperPath();
476
- if (existsSync(vbsPath)) {
477
- execSync(`wscript.exe "${vbsPath}"`, { stdio: 'pipe' });
478
- console.log('Service started.');
496
+ // Try ecosystem file
497
+ const ecoPath = getEcosystemPath();
498
+ if (existsSync(ecoPath)) {
499
+ execSync(`pm2 start "${ecoPath}"`, { stdio: 'inherit' });
479
500
  } else {
480
501
  console.error('Service not installed. Run "yeaft-agent install" first.');
481
502
  process.exit(1);
@@ -484,73 +505,43 @@ function winStart() {
484
505
  }
485
506
 
486
507
  function winStop() {
487
- // Find and kill the node process running our cli.js
488
508
  try {
489
- const output = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', {
490
- encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
491
- });
492
- const cliPath = getCliPath();
493
- for (const line of output.split('\n')) {
494
- if (line.includes('cli.js') && (line.includes(SERVICE_NAME) || line.includes('webchat-agent'))) {
495
- const pid = line.trim().split(',').pop();
496
- if (pid && /^\d+$/.test(pid)) {
497
- execSync(`taskkill /pid ${pid} /f`, { stdio: 'pipe' });
498
- }
499
- }
500
- }
501
- } catch {}
502
- // Also try to end the task
503
- try { execSync(`schtasks /end /tn "${WIN_TASK_NAME}"`, { stdio: 'pipe' }); } catch {}
504
- console.log('Service stopped.');
509
+ execSync(`pm2 stop ${PM2_APP_NAME}`, { stdio: 'inherit' });
510
+ } catch {
511
+ console.error('Service not running or not installed.');
512
+ }
505
513
  }
506
514
 
507
515
  function winRestart() {
508
- winStop();
509
- // Brief pause to ensure cleanup
510
- execSync('ping -n 2 127.0.0.1 >nul', { stdio: 'pipe' });
511
- winStart();
516
+ try {
517
+ execSync(`pm2 restart ${PM2_APP_NAME}`, { stdio: 'inherit' });
518
+ } catch {
519
+ console.error('Service not running. Use "yeaft-agent start" to start.');
520
+ }
512
521
  }
513
522
 
514
523
  function winStatus() {
515
524
  try {
516
- const output = execSync(`schtasks /query /tn "${WIN_TASK_NAME}" /fo csv /v`, {
517
- encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
518
- });
519
- const lines = output.trim().split('\n');
520
- if (lines.length >= 2) {
521
- // Parse CSV header + data
522
- const headers = lines[0].split('","').map(h => h.replace(/"/g, ''));
523
- const values = lines[1].split('","').map(v => v.replace(/"/g, ''));
524
- const statusIdx = headers.indexOf('Status');
525
- const status = statusIdx >= 0 ? values[statusIdx] : 'Unknown';
526
- console.log(`Service status: ${status}`);
527
- console.log(`Task name: ${WIN_TASK_NAME}`);
528
- }
525
+ execSync(`pm2 describe ${PM2_APP_NAME}`, { stdio: 'inherit' });
529
526
  } catch {
530
- // Check Startup folder fallback
531
- const startupVbs = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${WIN_TASK_NAME}.vbs`);
532
- if (existsSync(startupVbs)) {
533
- console.log('Service installed via Startup folder (no admin).');
534
- console.log(`Startup script: ${startupVbs}`);
535
- } else {
536
- console.log('Service is not installed.');
537
- }
527
+ console.log('Service is not installed.');
538
528
  }
539
529
  }
540
530
 
541
531
  function winLogs() {
542
- const logFile = join(getLogDir(), 'out.log');
543
- if (existsSync(logFile)) {
544
- // Windows: use PowerShell Get-Content -Wait (like tail -f)
545
- const child = spawn('powershell', ['-Command', `Get-Content -Path "${logFile}" -Tail 100 -Wait`], {
546
- stdio: 'inherit'
547
- });
548
- child.on('error', () => {
532
+ const child = spawn('pm2', ['logs', PM2_APP_NAME, '--lines', '100'], {
533
+ stdio: 'inherit',
534
+ shell: true
535
+ });
536
+ child.on('error', () => {
537
+ // Fallback to reading log file directly
538
+ const logFile = join(getLogDir(), 'out.log');
539
+ if (existsSync(logFile)) {
549
540
  console.log(readFileSync(logFile, 'utf-8'));
550
- });
551
- } else {
552
- console.log('No logs found.');
553
- }
541
+ } else {
542
+ console.log('No logs found.');
543
+ }
544
+ });
554
545
  }
555
546
 
556
547
  // ─── Platform dispatcher ─────────────────────────────────────