@yeaft/webchat-agent 0.1.223 → 0.1.225

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,169 @@
1
+ /**
2
+ * Conductor — UI 消息辅助函数
3
+ *
4
+ * 与 crew/ui-messages.js 结构一致,但用 conductor_ 前缀的消息类型。
5
+ * 核心职责:向 server 发送 WebSocket 消息,维护 uiMessages 持久化列表。
6
+ */
7
+ import ctx from '../context.js';
8
+ import { upsertConductorIndex, saveSessionMeta } from './persistence.js';
9
+
10
+ /**
11
+ * 发送 conductor 消息到 server(透传到 Web 前端)
12
+ */
13
+ export function sendConductorMessage(msg) {
14
+ if (ctx.sendToServer) {
15
+ ctx.sendToServer(msg);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * 发送 Conductor Claude 的输出到前端
21
+ * outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_created' | 'task_forwarded'
22
+ */
23
+ export function sendConductorOutput(session, outputType, rawMessage, extra = {}) {
24
+ sendConductorMessage({
25
+ type: 'conductor_output',
26
+ sessionId: session.id,
27
+ outputType,
28
+ data: rawMessage,
29
+ ...extra
30
+ });
31
+
32
+ // 记录精简 UI 消息
33
+ if (outputType === 'text') {
34
+ const content = rawMessage?.message?.content;
35
+ let text = '';
36
+ if (typeof content === 'string') {
37
+ text = content;
38
+ } else if (Array.isArray(content)) {
39
+ text = content.filter(b => b.type === 'text').map(b => b.text).join('');
40
+ }
41
+ if (!text) return;
42
+ // 反向查找 streaming 消息
43
+ let found = false;
44
+ for (let i = session.uiMessages.length - 1; i >= 0; i--) {
45
+ const msg = session.uiMessages[i];
46
+ if (msg.source === 'conductor' && msg.type === 'text' && msg._streaming) {
47
+ msg.content += text;
48
+ found = true;
49
+ break;
50
+ }
51
+ }
52
+ if (!found) {
53
+ session.uiMessages.push({
54
+ source: 'conductor', type: 'text', content: text,
55
+ _streaming: true, timestamp: Date.now()
56
+ });
57
+ }
58
+ } else if (outputType === 'system') {
59
+ const content = rawMessage?.message?.content;
60
+ let text = '';
61
+ if (typeof content === 'string') text = content;
62
+ else if (Array.isArray(content)) text = content.filter(b => b.type === 'text').map(b => b.text).join('');
63
+ if (!text) return;
64
+ session.uiMessages.push({
65
+ source: 'conductor', type: 'system', content: text, timestamp: Date.now()
66
+ });
67
+ } else if (outputType === 'task_created') {
68
+ session.uiMessages.push({
69
+ source: 'conductor', type: 'task_created',
70
+ taskId: extra.taskId, taskTitle: extra.taskTitle,
71
+ content: `Created task: ${extra.taskTitle}`,
72
+ timestamp: Date.now()
73
+ });
74
+ } else if (outputType === 'task_forwarded') {
75
+ session.uiMessages.push({
76
+ source: 'conductor', type: 'task_forwarded',
77
+ taskId: extra.taskId,
78
+ content: `Forwarded message to task: ${extra.taskId}`,
79
+ timestamp: Date.now()
80
+ });
81
+ } else if (outputType === 'tool_use') {
82
+ endConductorStreaming(session);
83
+ const content = rawMessage?.message?.content;
84
+ if (Array.isArray(content)) {
85
+ for (const block of content) {
86
+ if (block.type === 'tool_use') {
87
+ const input = block.input || {};
88
+ const trimmedInput = {};
89
+ if (input.file_path) trimmedInput.file_path = input.file_path;
90
+ if (input.command) trimmedInput.command = input.command.substring(0, 200);
91
+ if (input.pattern) trimmedInput.pattern = input.pattern;
92
+ session.uiMessages.push({
93
+ source: 'conductor', type: 'tool',
94
+ toolName: block.name, toolId: block.id,
95
+ toolInput: Object.keys(trimmedInput).length > 0 ? trimmedInput : null,
96
+ content: `${block.name} ${input.file_path || input.command?.substring(0, 60) || ''}`,
97
+ hasResult: false,
98
+ timestamp: Date.now()
99
+ });
100
+ }
101
+ }
102
+ }
103
+ } else if (outputType === 'tool_result') {
104
+ const toolId = rawMessage?.message?.tool_use_id;
105
+ if (toolId) {
106
+ for (let i = session.uiMessages.length - 1; i >= 0; i--) {
107
+ if (session.uiMessages[i].type === 'tool' && session.uiMessages[i].toolId === toolId) {
108
+ session.uiMessages[i].hasResult = true;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 结束 Conductor 的 streaming 消息
118
+ */
119
+ export function endConductorStreaming(session) {
120
+ for (let i = session.uiMessages.length - 1; i >= 0; i--) {
121
+ if (session.uiMessages[i].source === 'conductor' && session.uiMessages[i]._streaming) {
122
+ delete session.uiMessages[i]._streaming;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 记录用户消息到 uiMessages
130
+ */
131
+ export function recordUserMessage(session, content) {
132
+ session.uiMessages.push({
133
+ source: 'user', type: 'text', content,
134
+ timestamp: Date.now()
135
+ });
136
+ }
137
+
138
+ /**
139
+ * 发送 session 状态更新到前端
140
+ */
141
+ export function sendStatusUpdate(session) {
142
+ const tasks = Array.from(session.tasks.values()).map(t => ({
143
+ taskId: t.taskId,
144
+ title: t.title,
145
+ workDir: t.workDir,
146
+ status: t.status,
147
+ phase: t.phase,
148
+ progress: t.progress,
149
+ activeActors: t.activeActors || [],
150
+ createdAt: t.createdAt,
151
+ updatedAt: t.updatedAt
152
+ }));
153
+
154
+ sendConductorMessage({
155
+ type: 'conductor_status',
156
+ sessionId: session.id,
157
+ status: session.status,
158
+ workDir: session.workDir,
159
+ tasks,
160
+ costUsd: session.costUsd,
161
+ totalInputTokens: session.totalInputTokens,
162
+ totalOutputTokens: session.totalOutputTokens,
163
+ activeClaudes: session.activeClaudes || 0
164
+ });
165
+
166
+ // 异步持久化
167
+ upsertConductorIndex(session).catch(e => console.warn('[Conductor] Failed to update index:', e.message));
168
+ saveSessionMeta(session).catch(e => console.warn('[Conductor] Failed to save meta:', e.message));
169
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Conductor — Worktree 管理
3
+ *
4
+ * Conductor 的 worktree 策略与 Crew V1 不同:
5
+ * - Crew V1: 每个 dev group 一个 worktree
6
+ * - Conductor: 每个并行执行线程(thread) 一个 worktree
7
+ *
8
+ * worktree 位于 .conductor/tasks/task-N/worktrees/thread-N/
9
+ * 复用 crew/worktree.js 的 git 操作模式
10
+ */
11
+ import { promises as fs } from 'fs';
12
+ import { join, resolve } from 'path';
13
+ import { execFile as execFileCb } from 'child_process';
14
+ import { promisify } from 'util';
15
+
16
+ const execFile = promisify(execFileCb);
17
+
18
+ /**
19
+ * 为 task 的执行线程创建 worktree
20
+ *
21
+ * @param {string} projectDir - 主项目目录(git 仓库根目录)
22
+ * @param {string} taskDir - task 目录路径 (.conductor/tasks/task-N)
23
+ * @param {string} threadId - 线程 ID (e.g. 't1', 't2')
24
+ * @param {object} [options]
25
+ * @param {string} [options.baseBranch='HEAD'] - 基于哪个 ref 创建
26
+ * @returns {Promise<string>} worktree 路径
27
+ */
28
+ export async function createThreadWorktree(projectDir, taskDir, threadId, options = {}) {
29
+ const { baseBranch = 'HEAD' } = options;
30
+
31
+ const worktreeBase = join(taskDir, 'worktrees');
32
+ await fs.mkdir(worktreeBase, { recursive: true });
33
+
34
+ const wtDir = join(worktreeBase, threadId);
35
+ const branch = `conductor/${extractTaskId(taskDir)}-${threadId}`;
36
+
37
+ // 检查是否已存在
38
+ const exists = await directoryExists(wtDir);
39
+ if (exists) {
40
+ // 检查 git 是否认识它
41
+ const known = await isKnownWorktree(projectDir, wtDir);
42
+ if (known) {
43
+ console.log(`[Conductor/Worktree] Already exists: ${wtDir}`);
44
+ return wtDir;
45
+ }
46
+ // 孤立目录,清理后重建
47
+ console.warn(`[Conductor/Worktree] Orphaned dir, removing: ${wtDir}`);
48
+ await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
49
+ }
50
+
51
+ try {
52
+ // 创建分支
53
+ try {
54
+ await execFile('git', ['branch', branch, baseBranch], { cwd: projectDir });
55
+ } catch {
56
+ // 分支已存在,忽略
57
+ }
58
+
59
+ // 创建 worktree
60
+ await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: projectDir });
61
+ console.log(`[Conductor/Worktree] Created: ${wtDir} on branch ${branch}`);
62
+ return wtDir;
63
+ } catch (e) {
64
+ console.error(`[Conductor/Worktree] Failed to create ${threadId}:`, e.message);
65
+ throw e;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 清理 task 的全部 worktrees
71
+ *
72
+ * @param {string} projectDir - 主项目目录
73
+ * @param {string} taskDir - task 目录路径
74
+ */
75
+ export async function cleanupTaskWorktrees(projectDir, taskDir) {
76
+ const worktreeBase = join(taskDir, 'worktrees');
77
+
78
+ if (!(await directoryExists(worktreeBase))) {
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const entries = await fs.readdir(worktreeBase);
84
+ for (const entry of entries) {
85
+ const wtDir = join(worktreeBase, entry);
86
+ const branch = `conductor/${extractTaskId(taskDir)}-${entry}`;
87
+
88
+ try {
89
+ await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: projectDir });
90
+ console.log(`[Conductor/Worktree] Removed: ${wtDir}`);
91
+ } catch (e) {
92
+ console.warn(`[Conductor/Worktree] Failed to remove ${wtDir}:`, e.message);
93
+ }
94
+
95
+ try {
96
+ await execFile('git', ['branch', '-D', branch], { cwd: projectDir });
97
+ console.log(`[Conductor/Worktree] Deleted branch: ${branch}`);
98
+ } catch (e) {
99
+ console.warn(`[Conductor/Worktree] Failed to delete branch ${branch}:`, e.message);
100
+ }
101
+ }
102
+
103
+ // 尝试删除 worktrees 目录
104
+ try {
105
+ await fs.rmdir(worktreeBase);
106
+ } catch { /* 目录不空,忽略 */ }
107
+ } catch (e) {
108
+ console.error(`[Conductor/Worktree] Failed to cleanup ${taskDir}:`, e.message);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 清理整个 conductor 下的全部 worktrees
114
+ *
115
+ * @param {string} projectDir - 主项目目录
116
+ * @param {string} conductorDir - .conductor 目录路径
117
+ */
118
+ export async function cleanupAllConductorWorktrees(projectDir, conductorDir) {
119
+ const tasksDir = join(conductorDir, 'tasks');
120
+
121
+ if (!(await directoryExists(tasksDir))) return;
122
+
123
+ try {
124
+ const taskEntries = await fs.readdir(tasksDir);
125
+ for (const taskEntry of taskEntries) {
126
+ const taskDir = join(tasksDir, taskEntry);
127
+ const stat = await fs.stat(taskDir).catch(() => null);
128
+ if (stat?.isDirectory()) {
129
+ await cleanupTaskWorktrees(projectDir, taskDir);
130
+ }
131
+ }
132
+ } catch (e) {
133
+ console.error(`[Conductor/Worktree] Failed to cleanup all:`, e.message);
134
+ }
135
+ }
136
+
137
+ // =====================================================================
138
+ // Helpers
139
+ // =====================================================================
140
+
141
+ /**
142
+ * 检查目录是否存在
143
+ */
144
+ async function directoryExists(dir) {
145
+ try {
146
+ await fs.access(dir);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 检查目录是否是 git 认识的 worktree
155
+ */
156
+ async function isKnownWorktree(projectDir, wtDir) {
157
+ const normalizedWtDir = resolve(wtDir);
158
+ try {
159
+ const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], { cwd: projectDir });
160
+ for (const line of stdout.split('\n')) {
161
+ if (line.startsWith('worktree ')) {
162
+ const knownPath = resolve(line.slice('worktree '.length).trim());
163
+ if (knownPath === normalizedWtDir) return true;
164
+ }
165
+ }
166
+ } catch { /* ignore */ }
167
+ return false;
168
+ }
169
+
170
+ /**
171
+ * 从 taskDir 路径中提取 taskId(取最后一个路径段)
172
+ */
173
+ function extractTaskId(taskDir) {
174
+ return taskDir.split(/[\\/]/).filter(Boolean).pop() || 'unknown';
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.223",
3
+ "version": "0.1.225",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,13 +32,7 @@
32
32
  "url": "https://github.com/yeaft/claude-web-chat/issues"
33
33
  },
34
34
  "files": [
35
- "*.js",
36
- "connection/",
37
- "crew/",
38
- "sdk/",
39
- "scripts/",
40
- "service/",
41
- "workbench/"
35
+ "**/*.js"
42
36
  ],
43
37
  "license": "MIT",
44
38
  "author": "Yeaft",
@@ -1,134 +0,0 @@
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()
@@ -1,18 +0,0 @@
1
- # 创建开机自启动快捷方式
2
- $AgentDir = Split-Path -Parent $MyInvocation.MyCommand.Path
3
- $StartupFolder = [Environment]::GetFolderPath('Startup')
4
- $ShortcutPath = Join-Path $StartupFolder "Claude Agent.lnk"
5
-
6
- $WshShell = New-Object -ComObject WScript.Shell
7
- $Shortcut = $WshShell.CreateShortcut($ShortcutPath)
8
- $Shortcut.TargetPath = "powershell.exe"
9
- $Shortcut.Arguments = "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$AgentDir\start-with-tray.ps1`""
10
- $Shortcut.WorkingDirectory = $AgentDir
11
- $Shortcut.Description = "Claude Agent with Tray"
12
- $Shortcut.WindowStyle = 7 # Minimized
13
- $Shortcut.Save()
14
-
15
- Write-Host "Startup shortcut created at:" -ForegroundColor Green
16
- Write-Host " $ShortcutPath" -ForegroundColor Cyan
17
- Write-Host ""
18
- Write-Host "The agent will now start automatically when you log in." -ForegroundColor Green
@@ -1,46 +0,0 @@
1
- // 加载 .env 文件
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- function loadEnv() {
6
- const agentDir = path.join(__dirname, '..');
7
- const envPath = path.join(agentDir, '.env');
8
- const env = { NODE_ENV: 'production' };
9
-
10
- if (fs.existsSync(envPath)) {
11
- const content = fs.readFileSync(envPath, 'utf-8');
12
- for (const line of content.split('\n')) {
13
- const match = line.match(/^\s*([^#][^=]*)\s*=\s*(.*)$/);
14
- if (match) {
15
- const key = match[1].trim();
16
- let value = match[2].trim();
17
- // 移除引号
18
- value = value.replace(/^["']|["']$/g, '');
19
- env[key] = value;
20
- }
21
- }
22
- }
23
- return env;
24
- }
25
-
26
- module.exports = {
27
- apps: [{
28
- name: 'claude-agent',
29
- script: 'cli.js',
30
- cwd: path.join(__dirname, '..'),
31
- // 从 .env 文件加载环境变量
32
- env: loadEnv(),
33
- // 自动重启配置
34
- autorestart: true,
35
- watch: false,
36
- max_restarts: 10,
37
- restart_delay: 5000,
38
- // 日志配置
39
- log_date_format: 'YYYY-MM-DD HH:mm:ss',
40
- error_file: 'logs/error.log',
41
- out_file: 'logs/out.log',
42
- merge_logs: true,
43
- // 内存超限自动重启
44
- max_memory_restart: '500M',
45
- }]
46
- };
@@ -1,59 +0,0 @@
1
- # Claude Agent PM2 托管启动脚本
2
- # 用法: .\pm2-start.ps1
3
-
4
- $ErrorActionPreference = "Stop"
5
- $AgentDir = Split-Path -Parent $MyInvocation.MyCommand.Path
6
-
7
- # 切换到 agent 目录
8
- Set-Location $AgentDir
9
-
10
- # 检查 pm2 是否安装
11
- $pm2Path = Get-Command pm2 -ErrorAction SilentlyContinue
12
- if (-not $pm2Path) {
13
- Write-Host "Installing pm2 globally..." -ForegroundColor Yellow
14
- npm install -g pm2
15
- # Windows 上需要安装 pm2-windows-startup
16
- npm install -g pm2-windows-startup
17
- }
18
-
19
- # 安装依赖 (每次都检查确保完整)
20
- Write-Host "Checking dependencies..." -ForegroundColor Yellow
21
- npm install --silent
22
- if ($LASTEXITCODE -ne 0) {
23
- Write-Host "Failed to install dependencies!" -ForegroundColor Red
24
- exit 1
25
- }
26
-
27
- # 检查 .env 文件
28
- if (-not (Test-Path ".env")) {
29
- Write-Host "No .env file found. Creating from .env.example..." -ForegroundColor Yellow
30
- if (Test-Path ".env.example") {
31
- Copy-Item ".env.example" ".env"
32
- Write-Host "Please edit .env with your configuration, then run this script again." -ForegroundColor Red
33
- exit 1
34
- } else {
35
- Write-Host "No .env.example found!" -ForegroundColor Red
36
- exit 1
37
- }
38
- }
39
-
40
- # 启动 pm2
41
- Write-Host ""
42
- Write-Host "Starting Claude Agent with pm2..." -ForegroundColor Green
43
- pm2 start ecosystem.config.cjs
44
-
45
- Write-Host ""
46
- Write-Host "Agent started!" -ForegroundColor Green
47
- Write-Host ""
48
- Write-Host "Management commands:" -ForegroundColor Cyan
49
- Write-Host " pm2 status - Check status"
50
- Write-Host " pm2 logs claude-agent - View logs (follow mode)"
51
- Write-Host " pm2 logs claude-agent --lines 100 - View last 100 lines"
52
- Write-Host " pm2 stop claude-agent - Stop agent"
53
- Write-Host " pm2 restart claude-agent - Restart agent"
54
- Write-Host " pm2 delete claude-agent - Remove from pm2"
55
- Write-Host ""
56
- Write-Host "To enable startup on Windows boot:" -ForegroundColor Yellow
57
- Write-Host " 1. pm2 save"
58
- Write-Host " 2. pm2-startup install"
59
- Write-Host ""
@@ -1,36 +0,0 @@
1
- # 启动 Claude Agent (pm2 托管) + 托盘图标
2
- $AgentDir = Split-Path -Parent $MyInvocation.MyCommand.Path
3
- Set-Location $AgentDir
4
-
5
- # 检查 pm2
6
- $pm2 = Get-Command pm2 -ErrorAction SilentlyContinue
7
- if (-not $pm2) {
8
- Write-Host "Installing pm2..." -ForegroundColor Yellow
9
- npm install -g pm2
10
- }
11
-
12
- # 检查并安装 pm2-logrotate
13
- $logrotateInstalled = pm2 list 2>$null | Select-String "pm2-logrotate"
14
- if (-not $logrotateInstalled) {
15
- Write-Host "Installing pm2-logrotate..." -ForegroundColor Yellow
16
- pm2 install pm2-logrotate
17
- # 配置 logrotate
18
- pm2 set pm2-logrotate:max_size 10M
19
- pm2 set pm2-logrotate:retain 7
20
- pm2 set pm2-logrotate:compress true
21
- Write-Host "pm2-logrotate configured (max 10MB, keep 7 files)" -ForegroundColor Cyan
22
- }
23
-
24
- # 先停止已有进程,确保干净启动
25
- Write-Host "Checking existing processes..." -ForegroundColor Gray
26
- pm2 delete claude-agent 2>$null | Out-Null
27
-
28
- # 启动 agent
29
- Write-Host "Starting Claude Agent..." -ForegroundColor Green
30
- pm2 start ecosystem.config.cjs
31
-
32
- # 启动托盘(隐藏窗口)
33
- Write-Host "Starting tray icon..." -ForegroundColor Green
34
- Start-Process powershell -ArgumentList "-WindowStyle Hidden -File `"$AgentDir\agent-tray.ps1`"" -WindowStyle Hidden
35
-
36
- Write-Host "Done! Check the system tray." -ForegroundColor Green