@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,453 @@
1
+ /**
2
+ * Conductor — Conductor Claude 实例管理
3
+ *
4
+ * Conductor Claude 是常驻的 Claude Code 进程,负责:
5
+ * 1. 消息分类(简单问答 / 创建 task / 透传到 task / 查状态)
6
+ * 2. 回答简单问题
7
+ * 3. 绝不做任务分析或步骤拆解
8
+ *
9
+ * 复用 V1 的 role-query 模式:query() + Stream + for-await 循环。
10
+ */
11
+ import { query, Stream } from '../sdk/index.js';
12
+ import { promises as fs } from 'fs';
13
+ import { join } from 'path';
14
+ import { globalSemaphore } from './semaphore.js';
15
+ import {
16
+ sendConductorMessage, sendConductorOutput,
17
+ endConductorStreaming, sendStatusUpdate
18
+ } from './ui-messages.js';
19
+ import { getSessionDataDir } from './persistence.js';
20
+
21
+ // =====================================================================
22
+ // Session ID Persistence (Conductor Claude session)
23
+ // =====================================================================
24
+
25
+ async function saveConductorSessionId(sessionDataDir, claudeSessionId) {
26
+ await fs.mkdir(sessionDataDir, { recursive: true });
27
+ const filePath = join(sessionDataDir, 'conductor-claude-session.json');
28
+ await fs.writeFile(filePath, JSON.stringify({
29
+ claudeSessionId, savedAt: Date.now()
30
+ }));
31
+ }
32
+
33
+ async function loadConductorSessionId(sessionDataDir) {
34
+ const filePath = join(sessionDataDir, 'conductor-claude-session.json');
35
+ try {
36
+ const data = JSON.parse(await fs.readFile(filePath, 'utf-8'));
37
+ return data.claudeSessionId || null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ async function clearConductorSessionId(sessionDataDir) {
44
+ const filePath = join(sessionDataDir, 'conductor-claude-session.json');
45
+ try { await fs.unlink(filePath); } catch { /* ok */ }
46
+ }
47
+
48
+ // =====================================================================
49
+ // System Prompt
50
+ // =====================================================================
51
+
52
+ function buildConductorSystemPrompt(session) {
53
+ const taskSummaries = Array.from(session.tasks.values())
54
+ .map(t => `- ${t.taskId}: ${t.title} [${t.phase} ${t.progress}%] @ ${t.workDir}`)
55
+ .join('\n');
56
+
57
+ return `# Conductor — 交响乐指挥台
58
+
59
+ 你是 Conductor,项目的总指挥。你的职责是:
60
+ 1. **分类消息**:判断用户的输入是简单问答还是需要创建任务
61
+ 2. **创建任务**:通过 CREATE_TASK 指令创建新任务
62
+ 3. **转发消息**:通过 FORWARD_TASK 将用户消息传递给已有任务
63
+ 4. **回答问题**:直接回答简单的问题
64
+ 5. **汇报状态**:用户询问进度时汇总所有任务状态
65
+
66
+ ## 你绝对不做的事
67
+ - 不分析任务难度
68
+ - 不拆解任务步骤
69
+ - 不评估需要几个人
70
+ - 不写代码
71
+
72
+ ## 当前工作路径
73
+ ${session.workDir || '(未设置)'}
74
+
75
+ ## 当前活跃任务
76
+ ${taskSummaries || '(无)'}
77
+
78
+ ## 指令格式
79
+
80
+ ### 创建任务
81
+ 当用户的需求需要实际的开发/分析/设计工作时,输出:
82
+
83
+ \`\`\`
84
+ ---CREATE_TASK---
85
+ title: <任务标题>
86
+ ---END_CREATE_TASK---
87
+ \`\`\`
88
+
89
+ ### 转发消息到已有任务
90
+ 当用户的消息是针对某个已有任务的补充说明时,输出:
91
+
92
+ \`\`\`
93
+ ---FORWARD_TASK---
94
+ taskId: <task-id>
95
+ message: <要转发的内容>
96
+ ---END_FORWARD_TASK---
97
+ \`\`\`
98
+
99
+ ### 回答简单问题
100
+ 直接用文本回答,不需要任何指令块。
101
+
102
+ ## 规则
103
+ - 每条用户消息只需要一种响应:创建任务 / 转发 / 直接回答
104
+ - 创建任务时只给标题,不要描述实现步骤
105
+ - 如果用户问"进度怎样",直接汇总上面的任务列表状态
106
+ - 保持回复简洁
107
+ - 用中文回复`;
108
+ }
109
+
110
+ // =====================================================================
111
+ // Conductor Claude Instance
112
+ // =====================================================================
113
+
114
+ /**
115
+ * 创建或重建 Conductor Claude 实例
116
+ */
117
+ export async function createConductorClaude(session) {
118
+ const sessionDataDir = getSessionDataDir(session.id);
119
+
120
+ // 获取信号量
121
+ const release = await globalSemaphore.acquire();
122
+ session._conductorSemRelease = release;
123
+ session.activeClaudes = (session.activeClaudes || 0) + 1;
124
+
125
+ const inputStream = new Stream();
126
+ const abortController = new AbortController();
127
+ const systemPrompt = buildConductorSystemPrompt(session);
128
+
129
+ // 尝试恢复已有 session
130
+ const savedSessionId = await loadConductorSessionId(sessionDataDir);
131
+
132
+ const queryOptions = {
133
+ cwd: sessionDataDir,
134
+ permissionMode: 'bypassPermissions',
135
+ abort: abortController.signal,
136
+ appendSystemPrompt: systemPrompt
137
+ };
138
+
139
+ if (savedSessionId) {
140
+ queryOptions.resume = savedSessionId;
141
+ console.log(`[Conductor] Resuming Claude with sessionId: ${savedSessionId}`);
142
+ }
143
+
144
+ const conductorQuery = query({
145
+ prompt: inputStream,
146
+ options: queryOptions
147
+ });
148
+
149
+ session.conductorState = {
150
+ query: conductorQuery,
151
+ inputStream,
152
+ abortController,
153
+ claudeSessionId: savedSessionId,
154
+ accumulatedText: '',
155
+ turnActive: false,
156
+ lastCostUsd: 0,
157
+ lastInputTokens: 0,
158
+ lastOutputTokens: 0
159
+ };
160
+
161
+ // 启动输出处理循环
162
+ processConductorOutput(session, conductorQuery);
163
+
164
+ return session.conductorState;
165
+ }
166
+
167
+ /**
168
+ * 向 Conductor Claude 发送消息
169
+ */
170
+ export async function sendToConductor(session, content) {
171
+ let state = session.conductorState;
172
+
173
+ if (!state || !state.query || !state.inputStream) {
174
+ state = await createConductorClaude(session);
175
+ }
176
+
177
+ // 更新 system prompt(动态注入最新 task 列表)
178
+ // 注意:Claude Code 不支持动态更新 system prompt
179
+ // 所以我们在用户消息前注入上下文
180
+ const taskContext = buildTaskContext(session);
181
+ const fullContent = taskContext
182
+ ? `${content}\n\n---\n<conductor-context>\n${taskContext}\n</conductor-context>`
183
+ : content;
184
+
185
+ state.turnActive = true;
186
+ state.accumulatedText = '';
187
+ state.inputStream.enqueue({
188
+ type: 'user',
189
+ message: { role: 'user', content: fullContent }
190
+ });
191
+
192
+ sendStatusUpdate(session);
193
+ }
194
+
195
+ function buildTaskContext(session) {
196
+ if (session.tasks.size === 0) return null;
197
+ const lines = ['当前工作路径: ' + (session.workDir || '(未设置)'), '', '活跃任务:'];
198
+ for (const t of session.tasks.values()) {
199
+ lines.push(`- ${t.taskId}: ${t.title} [${t.phase} ${t.progress}%] @ ${t.workDir}`);
200
+ }
201
+ return lines.join('\n');
202
+ }
203
+
204
+ // =====================================================================
205
+ // Output Processing
206
+ // =====================================================================
207
+
208
+ // 解析 CREATE_TASK 指令
209
+ function parseCreateTask(text) {
210
+ const regex = /---CREATE_TASK---\s*\n([\s\S]*?)---END_CREATE_TASK---/g;
211
+ const tasks = [];
212
+ let match;
213
+ while ((match = regex.exec(text)) !== null) {
214
+ const block = match[1];
215
+ const titleMatch = block.match(/title:\s*(.+)/i);
216
+ if (titleMatch) {
217
+ tasks.push({ title: titleMatch[1].trim() });
218
+ }
219
+ }
220
+ return tasks;
221
+ }
222
+
223
+ // 解析 FORWARD_TASK 指令
224
+ function parseForwardTask(text) {
225
+ const regex = /---FORWARD_TASK---\s*\n([\s\S]*?)---END_FORWARD_TASK---/g;
226
+ const forwards = [];
227
+ let match;
228
+ while ((match = regex.exec(text)) !== null) {
229
+ const block = match[1];
230
+ const taskIdMatch = block.match(/^taskId:\s*(.+)/im);
231
+ const messageMatch = block.match(/^message:\s*([\s\S]*?)$/im);
232
+ if (taskIdMatch) {
233
+ forwards.push({
234
+ taskId: taskIdMatch[1].trim(),
235
+ message: messageMatch ? messageMatch[1].trim() : ''
236
+ });
237
+ }
238
+ }
239
+ return forwards;
240
+ }
241
+
242
+ /**
243
+ * 处理 Conductor Claude 的流式输出
244
+ */
245
+ async function processConductorOutput(session, conductorQuery) {
246
+ const state = session.conductorState;
247
+ const sessionDataDir = getSessionDataDir(session.id);
248
+
249
+ try {
250
+ for await (const message of conductorQuery) {
251
+ if (session.status === 'stopped') break;
252
+
253
+ if (message.type === 'system' && message.subtype === 'init') {
254
+ state.claudeSessionId = message.session_id;
255
+ console.log(`[Conductor] Claude session: ${message.session_id}`);
256
+ continue;
257
+ }
258
+
259
+ if (message.type === 'assistant') {
260
+ const content = message.message?.content;
261
+ if (content) {
262
+ if (typeof content === 'string') {
263
+ state.accumulatedText += content;
264
+ sendConductorOutput(session, 'text', message);
265
+ } else if (Array.isArray(content)) {
266
+ let hasText = false;
267
+ for (const block of content) {
268
+ if (block.type === 'text') {
269
+ state.accumulatedText += block.text;
270
+ hasText = true;
271
+ } else if (block.type === 'tool_use') {
272
+ endConductorStreaming(session);
273
+ sendConductorOutput(session, 'tool_use', message);
274
+ }
275
+ }
276
+ if (hasText) {
277
+ sendConductorOutput(session, 'text', message);
278
+ }
279
+ }
280
+ }
281
+ } else if (message.type === 'user') {
282
+ sendConductorOutput(session, 'tool_result', message);
283
+ } else if (message.type === 'result') {
284
+ // Turn 完成
285
+ console.log(`[Conductor] Turn completed`);
286
+ endConductorStreaming(session);
287
+
288
+ // 更新费用
289
+ if (message.total_cost_usd != null) {
290
+ const costDelta = message.total_cost_usd - state.lastCostUsd;
291
+ if (costDelta > 0) session.costUsd += costDelta;
292
+ state.lastCostUsd = message.total_cost_usd;
293
+ }
294
+ if (message.usage) {
295
+ const inputDelta = (message.usage.input_tokens || 0) - (state.lastInputTokens || 0);
296
+ const outputDelta = (message.usage.output_tokens || 0) - (state.lastOutputTokens || 0);
297
+ if (inputDelta > 0) session.totalInputTokens += inputDelta;
298
+ if (outputDelta > 0) session.totalOutputTokens += outputDelta;
299
+ state.lastInputTokens = message.usage.input_tokens || 0;
300
+ state.lastOutputTokens = message.usage.output_tokens || 0;
301
+ }
302
+
303
+ // 持久化 sessionId
304
+ if (state.claudeSessionId) {
305
+ saveConductorSessionId(sessionDataDir, state.claudeSessionId)
306
+ .catch(e => console.warn('[Conductor] Failed to save sessionId:', e.message));
307
+ }
308
+
309
+ // 解析指令
310
+ const createTasks = parseCreateTask(state.accumulatedText);
311
+ const forwardTasks = parseForwardTask(state.accumulatedText);
312
+
313
+ // 处理 CREATE_TASK
314
+ for (const ct of createTasks) {
315
+ const taskId = `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
316
+ const task = {
317
+ taskId,
318
+ title: ct.title,
319
+ workDir: session.workDir || '',
320
+ status: 'pending',
321
+ phase: 'created',
322
+ progress: 0,
323
+ activeActors: [],
324
+ createdAt: Date.now(),
325
+ updatedAt: Date.now()
326
+ };
327
+ session.tasks.set(taskId, task);
328
+
329
+ sendConductorOutput(session, 'task_created', null, {
330
+ taskId, taskTitle: ct.title
331
+ });
332
+
333
+ sendConductorMessage({
334
+ type: 'conductor_task_created',
335
+ sessionId: session.id,
336
+ task
337
+ });
338
+
339
+ console.log(`[Conductor] Task created: ${taskId} — ${ct.title}`);
340
+ }
341
+
342
+ // 处理 FORWARD_TASK
343
+ for (const ft of forwardTasks) {
344
+ const task = session.tasks.get(ft.taskId);
345
+ if (task) {
346
+ // 将消息写入 task 的 inbox(后续 orchestrator 读取)
347
+ if (!task.inbox) task.inbox = [];
348
+ task.inbox.push({
349
+ from: 'conductor',
350
+ content: ft.message,
351
+ timestamp: Date.now()
352
+ });
353
+ task.updatedAt = Date.now();
354
+
355
+ sendConductorOutput(session, 'task_forwarded', null, {
356
+ taskId: ft.taskId
357
+ });
358
+
359
+ sendConductorMessage({
360
+ type: 'conductor_task_message',
361
+ sessionId: session.id,
362
+ taskId: ft.taskId,
363
+ message: ft.message
364
+ });
365
+
366
+ console.log(`[Conductor] Forwarded to ${ft.taskId}: ${ft.message.substring(0, 60)}`);
367
+ } else {
368
+ console.warn(`[Conductor] Unknown task: ${ft.taskId}`);
369
+ }
370
+ }
371
+
372
+ state.accumulatedText = '';
373
+ state.turnActive = false;
374
+
375
+ sendConductorMessage({
376
+ type: 'conductor_turn_completed',
377
+ sessionId: session.id
378
+ });
379
+ sendStatusUpdate(session);
380
+ }
381
+ }
382
+ } catch (error) {
383
+ if (error.name === 'AbortError') {
384
+ console.log(`[Conductor] Claude aborted`);
385
+ } else {
386
+ console.error(`[Conductor] Claude error:`, error.message);
387
+ endConductorStreaming(session);
388
+ state.turnActive = false;
389
+ state.query = null;
390
+ state.inputStream = null;
391
+ state.accumulatedText = '';
392
+
393
+ sendConductorMessage({
394
+ type: 'conductor_error',
395
+ sessionId: session.id,
396
+ error: error.message.substring(0, 500)
397
+ });
398
+
399
+ // 转入等待用户
400
+ session.status = 'waiting_user';
401
+ sendStatusUpdate(session);
402
+ }
403
+ } finally {
404
+ // 释放信号量
405
+ if (session._conductorSemRelease) {
406
+ session._conductorSemRelease();
407
+ session._conductorSemRelease = null;
408
+ session.activeClaudes = Math.max(0, (session.activeClaudes || 1) - 1);
409
+ }
410
+ }
411
+ }
412
+
413
+ /**
414
+ * 停止 Conductor Claude
415
+ */
416
+ export async function stopConductorClaude(session) {
417
+ const state = session.conductorState;
418
+ if (!state) return;
419
+
420
+ const sessionDataDir = getSessionDataDir(session.id);
421
+
422
+ if (state.claudeSessionId) {
423
+ await saveConductorSessionId(sessionDataDir, state.claudeSessionId)
424
+ .catch(e => console.warn('[Conductor] Failed to save sessionId:', e.message));
425
+ }
426
+
427
+ if (state.abortController) {
428
+ state.abortController.abort();
429
+ }
430
+
431
+ state.query = null;
432
+ state.inputStream = null;
433
+ state.turnActive = false;
434
+ session.conductorState = null;
435
+
436
+ if (session._conductorSemRelease) {
437
+ session._conductorSemRelease();
438
+ session._conductorSemRelease = null;
439
+ session.activeClaudes = Math.max(0, (session.activeClaudes || 1) - 1);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * 清空 Conductor Claude(强制新建对话)
445
+ */
446
+ export async function clearConductorClaude(session) {
447
+ await stopConductorClaude(session);
448
+ const sessionDataDir = getSessionDataDir(session.id);
449
+ await clearConductorSessionId(sessionDataDir);
450
+ }
451
+
452
+ // 导出解析函数供测试用
453
+ export { parseCreateTask, parseForwardTask };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Conductor — 模块入口
3
+ *
4
+ * 导出三大核心子系统:
5
+ * 1. Personas — 18 个人物定义(4 场景)
6
+ * 2. Specialties — specialty 行为定义(通用 + 场景特有)
7
+ * 3. CLAUDE.md Generator — 三段式动态生成
8
+ * 4. Actor — Claude 实例创建/管理/释放
9
+ * 5. Worktree — 按线程的 worktree 管理
10
+ */
11
+
12
+ // ---- Personas ----
13
+ export {
14
+ getPersonaById,
15
+ getPersonasByScenario,
16
+ getPersonaPoolForOrchestrator,
17
+ getAllPersonas,
18
+ getScenarios,
19
+ personasByScenario
20
+ } from './personas/index.js';
21
+
22
+ // ---- Specialties ----
23
+ export {
24
+ getSpecialty,
25
+ getSpecialtiesByScenario,
26
+ getThinkingMode,
27
+ isDivergent,
28
+ getPersonalizedBehavior,
29
+ getOutputFormat,
30
+ formatToolRules,
31
+ personaSupportsSpecialty,
32
+ allSpecialties,
33
+ scenarioSpecialties
34
+ } from './specialties/index.js';
35
+
36
+ // ---- CLAUDE.md Generator ----
37
+ export {
38
+ generateActorCLAUDEmd,
39
+ generateActorInstanceId,
40
+ generateActorDirName
41
+ } from './claudemd-generator.js';
42
+
43
+ // ---- Actor Management ----
44
+ export {
45
+ createActor,
46
+ sendToActor,
47
+ releaseActor,
48
+ releaseAllActors,
49
+ getActor,
50
+ getActiveActors,
51
+ getAllActors,
52
+ getActorPublicState,
53
+ getActorStates,
54
+ classifyActorError,
55
+ actorRegistry
56
+ } from './actor.js';
57
+
58
+ // ---- Worktree Management ----
59
+ export {
60
+ createThreadWorktree,
61
+ cleanupTaskWorktrees,
62
+ cleanupAllConductorWorktrees
63
+ } from './worktree.js';