@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.
- package/conductor/actor.js +478 -0
- package/conductor/claudemd-generator.js +194 -0
- package/conductor/conductor-claude.js +453 -0
- package/conductor/index.js +63 -0
- package/conductor/orchestrator-flows.js +498 -0
- package/conductor/orchestrator.js +888 -0
- package/conductor/persistence.js +261 -0
- package/conductor/personas/dev.js +97 -0
- package/conductor/personas/index.js +77 -0
- package/conductor/personas/trading.js +92 -0
- package/conductor/personas/video.js +75 -0
- package/conductor/personas/writing.js +82 -0
- package/conductor/semaphore.js +73 -0
- package/conductor/session.js +355 -0
- package/conductor/specialties/common.js +350 -0
- package/conductor/specialties/index.js +135 -0
- package/conductor/specialties/trading.js +165 -0
- package/conductor/specialties/video.js +175 -0
- package/conductor/specialties/writing.js +120 -0
- package/conductor/task.js +333 -0
- package/conductor/ui-messages.js +169 -0
- package/conductor/worktree.js +175 -0
- package/package.json +2 -8
- package/scripts/agent-tray.ps1 +0 -134
- package/scripts/create-startup-shortcut.ps1 +0 -18
- package/scripts/ecosystem.config.cjs +0 -46
- package/scripts/pm2-start.ps1 +0 -59
- package/scripts/start-with-tray.ps1 +0 -36
|
@@ -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.
|
|
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
|
-
"
|
|
36
|
-
"connection/",
|
|
37
|
-
"crew/",
|
|
38
|
-
"sdk/",
|
|
39
|
-
"scripts/",
|
|
40
|
-
"service/",
|
|
41
|
-
"workbench/"
|
|
35
|
+
"**/*.js"
|
|
42
36
|
],
|
|
43
37
|
"license": "MIT",
|
|
44
38
|
"author": "Yeaft",
|
package/scripts/agent-tray.ps1
DELETED
|
@@ -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
|
-
};
|
package/scripts/pm2-start.ps1
DELETED
|
@@ -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
|