@yeaft/webchat-agent 0.1.224 → 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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conductor — Actor Claude 实例管理
|
|
3
|
+
*
|
|
4
|
+
* Actor 是最底层的执行单元:
|
|
5
|
+
* - 创建 actor session(写 CLAUDE.md → 启动 Claude)
|
|
6
|
+
* - 发送任务 → 收集结果 → 回调 Orchestrator
|
|
7
|
+
* - 释放 actor(清理 session)
|
|
8
|
+
*
|
|
9
|
+
* 复用 V1 的 createRoleQuery 模式,适配 Conductor 三层架构
|
|
10
|
+
*/
|
|
11
|
+
import { query, Stream } from '../sdk/index.js';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { mkdirSync } from 'fs';
|
|
14
|
+
import ctx from '../context.js';
|
|
15
|
+
import { generateActorCLAUDEmd, generateActorDirName, generateActorInstanceId } from './claudemd-generator.js';
|
|
16
|
+
import { getPersonaById } from './personas/index.js';
|
|
17
|
+
import { getSpecialty, getThinkingMode } from './specialties/index.js';
|
|
18
|
+
|
|
19
|
+
// =====================================================================
|
|
20
|
+
// Actor Instance Registry
|
|
21
|
+
// =====================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-task actor registry: taskId → Map<instanceId, ActorInstance>
|
|
25
|
+
* @type {Map<string, Map<string, ActorInstance>>}
|
|
26
|
+
*/
|
|
27
|
+
const actorRegistry = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-actor mutex to prevent concurrent createActorQuery calls
|
|
31
|
+
* @type {Map<string, Promise>}
|
|
32
|
+
*/
|
|
33
|
+
const _actorLocks = new Map();
|
|
34
|
+
|
|
35
|
+
// =====================================================================
|
|
36
|
+
// Actor Lifecycle
|
|
37
|
+
// =====================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 创建 Actor 实例
|
|
41
|
+
*
|
|
42
|
+
* 流程:生成 CLAUDE.md → 创建目录 → 启动 Claude query(appendSystemPrompt 注入)→ 注册
|
|
43
|
+
*
|
|
44
|
+
* @param {object} params
|
|
45
|
+
* @param {string} params.personaId - persona ID
|
|
46
|
+
* @param {string} params.specialtyId - specialty ID
|
|
47
|
+
* @param {string} params.taskId - 所属 task ID
|
|
48
|
+
* @param {string} params.taskDir - task 目录路径 (.conductor/tasks/task-N)
|
|
49
|
+
* @param {object} params.taskContext - 传递给 CLAUDE.md 生成器的上下文
|
|
50
|
+
* @param {string} [params.threadId] - 并行线程 ID(coding actor 用)
|
|
51
|
+
* @param {string} [params.worktreePath] - worktree 路径
|
|
52
|
+
* @param {string} [params.model] - 指定模型
|
|
53
|
+
* @param {function} [params.onOutput] - 输出回调 (instanceId, message) => void
|
|
54
|
+
* @param {function} [params.onComplete] - 完成回调 (instanceId, result) => void
|
|
55
|
+
* @param {function} [params.onError] - 错误回调 (instanceId, error) => void
|
|
56
|
+
* @returns {Promise<ActorInstance>}
|
|
57
|
+
*/
|
|
58
|
+
export async function createActor(params) {
|
|
59
|
+
const {
|
|
60
|
+
personaId,
|
|
61
|
+
specialtyId,
|
|
62
|
+
taskId,
|
|
63
|
+
taskDir,
|
|
64
|
+
taskContext = {},
|
|
65
|
+
threadId,
|
|
66
|
+
worktreePath,
|
|
67
|
+
model,
|
|
68
|
+
onOutput,
|
|
69
|
+
onComplete,
|
|
70
|
+
onError
|
|
71
|
+
} = params;
|
|
72
|
+
|
|
73
|
+
const instanceId = generateActorInstanceId(specialtyId, personaId, taskId, threadId);
|
|
74
|
+
const lockKey = instanceId;
|
|
75
|
+
|
|
76
|
+
// Mutex: 防止并发创建同一个 actor
|
|
77
|
+
if (_actorLocks.has(lockKey)) {
|
|
78
|
+
console.log(`[Actor] Waiting for existing lock on ${instanceId}`);
|
|
79
|
+
try { await _actorLocks.get(lockKey); } catch { /* 前一次失败了,继续 */ }
|
|
80
|
+
const existing = getActor(taskId, instanceId);
|
|
81
|
+
if (existing && existing.status === 'working') {
|
|
82
|
+
console.log(`[Actor] Reusing existing actor ${instanceId}`);
|
|
83
|
+
return existing;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const promise = _createActorInner(params, instanceId);
|
|
88
|
+
_actorLocks.set(lockKey, promise);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
return await promise;
|
|
92
|
+
} finally {
|
|
93
|
+
if (_actorLocks.get(lockKey) === promise) {
|
|
94
|
+
_actorLocks.delete(lockKey);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* createActor 内部实现
|
|
101
|
+
*/
|
|
102
|
+
async function _createActorInner(params, instanceId) {
|
|
103
|
+
const {
|
|
104
|
+
personaId,
|
|
105
|
+
specialtyId,
|
|
106
|
+
taskId,
|
|
107
|
+
taskDir,
|
|
108
|
+
taskContext = {},
|
|
109
|
+
threadId,
|
|
110
|
+
worktreePath,
|
|
111
|
+
model,
|
|
112
|
+
onOutput,
|
|
113
|
+
onComplete,
|
|
114
|
+
onError
|
|
115
|
+
} = params;
|
|
116
|
+
|
|
117
|
+
const persona = getPersonaById(personaId);
|
|
118
|
+
if (!persona) throw new Error(`Persona not found: ${personaId}`);
|
|
119
|
+
|
|
120
|
+
const specialty = getSpecialty(specialtyId);
|
|
121
|
+
if (!specialty) throw new Error(`Specialty not found: ${specialtyId}`);
|
|
122
|
+
|
|
123
|
+
// 1. 生成 CLAUDE.md
|
|
124
|
+
const claudemdContent = generateActorCLAUDEmd(persona, specialtyId, {
|
|
125
|
+
...taskContext,
|
|
126
|
+
worktreePath
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 2. 创建 actor 目录
|
|
130
|
+
const actorDirName = generateActorDirName(specialtyId, personaId, threadId);
|
|
131
|
+
const actorDir = join(taskDir, 'actors', actorDirName);
|
|
132
|
+
mkdirSync(actorDir, { recursive: true });
|
|
133
|
+
|
|
134
|
+
// 3. 确定 cwd(coding actor 用 worktree,其他用 actor 目录)
|
|
135
|
+
// CLAUDE.md 通过 appendSystemPrompt 注入 SDK,不写入文件(避免双重注入)
|
|
136
|
+
const actorCwd = worktreePath || actorDir;
|
|
137
|
+
mkdirSync(actorCwd, { recursive: true });
|
|
138
|
+
|
|
139
|
+
// 5. 创建 Claude query
|
|
140
|
+
const inputStream = new Stream();
|
|
141
|
+
const abortController = new AbortController();
|
|
142
|
+
|
|
143
|
+
// 继承全局 disallowedTools
|
|
144
|
+
const globalDisallowed = ctx.CONFIG?.disallowedTools || [];
|
|
145
|
+
const actorDisallowed = ['Agent']; // Actor 不允许使用 Agent 工具
|
|
146
|
+
const effectiveDisallowed = [...globalDisallowed, ...actorDisallowed];
|
|
147
|
+
|
|
148
|
+
const queryOptions = {
|
|
149
|
+
cwd: actorCwd,
|
|
150
|
+
permissionMode: 'bypassPermissions',
|
|
151
|
+
abort: abortController.signal,
|
|
152
|
+
model: model || undefined,
|
|
153
|
+
appendSystemPrompt: claudemdContent,
|
|
154
|
+
...(effectiveDisallowed.length > 0 && { disallowedTools: effectiveDisallowed })
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const roleQuery = query({
|
|
158
|
+
prompt: inputStream,
|
|
159
|
+
options: queryOptions
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 6. 创建 ActorInstance
|
|
163
|
+
const thinkingMode = getThinkingMode(specialtyId);
|
|
164
|
+
|
|
165
|
+
/** @type {ActorInstance} */
|
|
166
|
+
const instance = {
|
|
167
|
+
instanceId,
|
|
168
|
+
personaId,
|
|
169
|
+
personaName: persona.name,
|
|
170
|
+
specialtyId,
|
|
171
|
+
specialtyDisplayName: specialty.displayName,
|
|
172
|
+
thinkingMode,
|
|
173
|
+
taskId,
|
|
174
|
+
threadId: threadId || null,
|
|
175
|
+
worktreePath: worktreePath || null,
|
|
176
|
+
actorDir,
|
|
177
|
+
claudeSessionId: null,
|
|
178
|
+
status: 'working',
|
|
179
|
+
activity: `${persona.name} 正在执行 ${specialty.displayName}`,
|
|
180
|
+
startedAt: Date.now(),
|
|
181
|
+
// 内部管理
|
|
182
|
+
_query: roleQuery,
|
|
183
|
+
_inputStream: inputStream,
|
|
184
|
+
_abortController: abortController,
|
|
185
|
+
_accumulatedText: '',
|
|
186
|
+
_onOutput: onOutput || null,
|
|
187
|
+
_onComplete: onComplete || null,
|
|
188
|
+
_onError: onError || null
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// 7. 注册到 registry
|
|
192
|
+
if (!actorRegistry.has(taskId)) {
|
|
193
|
+
actorRegistry.set(taskId, new Map());
|
|
194
|
+
}
|
|
195
|
+
actorRegistry.get(taskId).set(instanceId, instance);
|
|
196
|
+
|
|
197
|
+
// 8. 异步处理 Actor 输出
|
|
198
|
+
processActorOutput(instance);
|
|
199
|
+
|
|
200
|
+
console.log(`[Actor] Created ${instanceId}: ${persona.name} × ${specialtyId} (${thinkingMode})`);
|
|
201
|
+
return instance;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =====================================================================
|
|
205
|
+
// Actor Communication
|
|
206
|
+
// =====================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 向 Actor 发送任务消息
|
|
210
|
+
*
|
|
211
|
+
* @param {string} taskId - task ID
|
|
212
|
+
* @param {string} instanceId - actor instance ID
|
|
213
|
+
* @param {string} message - 要发送的消息
|
|
214
|
+
*/
|
|
215
|
+
export function sendToActor(taskId, instanceId, message) {
|
|
216
|
+
const instance = getActor(taskId, instanceId);
|
|
217
|
+
if (!instance) {
|
|
218
|
+
console.error(`[Actor] Cannot send to unknown actor: ${instanceId}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (instance._inputStream.isDone) {
|
|
223
|
+
console.error(`[Actor] Cannot send to completed actor: ${instanceId}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`[Actor] Sending message to ${instanceId} (${message.length} chars)`);
|
|
228
|
+
instance._inputStream.enqueue(message);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 异步处理 Actor 输出流
|
|
233
|
+
*/
|
|
234
|
+
async function processActorOutput(instance) {
|
|
235
|
+
const { instanceId, _query: roleQuery } = instance;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
for await (const message of roleQuery) {
|
|
239
|
+
// 提取 sessionId
|
|
240
|
+
if (message.sessionId && !instance.claudeSessionId) {
|
|
241
|
+
instance.claudeSessionId = message.sessionId;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 处理助手消息
|
|
245
|
+
if (message.type === 'assistant') {
|
|
246
|
+
const text = extractText(message);
|
|
247
|
+
if (text) {
|
|
248
|
+
instance._accumulatedText += text;
|
|
249
|
+
|
|
250
|
+
// 实时输出回调
|
|
251
|
+
if (instance._onOutput) {
|
|
252
|
+
try {
|
|
253
|
+
instance._onOutput(instanceId, message);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
console.error(`[Actor] onOutput callback error for ${instanceId}:`, e.message);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// turn 结束的标志
|
|
262
|
+
if (message.type === 'result') {
|
|
263
|
+
instance.status = 'done';
|
|
264
|
+
instance.activity = '已完成';
|
|
265
|
+
|
|
266
|
+
const result = {
|
|
267
|
+
instanceId,
|
|
268
|
+
personaId: instance.personaId,
|
|
269
|
+
personaName: instance.personaName,
|
|
270
|
+
specialtyId: instance.specialtyId,
|
|
271
|
+
thinkingMode: instance.thinkingMode,
|
|
272
|
+
text: instance._accumulatedText,
|
|
273
|
+
costUsd: message.costUSD || 0,
|
|
274
|
+
inputTokens: message.inputTokens || 0,
|
|
275
|
+
outputTokens: message.outputTokens || 0,
|
|
276
|
+
duration: Date.now() - instance.startedAt
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
console.log(`[Actor] ${instanceId} completed (${result.duration}ms, $${result.costUsd.toFixed(4)})`);
|
|
280
|
+
|
|
281
|
+
if (instance._onComplete) {
|
|
282
|
+
try {
|
|
283
|
+
instance._onComplete(instanceId, result);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.error(`[Actor] onComplete callback error for ${instanceId}:`, e.message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
instance.status = 'failed';
|
|
292
|
+
instance.activity = `失败: ${error.message}`;
|
|
293
|
+
console.error(`[Actor] ${instanceId} error:`, error.message);
|
|
294
|
+
|
|
295
|
+
if (instance._onError) {
|
|
296
|
+
try {
|
|
297
|
+
instance._onError(instanceId, error);
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.error(`[Actor] onError callback error for ${instanceId}:`, e.message);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 从消息中提取文本内容
|
|
307
|
+
*/
|
|
308
|
+
function extractText(message) {
|
|
309
|
+
if (!message.message?.content) return '';
|
|
310
|
+
const content = message.message.content;
|
|
311
|
+
if (typeof content === 'string') return content;
|
|
312
|
+
if (Array.isArray(content)) {
|
|
313
|
+
return content
|
|
314
|
+
.filter(c => c.type === 'text')
|
|
315
|
+
.map(c => c.text)
|
|
316
|
+
.join('');
|
|
317
|
+
}
|
|
318
|
+
return '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// =====================================================================
|
|
322
|
+
// Actor Release & Cleanup
|
|
323
|
+
// =====================================================================
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 释放 Actor 实例(清理 session)
|
|
327
|
+
*
|
|
328
|
+
* @param {string} taskId
|
|
329
|
+
* @param {string} instanceId
|
|
330
|
+
* @param {object} [options]
|
|
331
|
+
* @param {boolean} [options.force=false] - 强制终止(abort)
|
|
332
|
+
*/
|
|
333
|
+
export async function releaseActor(taskId, instanceId, options = {}) {
|
|
334
|
+
const instance = getActor(taskId, instanceId);
|
|
335
|
+
if (!instance) {
|
|
336
|
+
console.warn(`[Actor] Cannot release unknown actor: ${instanceId}`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { force = false } = options;
|
|
341
|
+
|
|
342
|
+
// 关闭 input stream
|
|
343
|
+
if (!instance._inputStream.isDone) {
|
|
344
|
+
instance._inputStream.done();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 强制终止
|
|
348
|
+
if (force && instance._abortController) {
|
|
349
|
+
instance._abortController.abort();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
instance.status = 'done';
|
|
353
|
+
instance.activity = '已释放';
|
|
354
|
+
|
|
355
|
+
// 从 registry 移除,防止内存泄漏
|
|
356
|
+
const taskActors = actorRegistry.get(taskId);
|
|
357
|
+
if (taskActors) {
|
|
358
|
+
taskActors.delete(instanceId);
|
|
359
|
+
if (taskActors.size === 0) actorRegistry.delete(taskId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log(`[Actor] Released ${instanceId}${force ? ' (forced)' : ''}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 释放指定 task 的全部 actor
|
|
367
|
+
*
|
|
368
|
+
* @param {string} taskId
|
|
369
|
+
* @param {object} [options]
|
|
370
|
+
* @param {boolean} [options.force=false]
|
|
371
|
+
*/
|
|
372
|
+
export async function releaseAllActors(taskId, options = {}) {
|
|
373
|
+
const taskActors = actorRegistry.get(taskId);
|
|
374
|
+
if (!taskActors) return;
|
|
375
|
+
|
|
376
|
+
const promises = [];
|
|
377
|
+
for (const [instanceId] of taskActors) {
|
|
378
|
+
promises.push(releaseActor(taskId, instanceId, options));
|
|
379
|
+
}
|
|
380
|
+
await Promise.allSettled(promises);
|
|
381
|
+
|
|
382
|
+
actorRegistry.delete(taskId);
|
|
383
|
+
console.log(`[Actor] Released all actors for task ${taskId}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// =====================================================================
|
|
387
|
+
// Actor Query
|
|
388
|
+
// =====================================================================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 获取单个 actor 实例
|
|
392
|
+
* @param {string} taskId
|
|
393
|
+
* @param {string} instanceId
|
|
394
|
+
* @returns {ActorInstance|null}
|
|
395
|
+
*/
|
|
396
|
+
export function getActor(taskId, instanceId) {
|
|
397
|
+
return actorRegistry.get(taskId)?.get(instanceId) || null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 获取指定 task 的全部活跃 actor
|
|
402
|
+
* @param {string} taskId
|
|
403
|
+
* @returns {ActorInstance[]}
|
|
404
|
+
*/
|
|
405
|
+
export function getActiveActors(taskId) {
|
|
406
|
+
const taskActors = actorRegistry.get(taskId);
|
|
407
|
+
if (!taskActors) return [];
|
|
408
|
+
return Array.from(taskActors.values()).filter(a => a.status === 'working');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 获取指定 task 的全部 actor(含已完成)
|
|
413
|
+
* @param {string} taskId
|
|
414
|
+
* @returns {ActorInstance[]}
|
|
415
|
+
*/
|
|
416
|
+
export function getAllActors(taskId) {
|
|
417
|
+
const taskActors = actorRegistry.get(taskId);
|
|
418
|
+
if (!taskActors) return [];
|
|
419
|
+
return Array.from(taskActors.values());
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 获取 actor 实例的公开状态(用于前端展示)
|
|
424
|
+
* @param {ActorInstance} instance
|
|
425
|
+
* @returns {object}
|
|
426
|
+
*/
|
|
427
|
+
export function getActorPublicState(instance) {
|
|
428
|
+
return {
|
|
429
|
+
instanceId: instance.instanceId,
|
|
430
|
+
personaId: instance.personaId,
|
|
431
|
+
personaName: instance.personaName,
|
|
432
|
+
specialtyId: instance.specialtyId,
|
|
433
|
+
specialtyDisplayName: instance.specialtyDisplayName,
|
|
434
|
+
thinkingMode: instance.thinkingMode,
|
|
435
|
+
taskId: instance.taskId,
|
|
436
|
+
threadId: instance.threadId,
|
|
437
|
+
status: instance.status,
|
|
438
|
+
activity: instance.activity,
|
|
439
|
+
startedAt: instance.startedAt
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* 获取指定 task 全部 actor 的公开状态列表
|
|
445
|
+
* @param {string} taskId
|
|
446
|
+
* @returns {object[]}
|
|
447
|
+
*/
|
|
448
|
+
export function getActorStates(taskId) {
|
|
449
|
+
return getAllActors(taskId).map(getActorPublicState);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// =====================================================================
|
|
453
|
+
// Error Classification (复用 V1 模式)
|
|
454
|
+
// =====================================================================
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 判断 actor 错误是否可恢复
|
|
458
|
+
* @param {Error} error
|
|
459
|
+
* @returns {{ recoverable: boolean, reason: string }}
|
|
460
|
+
*/
|
|
461
|
+
export function classifyActorError(error) {
|
|
462
|
+
const msg = error.message || '';
|
|
463
|
+
if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
|
|
464
|
+
return { recoverable: true, reason: 'context_exceeded' };
|
|
465
|
+
}
|
|
466
|
+
if (/rate.?limit|429|overloaded|503|502|timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) {
|
|
467
|
+
return { recoverable: true, reason: 'transient_api_error' };
|
|
468
|
+
}
|
|
469
|
+
if (/exited with code [1-9]/i.test(msg) && msg.length < 100) {
|
|
470
|
+
return { recoverable: true, reason: 'process_crashed' };
|
|
471
|
+
}
|
|
472
|
+
if (/spawn|ENOENT|not found/i.test(msg)) {
|
|
473
|
+
return { recoverable: false, reason: 'spawn_failed' };
|
|
474
|
+
}
|
|
475
|
+
return { recoverable: true, reason: 'unknown' };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export { actorRegistry };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conductor — 三段式 CLAUDE.md 生成器
|
|
3
|
+
*
|
|
4
|
+
* CLAUDE.md = 人物基底(persona base) + specialty 行为(specialty behavior) + 任务上下文(task context)
|
|
5
|
+
*
|
|
6
|
+
* 核心原则:一个实例一个文件。每个 persona × specialty 组合生成独立的 CLAUDE.md。
|
|
7
|
+
* Torvalds 做 planning 和 Torvalds 做 coding 是两个完全不同的文件。
|
|
8
|
+
*/
|
|
9
|
+
import { getPersonaById } from './personas/index.js';
|
|
10
|
+
import {
|
|
11
|
+
getSpecialty,
|
|
12
|
+
getThinkingMode,
|
|
13
|
+
getPersonalizedBehavior,
|
|
14
|
+
getOutputFormat,
|
|
15
|
+
formatToolRules
|
|
16
|
+
} from './specialties/index.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 生成 Actor 的 CLAUDE.md 内容
|
|
20
|
+
*
|
|
21
|
+
* @param {object|string} personaOrId - persona 对象 或 persona ID
|
|
22
|
+
* @param {string} specialtyId - specialty ID (e.g. 'planning', 'coding', 'review')
|
|
23
|
+
* @param {object} taskContext - 任务上下文
|
|
24
|
+
* @param {string} taskContext.taskId - 任务 ID
|
|
25
|
+
* @param {string} taskContext.description - 任务描述
|
|
26
|
+
* @param {string} [taskContext.memoryPath] - memory.md 路径
|
|
27
|
+
* @param {string} [taskContext.planPath] - plan.json 路径
|
|
28
|
+
* @param {string} [taskContext.worktreePath] - worktree 路径 (coding actor 用)
|
|
29
|
+
* @param {string} [taskContext.reviewTarget] - 审查目标描述 (review actor 用)
|
|
30
|
+
* @param {string} [taskContext.assignedStep] - 分配的执行步骤 (coding actor 用)
|
|
31
|
+
* @param {string} [taskContext.scenario] - 场景名称
|
|
32
|
+
* @param {object} [taskContext.extra] - 额外上下文(自由格式)
|
|
33
|
+
* @returns {string} 完整的 CLAUDE.md 内容
|
|
34
|
+
*/
|
|
35
|
+
export function generateActorCLAUDEmd(personaOrId, specialtyId, taskContext = {}) {
|
|
36
|
+
// 解析 persona
|
|
37
|
+
const persona = typeof personaOrId === 'string'
|
|
38
|
+
? getPersonaById(personaOrId)
|
|
39
|
+
: personaOrId;
|
|
40
|
+
|
|
41
|
+
if (!persona) {
|
|
42
|
+
throw new Error(`Persona not found: ${personaOrId}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const specialty = getSpecialty(specialtyId);
|
|
46
|
+
if (!specialty) {
|
|
47
|
+
throw new Error(`Specialty not found: ${specialtyId}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 三段生成
|
|
51
|
+
const section1 = generatePersonaBase(persona, specialty);
|
|
52
|
+
const section2 = generateSpecialtyBehavior(persona, specialty);
|
|
53
|
+
const section3 = generateTaskContext(persona, specialty, taskContext);
|
|
54
|
+
|
|
55
|
+
return `${section1}
|
|
56
|
+
|
|
57
|
+
${section2}
|
|
58
|
+
|
|
59
|
+
${section3}`.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =====================================================================
|
|
63
|
+
// 第一段:人物基底
|
|
64
|
+
// =====================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 生成人物基底部分
|
|
68
|
+
*/
|
|
69
|
+
function generatePersonaBase(persona, specialty) {
|
|
70
|
+
return `# 你是 ${persona.name} — 此刻你在做 ${specialty.displayName}
|
|
71
|
+
|
|
72
|
+
${persona.base}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =====================================================================
|
|
76
|
+
// 第二段:Specialty 行为(结合人物个性定制)
|
|
77
|
+
// =====================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 生成 specialty 行为部分
|
|
81
|
+
*/
|
|
82
|
+
function generateSpecialtyBehavior(persona, specialty) {
|
|
83
|
+
const thinkingMode = getThinkingMode(specialty.id);
|
|
84
|
+
const thinkingLabel = thinkingMode === 'divergent' ? '发散思维' : '收敛执行';
|
|
85
|
+
|
|
86
|
+
const personalizedBehavior = getPersonalizedBehavior(specialty.id, persona);
|
|
87
|
+
const outputFormat = getOutputFormat(specialty.id);
|
|
88
|
+
const toolRules = formatToolRules(specialty.id);
|
|
89
|
+
|
|
90
|
+
return `## 作为 ${specialty.displayName} 的你(${thinkingLabel})
|
|
91
|
+
${personalizedBehavior}
|
|
92
|
+
|
|
93
|
+
## 输出要求
|
|
94
|
+
${outputFormat}
|
|
95
|
+
|
|
96
|
+
## 工具使用
|
|
97
|
+
${toolRules}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =====================================================================
|
|
101
|
+
// 第三段:任务上下文(动态注入)
|
|
102
|
+
// =====================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 生成任务上下文部分
|
|
106
|
+
*/
|
|
107
|
+
function generateTaskContext(persona, specialty, ctx) {
|
|
108
|
+
const lines = ['## Task 上下文'];
|
|
109
|
+
|
|
110
|
+
if (ctx.taskId) {
|
|
111
|
+
lines.push(`- 任务 ID:${ctx.taskId}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (ctx.description) {
|
|
115
|
+
lines.push(`- 任务描述:${ctx.description}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ctx.scenario) {
|
|
119
|
+
lines.push(`- 场景:${ctx.scenario}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (ctx.assignedStep) {
|
|
123
|
+
lines.push(`- 执行步骤:${ctx.assignedStep}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (ctx.reviewTarget) {
|
|
127
|
+
lines.push(`- 审查目标:${ctx.reviewTarget}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (ctx.memoryPath) {
|
|
131
|
+
lines.push(`- 共享记忆:${ctx.memoryPath}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (ctx.planPath) {
|
|
135
|
+
lines.push(`- 计划文件:${ctx.planPath}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// worktree 路径和纪律规则(coding actor 专用)
|
|
139
|
+
if (ctx.worktreePath) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push('## Worktree 纪律');
|
|
142
|
+
lines.push(`- 代码工作目录:${ctx.worktreePath}`);
|
|
143
|
+
lines.push('- 所有代码操作必须在此目录内');
|
|
144
|
+
lines.push('- 禁止在主目录或 main 分支修改代码');
|
|
145
|
+
lines.push('- 完成后通过回调通知 Orchestrator');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 额外上下文
|
|
149
|
+
if (ctx.extra) {
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push('## 补充信息');
|
|
152
|
+
if (typeof ctx.extra === 'string') {
|
|
153
|
+
lines.push(ctx.extra);
|
|
154
|
+
} else {
|
|
155
|
+
for (const [key, value] of Object.entries(ctx.extra)) {
|
|
156
|
+
lines.push(`- ${key}:${value}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 通用结尾指令
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push('## 工作完成后');
|
|
164
|
+
lines.push('完成工作后,请输出你的产出结果。Orchestrator 会接收并处理。');
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 生成 Actor 实例 ID
|
|
171
|
+
*
|
|
172
|
+
* @param {string} specialtyId - specialty ID
|
|
173
|
+
* @param {string} personaId - persona ID
|
|
174
|
+
* @param {string} taskId - task ID
|
|
175
|
+
* @param {string} [threadId] - 并行线程 ID(可选)
|
|
176
|
+
* @returns {string} 实例 ID,如 "planning-jobs-task-1" 或 "coding-torvalds-task-1-t1"
|
|
177
|
+
*/
|
|
178
|
+
export function generateActorInstanceId(specialtyId, personaId, taskId, threadId) {
|
|
179
|
+
const base = `${specialtyId}-${personaId}-${taskId}`;
|
|
180
|
+
return threadId ? `${base}-${threadId}` : base;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 生成 Actor 的目录名(用于 .conductor/tasks/task-N/actors/ 下)
|
|
185
|
+
*
|
|
186
|
+
* @param {string} specialtyId
|
|
187
|
+
* @param {string} personaId
|
|
188
|
+
* @param {string} [threadId]
|
|
189
|
+
* @returns {string} 目录名,如 "planning-jobs" 或 "coding-torvalds-t1"
|
|
190
|
+
*/
|
|
191
|
+
export function generateActorDirName(specialtyId, personaId, threadId) {
|
|
192
|
+
const base = `${specialtyId}-${personaId}`;
|
|
193
|
+
return threadId ? `${base}-${threadId}` : base;
|
|
194
|
+
}
|