@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,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conductor — 全局并发信号量
|
|
3
|
+
*
|
|
4
|
+
* 控制 Claude 实例并发上限。所有需要启动 Claude 进程的地方
|
|
5
|
+
* 都必须先 acquire,用完 release。
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* const release = await semaphore.acquire();
|
|
9
|
+
* try { ... } finally { release(); }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class Semaphore {
|
|
13
|
+
/**
|
|
14
|
+
* @param {number} max - 最大并发数
|
|
15
|
+
*/
|
|
16
|
+
constructor(max = 5) {
|
|
17
|
+
this._max = max;
|
|
18
|
+
this._current = 0;
|
|
19
|
+
this._queue = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get current() { return this._current; }
|
|
23
|
+
get max() { return this._max; }
|
|
24
|
+
get waiting() { return this._queue.length; }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 获取一个许可。如果已满则等待。
|
|
28
|
+
* @returns {Promise<() => void>} release 函数
|
|
29
|
+
*/
|
|
30
|
+
acquire() {
|
|
31
|
+
if (this._current < this._max) {
|
|
32
|
+
this._current++;
|
|
33
|
+
return Promise.resolve(this._createRelease());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Promise(resolve => {
|
|
37
|
+
this._queue.push(() => {
|
|
38
|
+
this._current++;
|
|
39
|
+
resolve(this._createRelease());
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 尝试非阻塞获取。返回 release 函数或 null。
|
|
46
|
+
* @returns {(() => void) | null}
|
|
47
|
+
*/
|
|
48
|
+
tryAcquire() {
|
|
49
|
+
if (this._current < this._max) {
|
|
50
|
+
this._current++;
|
|
51
|
+
return this._createRelease();
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_createRelease() {
|
|
57
|
+
let released = false;
|
|
58
|
+
return () => {
|
|
59
|
+
if (released) return;
|
|
60
|
+
released = true;
|
|
61
|
+
this._current--;
|
|
62
|
+
if (this._queue.length > 0) {
|
|
63
|
+
const next = this._queue.shift();
|
|
64
|
+
next();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 全局单例 */
|
|
71
|
+
export const globalSemaphore = new Semaphore(
|
|
72
|
+
parseInt(process.env.CONDUCTOR_MAX_CLAUDES || '5', 10)
|
|
73
|
+
);
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conductor — Session 核心
|
|
3
|
+
*
|
|
4
|
+
* 管理 Conductor session 的完整生命周期。
|
|
5
|
+
* 与 V1 Crew 的关键区别:
|
|
6
|
+
* - 不绑定固定工作路径,workDir 可动态切换
|
|
7
|
+
* - 不定义固定角色列表,task 创建时由 orchestrator 决定
|
|
8
|
+
* - session 级元数据存在 ~/.claude/conductor/<sessionId>/
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
import ctx from '../context.js';
|
|
12
|
+
import {
|
|
13
|
+
loadConductorIndex, upsertConductorIndex,
|
|
14
|
+
loadSessionMeta, saveSessionMeta, loadSessionMessages,
|
|
15
|
+
getMaxShardIndex, getSessionDataDir, initSessionDataDir,
|
|
16
|
+
cleanupMessageShards
|
|
17
|
+
} from './persistence.js';
|
|
18
|
+
import {
|
|
19
|
+
sendConductorMessage, sendConductorOutput, sendStatusUpdate, recordUserMessage
|
|
20
|
+
} from './ui-messages.js';
|
|
21
|
+
import { createConductorClaude, sendToConductor, stopConductorClaude } from './conductor-claude.js';
|
|
22
|
+
|
|
23
|
+
// =====================================================================
|
|
24
|
+
// Data Structures
|
|
25
|
+
// =====================================================================
|
|
26
|
+
|
|
27
|
+
/** @type {Map<string, ConductorSession>} */
|
|
28
|
+
export const conductorSessions = new Map();
|
|
29
|
+
|
|
30
|
+
// =====================================================================
|
|
31
|
+
// Session Lifecycle
|
|
32
|
+
// =====================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 创建 Conductor Session
|
|
36
|
+
*/
|
|
37
|
+
export async function createConductorSession(msg) {
|
|
38
|
+
const {
|
|
39
|
+
sessionId = randomUUID(),
|
|
40
|
+
name = '',
|
|
41
|
+
workDir = null,
|
|
42
|
+
scenarioId = null,
|
|
43
|
+
userId,
|
|
44
|
+
username
|
|
45
|
+
} = msg;
|
|
46
|
+
|
|
47
|
+
const session = {
|
|
48
|
+
id: sessionId,
|
|
49
|
+
name: name || 'Conductor',
|
|
50
|
+
workDir,
|
|
51
|
+
scenarioId,
|
|
52
|
+
status: 'running',
|
|
53
|
+
tasks: new Map(),
|
|
54
|
+
conductorState: null,
|
|
55
|
+
costUsd: 0,
|
|
56
|
+
totalInputTokens: 0,
|
|
57
|
+
totalOutputTokens: 0,
|
|
58
|
+
activeClaudes: 0,
|
|
59
|
+
uiMessages: [],
|
|
60
|
+
userId,
|
|
61
|
+
username,
|
|
62
|
+
agentId: ctx.CONFIG?.agentName || null,
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
_rotating: false,
|
|
65
|
+
_conductorSemRelease: null
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
conductorSessions.set(sessionId, session);
|
|
69
|
+
|
|
70
|
+
// 初始化数据目录
|
|
71
|
+
await initSessionDataDir(sessionId);
|
|
72
|
+
|
|
73
|
+
// 通知前端
|
|
74
|
+
sendConductorMessage({
|
|
75
|
+
type: 'conductor_session_created',
|
|
76
|
+
sessionId,
|
|
77
|
+
name: session.name,
|
|
78
|
+
workDir: session.workDir,
|
|
79
|
+
scenarioId: session.scenarioId,
|
|
80
|
+
userId,
|
|
81
|
+
username
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
sendStatusUpdate(session);
|
|
85
|
+
|
|
86
|
+
// 注册到全局 index
|
|
87
|
+
await upsertConductorIndex(session);
|
|
88
|
+
await saveSessionMeta(session);
|
|
89
|
+
|
|
90
|
+
// 启动 Conductor Claude
|
|
91
|
+
try {
|
|
92
|
+
await createConductorClaude(session);
|
|
93
|
+
console.log(`[Conductor] Session ${sessionId} created, Claude ready`);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error('[Conductor] Failed to start Claude:', e.message);
|
|
96
|
+
sendConductorOutput(session, 'system', {
|
|
97
|
+
message: { role: 'assistant', content: `Conductor 启动失败: ${e.message}` }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return session;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 列出所有 conductor sessions
|
|
106
|
+
*/
|
|
107
|
+
export async function handleListConductorSessions(msg) {
|
|
108
|
+
const { requestId, _requestClientId } = msg;
|
|
109
|
+
const index = await loadConductorIndex();
|
|
110
|
+
|
|
111
|
+
const agentId = ctx.CONFIG?.agentName || null;
|
|
112
|
+
const filtered = agentId
|
|
113
|
+
? index.filter(e => !e.agentId || e.agentId === agentId)
|
|
114
|
+
: index;
|
|
115
|
+
|
|
116
|
+
// 更新活跃 session 的状态
|
|
117
|
+
for (const entry of filtered) {
|
|
118
|
+
const active = conductorSessions.get(entry.sessionId);
|
|
119
|
+
if (active) {
|
|
120
|
+
entry.status = active.status;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.sendToServer({
|
|
125
|
+
type: 'conductor_sessions_list',
|
|
126
|
+
requestId,
|
|
127
|
+
_requestClientId,
|
|
128
|
+
sessions: filtered.filter(e => !e.hidden)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 恢复已停止的 conductor session
|
|
134
|
+
*/
|
|
135
|
+
export async function resumeConductorSession(msg) {
|
|
136
|
+
const { sessionId, userId, username } = msg;
|
|
137
|
+
|
|
138
|
+
// 已在内存中
|
|
139
|
+
if (conductorSessions.has(sessionId)) {
|
|
140
|
+
const session = conductorSessions.get(sessionId);
|
|
141
|
+
// 加载 UI 消息
|
|
142
|
+
if (!session.uiMessages || session.uiMessages.length === 0) {
|
|
143
|
+
const loaded = await loadSessionMessages(sessionId);
|
|
144
|
+
session.uiMessages = loaded.messages;
|
|
145
|
+
}
|
|
146
|
+
const cleaned = (session.uiMessages || []).map(m => {
|
|
147
|
+
const { _streaming, ...rest } = m;
|
|
148
|
+
return rest;
|
|
149
|
+
});
|
|
150
|
+
const dir = getSessionDataDir(sessionId);
|
|
151
|
+
const hasOlderMessages = await getMaxShardIndex(dir) > 0;
|
|
152
|
+
|
|
153
|
+
sendConductorMessage({
|
|
154
|
+
type: 'conductor_session_restored',
|
|
155
|
+
sessionId,
|
|
156
|
+
name: session.name,
|
|
157
|
+
workDir: session.workDir,
|
|
158
|
+
scenarioId: session.scenarioId,
|
|
159
|
+
tasks: Array.from(session.tasks.values()),
|
|
160
|
+
userId: session.userId,
|
|
161
|
+
username: session.username,
|
|
162
|
+
uiMessages: cleaned,
|
|
163
|
+
hasOlderMessages
|
|
164
|
+
});
|
|
165
|
+
sendStatusUpdate(session);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 从磁盘恢复
|
|
170
|
+
const meta = await loadSessionMeta(sessionId);
|
|
171
|
+
if (!meta) {
|
|
172
|
+
sendConductorMessage({
|
|
173
|
+
type: 'error', sessionId,
|
|
174
|
+
message: 'Conductor session not found'
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const session = {
|
|
180
|
+
id: sessionId,
|
|
181
|
+
name: meta.name || '',
|
|
182
|
+
workDir: meta.workDir || null,
|
|
183
|
+
scenarioId: meta.scenarioId || null,
|
|
184
|
+
status: 'running',
|
|
185
|
+
tasks: new Map((meta.tasks || []).map(t => [t.taskId, t])),
|
|
186
|
+
conductorState: null,
|
|
187
|
+
costUsd: meta.costUsd || 0,
|
|
188
|
+
totalInputTokens: meta.totalInputTokens || 0,
|
|
189
|
+
totalOutputTokens: meta.totalOutputTokens || 0,
|
|
190
|
+
activeClaudes: 0,
|
|
191
|
+
uiMessages: [],
|
|
192
|
+
userId: userId || meta.userId,
|
|
193
|
+
username: username || meta.username,
|
|
194
|
+
agentId: meta.agentId || ctx.CONFIG?.agentName || null,
|
|
195
|
+
createdAt: meta.createdAt || Date.now(),
|
|
196
|
+
_rotating: false,
|
|
197
|
+
_conductorSemRelease: null
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
conductorSessions.set(sessionId, session);
|
|
201
|
+
|
|
202
|
+
const loaded = await loadSessionMessages(sessionId);
|
|
203
|
+
session.uiMessages = loaded.messages;
|
|
204
|
+
|
|
205
|
+
sendConductorMessage({
|
|
206
|
+
type: 'conductor_session_restored',
|
|
207
|
+
sessionId,
|
|
208
|
+
name: session.name,
|
|
209
|
+
workDir: session.workDir,
|
|
210
|
+
scenarioId: session.scenarioId,
|
|
211
|
+
tasks: Array.from(session.tasks.values()),
|
|
212
|
+
userId: session.userId,
|
|
213
|
+
username: session.username,
|
|
214
|
+
uiMessages: session.uiMessages,
|
|
215
|
+
hasOlderMessages: loaded.hasOlderMessages
|
|
216
|
+
});
|
|
217
|
+
sendStatusUpdate(session);
|
|
218
|
+
|
|
219
|
+
// 启动 Conductor Claude
|
|
220
|
+
try {
|
|
221
|
+
await createConductorClaude(session);
|
|
222
|
+
console.log(`[Conductor] Session ${sessionId} resumed, Claude ready`);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error('[Conductor] Failed to resume Claude:', e.message);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await upsertConductorIndex(session);
|
|
228
|
+
await saveSessionMeta(session);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 处理用户输入
|
|
233
|
+
*/
|
|
234
|
+
export async function handleConductorUserInput(msg) {
|
|
235
|
+
const { sessionId, content } = msg;
|
|
236
|
+
const session = conductorSessions.get(sessionId);
|
|
237
|
+
if (!session) {
|
|
238
|
+
console.warn(`[Conductor] Session not found: ${sessionId}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 确保 session 在运行态(先检查再记录,避免停止态也记录消息)
|
|
243
|
+
if (session.status === 'stopped') {
|
|
244
|
+
sendConductorMessage({
|
|
245
|
+
type: 'conductor_error',
|
|
246
|
+
sessionId,
|
|
247
|
+
error: 'Session is stopped'
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 记录到 UI
|
|
253
|
+
recordUserMessage(session, content);
|
|
254
|
+
|
|
255
|
+
session.status = 'running';
|
|
256
|
+
|
|
257
|
+
// 发给 Conductor Claude
|
|
258
|
+
await sendToConductor(session, content);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 更新工作路径
|
|
263
|
+
*/
|
|
264
|
+
export async function handleUpdateWorkDir(msg) {
|
|
265
|
+
const { sessionId, workDir } = msg;
|
|
266
|
+
const session = conductorSessions.get(sessionId);
|
|
267
|
+
if (!session) return;
|
|
268
|
+
|
|
269
|
+
session.workDir = workDir;
|
|
270
|
+
console.log(`[Conductor] Session ${sessionId} workDir updated: ${workDir}`);
|
|
271
|
+
|
|
272
|
+
sendConductorMessage({
|
|
273
|
+
type: 'conductor_workdir_updated',
|
|
274
|
+
sessionId,
|
|
275
|
+
workDir
|
|
276
|
+
});
|
|
277
|
+
sendStatusUpdate(session);
|
|
278
|
+
await saveSessionMeta(session);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 更新 session name
|
|
283
|
+
*/
|
|
284
|
+
export async function handleUpdateConductorSession(msg) {
|
|
285
|
+
const { sessionId, name, workDir } = msg;
|
|
286
|
+
const session = conductorSessions.get(sessionId);
|
|
287
|
+
if (!session) return;
|
|
288
|
+
|
|
289
|
+
if (name !== undefined) session.name = name;
|
|
290
|
+
if (workDir !== undefined) session.workDir = workDir;
|
|
291
|
+
await saveSessionMeta(session);
|
|
292
|
+
await upsertConductorIndex(session);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 停止 session
|
|
297
|
+
*/
|
|
298
|
+
export async function stopConductorSession(sessionId) {
|
|
299
|
+
const session = conductorSessions.get(sessionId);
|
|
300
|
+
if (!session) return;
|
|
301
|
+
|
|
302
|
+
session.status = 'stopped';
|
|
303
|
+
|
|
304
|
+
// 停止 Conductor Claude
|
|
305
|
+
await stopConductorClaude(session);
|
|
306
|
+
|
|
307
|
+
sendConductorOutput(session, 'system', {
|
|
308
|
+
message: { role: 'assistant', content: 'Session 已停止' }
|
|
309
|
+
});
|
|
310
|
+
sendStatusUpdate(session);
|
|
311
|
+
await saveSessionMeta(session);
|
|
312
|
+
await upsertConductorIndex(session);
|
|
313
|
+
|
|
314
|
+
conductorSessions.delete(sessionId);
|
|
315
|
+
console.log(`[Conductor] Session ${sessionId} stopped`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 清空 session
|
|
320
|
+
*/
|
|
321
|
+
export async function clearConductorSession(sessionId) {
|
|
322
|
+
const session = conductorSessions.get(sessionId);
|
|
323
|
+
if (!session) return;
|
|
324
|
+
|
|
325
|
+
// 停止 Claude
|
|
326
|
+
await stopConductorClaude(session);
|
|
327
|
+
|
|
328
|
+
// 清空数据
|
|
329
|
+
session.tasks.clear();
|
|
330
|
+
session.uiMessages = [];
|
|
331
|
+
session.costUsd = 0;
|
|
332
|
+
session.totalInputTokens = 0;
|
|
333
|
+
session.totalOutputTokens = 0;
|
|
334
|
+
|
|
335
|
+
const dir = getSessionDataDir(sessionId);
|
|
336
|
+
await cleanupMessageShards(dir);
|
|
337
|
+
|
|
338
|
+
session.status = 'running';
|
|
339
|
+
|
|
340
|
+
sendConductorMessage({
|
|
341
|
+
type: 'conductor_session_cleared',
|
|
342
|
+
sessionId
|
|
343
|
+
});
|
|
344
|
+
sendStatusUpdate(session);
|
|
345
|
+
|
|
346
|
+
// 重启 Claude
|
|
347
|
+
try {
|
|
348
|
+
await createConductorClaude(session);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
console.error('[Conductor] Failed to restart Claude after clear:', e.message);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await saveSessionMeta(session);
|
|
354
|
+
console.log(`[Conductor] Session ${sessionId} cleared`);
|
|
355
|
+
}
|