@yeaft/webchat-agent 0.0.229 → 0.0.231

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.
Files changed (2) hide show
  1. package/crew.js +32 -3089
  2. package/package.json +1 -1
package/crew.js CHANGED
@@ -10,3093 +10,36 @@
10
10
  * - 共享级 .crew/CLAUDE.md(所有角色自动继承)
11
11
  * - Session resume(每个角色的 claudeSessionId 持久化)
12
12
  * - 自动路由 + 人工混合
13
- */
14
-
15
- import { query, Stream } from './sdk/index.js';
16
- import { promises as fs } from 'fs';
17
- import { join, isAbsolute } from 'path';
18
- import { homedir } from 'os';
19
- import { execFile as execFileCb } from 'child_process';
20
- import { promisify } from 'util';
21
- import ctx from './context.js';
22
- import { getMessages } from './crew-i18n.js';
23
-
24
- const execFile = promisify(execFileCb);
25
-
26
- /** Format role label: "icon displayName" or just "displayName" if no icon */
27
- function roleLabel(r) {
28
- return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
29
- }
30
-
31
- // =====================================================================
32
- // Data Structures
33
- // =====================================================================
34
-
35
- /** @type {Map<string, CrewSession>} */
36
- const crewSessions = new Map();
37
-
38
- // 导出供 connection.js / conversation.js 使用
39
- export { crewSessions };
40
-
41
- // =====================================================================
42
- // Role Multi-Instance Expansion
43
- // =====================================================================
44
-
45
- // 短前缀映射:用于 count > 1 时生成实例名
46
- const SHORT_PREFIX = {
47
- developer: 'dev',
48
- tester: 'test',
49
- reviewer: 'rev'
50
- };
51
-
52
- // 只有执行者角色支持多实例
53
- const EXPANDABLE_ROLES = new Set(['developer', 'tester', 'reviewer']);
54
-
55
- /**
56
- * 展开角色列表:count > 1 的执行者角色展开为多个实例
57
- * count === 1 或管理者角色保持原样(向后兼容)
58
13
  *
59
- * @param {Array} roles - 原始角色配置
60
- * @returns {Array} 展开后的角色列表
61
- */
62
- function expandRoles(roles) {
63
- // 找到 developer 的 count,reviewer/tester 自动跟随
64
- const devRole = roles.find(r => r.name === 'developer');
65
- const devCount = devRole?.count > 1 ? devRole.count : 1;
66
-
67
- const expanded = [];
68
- for (const role of roles) {
69
- const isExpandable = EXPANDABLE_ROLES.has(role.name);
70
- // reviewer/tester 跟随 developer 的 count
71
- const count = isExpandable ? devCount : 1;
72
-
73
- if (count <= 1) {
74
- // 单实例:保持原名,expandable 角色也分配 groupIndex=1 以获得独立 worktree
75
- expanded.push({
76
- ...role,
77
- roleType: role.name,
78
- groupIndex: isExpandable ? 1 : 0
79
- });
80
- } else {
81
- // 多实例展开
82
- const prefix = SHORT_PREFIX[role.name] || role.name;
83
- for (let i = 1; i <= count; i++) {
84
- expanded.push({
85
- ...role,
86
- name: `${prefix}-${i}`,
87
- displayName: `${role.displayName}-${i}`,
88
- roleType: role.name,
89
- groupIndex: i,
90
- count: undefined // 展开后不再需要 count
91
- });
92
- }
93
- }
94
- }
95
- return expanded;
96
- }
97
-
98
- // =====================================================================
99
- // Git Worktree Management
100
- // =====================================================================
101
-
102
- /**
103
- * 为开发组创建 git worktree
104
- * 每个 groupIndex 对应一个 worktree,同组的 dev/rev/test 共享
105
- * 所有 EXPANDABLE_ROLES(包括 count=1)都会获得独立 worktree
106
- *
107
- * @param {string} projectDir - 主项目目录
108
- * @param {Array} roles - 展开后的角色列表
109
- * @returns {Map<number, string>} groupIndex → worktree 路径
110
- */
111
- async function initWorktrees(projectDir, roles) {
112
- const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
113
- if (groupIndices.length === 0) return new Map();
114
-
115
- const worktreeBase = join(projectDir, '.worktrees');
116
- await fs.mkdir(worktreeBase, { recursive: true });
117
-
118
- // 获取 git 已知的 worktree 列表
119
- let knownWorktrees = new Set();
120
- try {
121
- const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], { cwd: projectDir });
122
- for (const line of stdout.split('\n')) {
123
- if (line.startsWith('worktree ')) {
124
- knownWorktrees.add(line.slice('worktree '.length).trim());
125
- }
126
- }
127
- } catch {
128
- // git worktree list 失败,视为空集
129
- }
130
-
131
- const worktreeMap = new Map();
132
-
133
- for (const idx of groupIndices) {
134
- const wtDir = join(worktreeBase, `dev-${idx}`);
135
- const branch = `crew/dev-${idx}`;
136
-
137
- // 检查目录是否存在
138
- let dirExists = false;
139
- try {
140
- await fs.access(wtDir);
141
- dirExists = true;
142
- } catch {}
143
-
144
- if (dirExists) {
145
- if (knownWorktrees.has(wtDir)) {
146
- // 目录存在且 git 记录中也有,直接复用
147
- console.log(`[Crew] Worktree already exists: ${wtDir}`);
148
- worktreeMap.set(idx, wtDir);
149
- continue;
150
- } else {
151
- // 孤立目录:目录存在但 git 不认识,先删除再重建
152
- console.warn(`[Crew] Orphaned worktree dir, removing: ${wtDir}`);
153
- await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
154
- }
155
- }
156
-
157
- try {
158
- // 创建分支(如果不存在)
159
- try {
160
- await execFile('git', ['branch', branch], { cwd: projectDir });
161
- } catch {
162
- // 分支已存在,忽略
163
- }
164
-
165
- // 创建 worktree
166
- await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: projectDir });
167
- console.log(`[Crew] Created worktree: ${wtDir} on branch ${branch}`);
168
- worktreeMap.set(idx, wtDir);
169
- } catch (e) {
170
- console.error(`[Crew] Failed to create worktree for group ${idx}:`, e.message);
171
- }
172
- }
173
-
174
- return worktreeMap;
175
- }
176
-
177
- /**
178
- * 清理 session 的 git worktrees
179
- * @param {string} projectDir - 主项目目录
180
- */
181
- async function cleanupWorktrees(projectDir) {
182
- const worktreeBase = join(projectDir, '.worktrees');
183
-
184
- try {
185
- await fs.access(worktreeBase);
186
- } catch {
187
- return; // .worktrees 目录不存在,无需清理
188
- }
189
-
190
- try {
191
- const entries = await fs.readdir(worktreeBase);
192
- for (const entry of entries) {
193
- if (!entry.startsWith('dev-')) continue;
194
- const wtDir = join(worktreeBase, entry);
195
- const branch = `crew/${entry}`;
196
-
197
- try {
198
- await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: projectDir });
199
- console.log(`[Crew] Removed worktree: ${wtDir}`);
200
- } catch (e) {
201
- console.warn(`[Crew] Failed to remove worktree ${wtDir}:`, e.message);
202
- }
203
-
204
- try {
205
- await execFile('git', ['branch', '-D', branch], { cwd: projectDir });
206
- console.log(`[Crew] Deleted branch: ${branch}`);
207
- } catch (e) {
208
- console.warn(`[Crew] Failed to delete branch ${branch}:`, e.message);
209
- }
210
- }
211
-
212
- // 尝试删除 .worktrees 目录(如果已空)
213
- try {
214
- await fs.rmdir(worktreeBase);
215
- } catch {
216
- // 目录不空或其他原因,忽略
217
- }
218
- } catch (e) {
219
- console.error(`[Crew] Failed to cleanup worktrees:`, e.message);
220
- }
221
- }
222
-
223
- // =====================================================================
224
- // Crew Session Index (~/.claude/crew-sessions.json)
225
- // =====================================================================
226
-
227
- const CREW_INDEX_PATH = join(homedir(), '.claude', 'crew-sessions.json');
228
-
229
- // 写入锁:防止并发写入导致文件损坏
230
- let _indexWriteLock = Promise.resolve();
231
-
232
- export async function loadCrewIndex() {
233
- try { return JSON.parse(await fs.readFile(CREW_INDEX_PATH, 'utf-8')); }
234
- catch { return []; }
235
- }
236
-
237
- async function saveCrewIndex(index) {
238
- const doWrite = async () => {
239
- await fs.mkdir(join(homedir(), '.claude'), { recursive: true });
240
- const data = JSON.stringify(index, null, 2);
241
- // 先写临时文件再 rename,保证原子性
242
- const tmpPath = CREW_INDEX_PATH + '.tmp';
243
- await fs.writeFile(tmpPath, data);
244
- await fs.rename(tmpPath, CREW_INDEX_PATH);
245
- };
246
- // 串行化写入
247
- _indexWriteLock = _indexWriteLock.then(doWrite, doWrite);
248
- return _indexWriteLock;
249
- }
250
-
251
- function sessionToIndexEntry(session) {
252
- return {
253
- sessionId: session.id,
254
- projectDir: session.projectDir,
255
- sharedDir: session.sharedDir,
256
- status: session.status,
257
- name: session.name || '',
258
- userId: session.userId,
259
- username: session.username,
260
- agentId: session.agentId || null,
261
- createdAt: session.createdAt,
262
- updatedAt: Date.now()
263
- };
264
- }
265
-
266
- async function upsertCrewIndex(session) {
267
- const index = await loadCrewIndex();
268
- const entry = sessionToIndexEntry(session);
269
- const idx = index.findIndex(e => e.sessionId === session.id);
270
- if (idx >= 0) index[idx] = entry; else index.push(entry);
271
- await saveCrewIndex(index);
272
- }
273
-
274
- export async function removeFromCrewIndex(sessionId) {
275
- const index = await loadCrewIndex();
276
- const entry = index.find(e => e.sessionId === sessionId);
277
- const filtered = index.filter(e => e.sessionId !== sessionId);
278
- if (filtered.length !== index.length) {
279
- await saveCrewIndex(filtered);
280
- console.log(`[Crew] Removed session ${sessionId} from index`);
281
- }
282
- // 从内存中也移除(防止 sendConversationList 重新加入)
283
- if (crewSessions.has(sessionId)) {
284
- crewSessions.delete(sessionId);
285
- console.log(`[Crew] Removed session ${sessionId} from active sessions`);
286
- }
287
- // 删除磁盘上的 session 数据文件
288
- const sharedDir = entry?.sharedDir;
289
- if (sharedDir) {
290
- try {
291
- for (const file of ['session.json', 'messages.json']) {
292
- await fs.unlink(join(sharedDir, file)).catch(() => {});
293
- }
294
- // Clean up message shard files
295
- await cleanupMessageShards(sharedDir);
296
- console.log(`[Crew] Cleaned session files in ${sharedDir}`);
297
- } catch (e) {
298
- console.warn(`[Crew] Failed to clean session files:`, e.message);
299
- }
300
- }
301
- }
302
-
303
- // =====================================================================
304
- // Session Metadata (.crew/session.json)
305
- // =====================================================================
306
-
307
- const MESSAGE_SHARD_SIZE = 256 * 1024; // 256KB per shard
308
-
309
- async function saveSessionMeta(session) {
310
- const meta = {
311
- sessionId: session.id,
312
- projectDir: session.projectDir,
313
- sharedDir: session.sharedDir,
314
- name: session.name || '',
315
- status: session.status,
316
- roles: Array.from(session.roles.values()).map(r => ({
317
- name: r.name, displayName: r.displayName, icon: r.icon,
318
- description: r.description, isDecisionMaker: r.isDecisionMaker || false,
319
- groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
320
- })),
321
- decisionMaker: session.decisionMaker,
322
- round: session.round,
323
- createdAt: session.createdAt,
324
- updatedAt: Date.now(),
325
- userId: session.userId,
326
- username: session.username,
327
- agentId: session.agentId || null,
328
- teamType: session.teamType || 'dev',
329
- language: session.language || 'zh-CN',
330
- costUsd: session.costUsd,
331
- totalInputTokens: session.totalInputTokens,
332
- totalOutputTokens: session.totalOutputTokens,
333
- features: Array.from(session.features.values()),
334
- _completedTaskIds: Array.from(session._completedTaskIds || [])
335
- };
336
- await fs.writeFile(join(session.sharedDir, 'session.json'), JSON.stringify(meta, null, 2));
337
- // 保存 UI 消息历史(用于恢复时重放)
338
- if (session.uiMessages && session.uiMessages.length > 0) {
339
- // 清理 _streaming 标记后保存
340
- const cleaned = session.uiMessages.map(m => {
341
- const { _streaming, ...rest } = m;
342
- return rest;
343
- });
344
- const json = JSON.stringify(cleaned);
345
- // 超过阈值时直接归档(rotateMessages 内部写两个文件,避免双写)
346
- if (json.length > MESSAGE_SHARD_SIZE && !session._rotating) {
347
- await rotateMessages(session, cleaned);
348
- } else {
349
- await fs.writeFile(join(session.sharedDir, 'messages.json'), json);
350
- }
351
- }
352
- }
353
-
354
- /**
355
- * 归档旧消息到分片文件(logrotate 风格)
356
- * messages.json = 当前活跃分片(最新消息)
357
- * messages.1.json = 最近归档,messages.2.json = 更早归档 ...
358
- */
359
- async function rotateMessages(session, cleaned) {
360
- session._rotating = true;
361
- try {
362
- // 找到分割点:优先在 turn 边界(route/system 消息)分割,约归档前半部分
363
- const halfLen = Math.floor(cleaned.length / 2);
364
- let splitIdx = halfLen;
365
- // 从 halfLen 附近向前搜索 turn 边界
366
- for (let i = halfLen; i > Math.max(0, halfLen - 20); i--) {
367
- if (cleaned[i].type === 'route' || cleaned[i].type === 'system') {
368
- splitIdx = i + 1; // 在边界消息之后分割
369
- break;
370
- }
371
- }
372
- // 如果向前没找到,向后搜索
373
- if (splitIdx === halfLen) {
374
- for (let i = halfLen + 1; i < Math.min(cleaned.length - 1, halfLen + 20); i++) {
375
- if (cleaned[i].type === 'route' || cleaned[i].type === 'system') {
376
- splitIdx = i + 1;
377
- break;
378
- }
379
- }
380
- }
381
- // 确保至少归档 1 条且保留 1 条
382
- splitIdx = Math.max(1, Math.min(splitIdx, cleaned.length - 1));
383
-
384
- const archivePart = cleaned.slice(0, splitIdx);
385
- const remainPart = cleaned.slice(splitIdx);
386
-
387
- // 将现有归档文件编号 +1(从最大编号开始,避免覆盖)
388
- const maxShard = await getMaxShardIndex(session.sharedDir);
389
- for (let i = maxShard; i >= 1; i--) {
390
- const src = join(session.sharedDir, `messages.${i}.json`);
391
- const dst = join(session.sharedDir, `messages.${i + 1}.json`);
392
- await fs.rename(src, dst).catch(() => {});
393
- }
394
-
395
- // 写入归档分片
396
- await fs.writeFile(join(session.sharedDir, 'messages.1.json'), JSON.stringify(archivePart));
397
- // 重写当前活跃文件
398
- await fs.writeFile(join(session.sharedDir, 'messages.json'), JSON.stringify(remainPart));
399
- // 同步内存中的 uiMessages
400
- session.uiMessages = remainPart.map(m => ({ ...m }));
401
-
402
- console.log(`[Crew] Rotated messages: archived ${archivePart.length} msgs to shard 1, kept ${remainPart.length} in active`);
403
- } finally {
404
- session._rotating = false;
405
- }
406
- }
407
-
408
- /**
409
- * 获取当前最大分片编号
410
- */
411
- async function getMaxShardIndex(sharedDir) {
412
- let max = 0;
413
- try {
414
- const files = await fs.readdir(sharedDir);
415
- for (const f of files) {
416
- const match = f.match(/^messages\.(\d+)\.json$/);
417
- if (match) {
418
- const idx = parseInt(match[1], 10);
419
- if (idx > max) max = idx;
420
- }
421
- }
422
- } catch { /* dir may not exist */ }
423
- return max;
424
- }
425
-
426
- /**
427
- * 删除所有消息分片文件(messages.1.json, messages.2.json, ...)
428
- */
429
- async function cleanupMessageShards(sharedDir) {
430
- try {
431
- const files = await fs.readdir(sharedDir);
432
- for (const f of files) {
433
- if (/^messages\.\d+\.json$/.test(f)) {
434
- await fs.unlink(join(sharedDir, f)).catch(() => {});
435
- }
436
- }
437
- } catch { /* dir may not exist */ }
438
- }
439
-
440
- async function loadSessionMeta(sharedDir) {
441
- try { return JSON.parse(await fs.readFile(join(sharedDir, 'session.json'), 'utf-8')); }
442
- catch { return null; }
443
- }
444
-
445
- async function loadSessionMessages(sharedDir) {
446
- let messages = [];
447
- try { messages = JSON.parse(await fs.readFile(join(sharedDir, 'messages.json'), 'utf-8')); }
448
- catch { /* file may not exist */ }
449
- // Check if older shards exist
450
- let hasOlderMessages = false;
451
- try {
452
- await fs.access(join(sharedDir, 'messages.1.json'));
453
- hasOlderMessages = true;
454
- } catch { /* no older shards */ }
455
- return { messages, hasOlderMessages };
456
- }
457
-
458
- /**
459
- * 加载历史消息分片
460
- * 前端上滑到顶部时按需请求
461
- */
462
- export async function handleLoadCrewHistory(msg) {
463
- const { sessionId, requestId } = msg;
464
- // Validate shardIndex: must be a positive integer to prevent path traversal
465
- const shardIndex = parseInt(msg.shardIndex, 10);
466
- if (!Number.isFinite(shardIndex) || shardIndex < 1) {
467
- sendCrewMessage({
468
- type: 'crew_history_loaded',
469
- sessionId,
470
- shardIndex: msg.shardIndex,
471
- requestId,
472
- messages: [],
473
- hasMore: false
474
- });
475
- return;
476
- }
477
- const session = crewSessions.get(sessionId);
478
- if (!session) {
479
- sendCrewMessage({
480
- type: 'crew_history_loaded',
481
- sessionId,
482
- shardIndex,
483
- requestId,
484
- messages: [],
485
- hasMore: false
486
- });
487
- return;
488
- }
489
-
490
- const shardPath = join(session.sharedDir, `messages.${shardIndex}.json`);
491
- let messages = [];
492
- try {
493
- messages = JSON.parse(await fs.readFile(shardPath, 'utf-8'));
494
- } catch { /* shard file doesn't exist */ }
495
-
496
- // Check if there's an even older shard
497
- const hasMore = shardIndex < await getMaxShardIndex(session.sharedDir);
498
-
499
- sendCrewMessage({
500
- type: 'crew_history_loaded',
501
- sessionId,
502
- shardIndex,
503
- requestId,
504
- messages,
505
- hasMore
506
- });
507
- }
508
-
509
- // =====================================================================
510
- // List & Resume Crew Sessions
511
- // =====================================================================
512
-
513
- /**
514
- * 列出所有 crew sessions(从索引文件 + 活跃 sessions 合并)
515
- */
516
- export async function handleListCrewSessions(msg) {
517
- const { requestId, _requestClientId } = msg;
518
- const index = await loadCrewIndex();
519
-
520
- // 按 agentId 过滤(兼容旧数据:无 agentId 的 session 在所有 agent 中显示)
521
- const agentId = ctx.CONFIG?.agentName || null;
522
- const filtered = agentId
523
- ? index.filter(e => !e.agentId || e.agentId === agentId)
524
- : index;
525
-
526
- // 用活跃 session 更新实时状态
527
- for (const entry of filtered) {
528
- const active = crewSessions.get(entry.sessionId);
529
- if (active) {
530
- entry.status = active.status;
531
- }
532
- }
533
-
534
- ctx.sendToServer({
535
- type: 'crew_sessions_list',
536
- requestId,
537
- _requestClientId,
538
- sessions: filtered
539
- });
540
- }
541
-
542
- /**
543
- * 验证 projectDir 路径安全性:必须是绝对路径且不包含路径遍历
544
- */
545
- function isValidProjectDir(dir) {
546
- if (!dir || typeof dir !== 'string') return false;
547
- if (!isAbsolute(dir)) return false;
548
- if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(dir)) return false;
549
- return true;
550
- }
551
-
552
- /**
553
- * 检查工作目录下是否存在 .crew 目录
554
- */
555
- export async function handleCheckCrewExists(msg) {
556
- const { projectDir, requestId, _requestClientId } = msg;
557
- if (!projectDir || !isValidProjectDir(projectDir)) {
558
- ctx.sendToServer({
559
- type: 'crew_exists_result',
560
- requestId,
561
- _requestClientId,
562
- exists: false,
563
- error: 'projectDir is required'
564
- });
565
- return;
566
- }
567
-
568
- const crewDir = join(projectDir, '.crew');
569
- try {
570
- const stat = await fs.stat(crewDir);
571
- if (stat.isDirectory()) {
572
- // 尝试读取 session.json 获取 session 信息
573
- let sessionInfo = null;
574
- try {
575
- const sessionPath = join(crewDir, 'session.json');
576
- const data = await fs.readFile(sessionPath, 'utf-8');
577
- sessionInfo = JSON.parse(data);
578
- } catch {
579
- // session.json 可能不存在,不影响
580
- }
581
- ctx.sendToServer({
582
- type: 'crew_exists_result',
583
- requestId,
584
- _requestClientId,
585
- exists: true,
586
- projectDir,
587
- sessionInfo
588
- });
589
- } else {
590
- ctx.sendToServer({
591
- type: 'crew_exists_result',
592
- requestId,
593
- _requestClientId,
594
- exists: false,
595
- projectDir
596
- });
597
- }
598
- } catch {
599
- ctx.sendToServer({
600
- type: 'crew_exists_result',
601
- requestId,
602
- _requestClientId,
603
- exists: false,
604
- projectDir
605
- });
606
- }
607
- }
608
-
609
- /**
610
- * 删除工作目录下的 .crew 目录
611
- */
612
- export async function handleDeleteCrewDir(msg) {
613
- const { projectDir, _requestClientId } = msg;
614
- if (!isValidProjectDir(projectDir)) return;
615
- const crewDir = join(projectDir, '.crew');
616
- try {
617
- await fs.rm(crewDir, { recursive: true, force: true });
618
- } catch {
619
- // ignore errors (dir may not exist)
620
- }
621
- }
622
-
623
- /**
624
- * 恢复已停止的 crew session
625
- */
626
- export async function resumeCrewSession(msg) {
627
- const { sessionId, userId, username } = msg;
628
-
629
- // 如果已经在活跃 sessions 中,重新发送完整信息让前端重建
630
- if (crewSessions.has(sessionId)) {
631
- const session = crewSessions.get(sessionId);
632
- const roles = Array.from(session.roles.values());
633
- // 如果内存中没有 uiMessages,尝试从磁盘加载
634
- if ((!session.uiMessages || session.uiMessages.length === 0) && session.sharedDir) {
635
- const loaded = await loadSessionMessages(session.sharedDir);
636
- session.uiMessages = loaded.messages;
637
- }
638
- // 发送前清理 _streaming 标记(跟磁盘保存逻辑保持一致)
639
- const cleanedMessages = (session.uiMessages || []).map(m => {
640
- const { _streaming, ...rest } = m;
641
- return rest;
642
- });
643
- // 检查是否有历史分片
644
- const hasOlderMessages = await getMaxShardIndex(session.sharedDir) > 0;
645
-
646
- sendCrewMessage({
647
- type: 'crew_session_restored',
648
- sessionId,
649
- projectDir: session.projectDir,
650
- sharedDir: session.sharedDir,
651
- name: session.name || '',
652
- roles: roles.map(r => ({
653
- name: r.name, displayName: r.displayName, icon: r.icon,
654
- description: r.description, isDecisionMaker: r.isDecisionMaker || false,
655
- groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
656
- })),
657
- decisionMaker: session.decisionMaker,
658
- userId: session.userId,
659
- username: session.username,
660
- uiMessages: cleanedMessages,
661
- hasOlderMessages
662
- });
663
- sendStatusUpdate(session);
664
- return;
665
- }
666
-
667
- // 从索引获取 sharedDir
668
- const index = await loadCrewIndex();
669
- const indexEntry = index.find(e => e.sessionId === sessionId);
670
- if (!indexEntry) {
671
- sendCrewMessage({ type: 'error', sessionId, message: 'Crew session not found in index' });
672
- return;
673
- }
674
-
675
- // 从 session.json 加载详细元数据
676
- const meta = await loadSessionMeta(indexEntry.sharedDir);
677
- if (!meta) {
678
- sendCrewMessage({ type: 'error', sessionId, message: 'Crew session metadata not found' });
679
- return;
680
- }
681
-
682
- // 重建 session(跳过 initSharedDir,目录已存在)
683
- const roles = meta.roles || [];
684
- const decisionMaker = meta.decisionMaker || roles[0]?.name || null;
685
- const session = {
686
- id: sessionId,
687
- projectDir: meta.projectDir,
688
- sharedDir: meta.sharedDir || indexEntry.sharedDir,
689
- name: meta.name || '',
690
- roles: new Map(roles.map(r => [r.name, r])),
691
- roleStates: new Map(),
692
- decisionMaker,
693
- status: 'waiting_human',
694
- round: meta.round || 0,
695
- costUsd: meta.costUsd || 0,
696
- totalInputTokens: meta.totalInputTokens || 0,
697
- totalOutputTokens: meta.totalOutputTokens || 0,
698
- messageHistory: [],
699
- uiMessages: [], // will be loaded from messages.json
700
- humanMessageQueue: [],
701
- waitingHumanContext: null,
702
- pendingRoutes: [],
703
- features: new Map((meta.features || []).map(f => [f.taskId, f])),
704
- _completedTaskIds: new Set(meta._completedTaskIds || []),
705
- userId: userId || meta.userId,
706
- username: username || meta.username,
707
- agentId: meta.agentId || ctx.CONFIG?.agentName || null,
708
- teamType: meta.teamType || 'dev',
709
- language: meta.language || 'zh-CN',
710
- createdAt: meta.createdAt || Date.now()
711
- };
712
- crewSessions.set(sessionId, session);
713
-
714
- // 加载 UI 消息历史(仅最新分片)
715
- const loaded = await loadSessionMessages(session.sharedDir);
716
- session.uiMessages = loaded.messages;
717
-
718
- // 通知 server
719
- sendCrewMessage({
720
- type: 'crew_session_restored',
721
- sessionId,
722
- projectDir: session.projectDir,
723
- sharedDir: session.sharedDir,
724
- name: session.name || '',
725
- roles: roles.map(r => ({
726
- name: r.name, displayName: r.displayName, icon: r.icon,
727
- description: r.description, isDecisionMaker: r.isDecisionMaker || false,
728
- groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
729
- })),
730
- decisionMaker,
731
- userId: session.userId,
732
- username: session.username,
733
- uiMessages: session.uiMessages,
734
- hasOlderMessages: loaded.hasOlderMessages
735
- });
736
- sendStatusUpdate(session);
737
-
738
- // 更新索引和 session.json
739
- await upsertCrewIndex(session);
740
- await saveSessionMeta(session);
741
-
742
- console.log(`[Crew] Session ${sessionId} resumed, waiting for human input`);
743
- }
744
-
745
- /**
746
- * 查找指定 projectDir 的已有 crew session(内存活跃 > 磁盘索引)
747
- */
748
- async function findExistingSessionByProjectDir(projectDir) {
749
- const normalizedDir = projectDir.replace(/\/+$/, '');
750
-
751
- for (const [, session] of crewSessions) {
752
- if (session.projectDir.replace(/\/+$/, '') === normalizedDir
753
- && session.status !== 'completed') {
754
- return { sessionId: session.id, source: 'active' };
755
- }
756
- }
757
-
758
- const index = await loadCrewIndex();
759
- const agentId = ctx.CONFIG?.agentName || null;
760
- const match = index.find(e =>
761
- e.projectDir.replace(/\/+$/, '') === normalizedDir
762
- && (!agentId || !e.agentId || e.agentId === agentId)
763
- && e.status !== 'completed'
764
- );
765
-
766
- if (match) {
767
- const meta = await loadSessionMeta(match.sharedDir);
768
- if (meta) return { sessionId: match.sessionId, source: 'index' };
769
- await removeFromCrewIndex(match.sessionId);
770
- }
771
-
772
- return null;
773
- }
774
-
775
- // =====================================================================
776
- // Session Lifecycle
777
- // =====================================================================
778
-
779
- /**
780
- * 创建 Crew Session
781
- * 支持带角色创建或空 session(后续动态添加角色)
782
- */
783
- export async function createCrewSession(msg) {
784
- const {
785
- sessionId,
786
- projectDir,
787
- sharedDir: sharedDirRel,
788
- name,
789
- roles: rawRoles = [], // [{ name, displayName, icon, description, claudeMd, model, budget, isDecisionMaker, count }]
790
- teamType = 'dev',
791
- language = 'zh-CN',
792
- userId,
793
- username
794
- } = msg;
795
-
796
- // 同目录检查:如果 projectDir 已有活跃或可恢复的 session,自动 resume
797
- const existingSession = await findExistingSessionByProjectDir(projectDir);
798
- if (existingSession) {
799
- console.log(`[Crew] Found existing session for ${projectDir}: ${existingSession.sessionId}, auto-resuming`);
800
- await resumeCrewSession({ sessionId: existingSession.sessionId, userId, username });
801
- return;
802
- }
803
-
804
- // 展开多实例角色(count > 1 的执行者角色)
805
- const roles = expandRoles(rawRoles);
806
-
807
- // 解析共享目录(相对路径相对于 projectDir)
808
- const sharedDir = sharedDirRel?.startsWith('/')
809
- ? sharedDirRel
810
- : join(projectDir, sharedDirRel || '.crew');
811
-
812
- // 找到决策者
813
- const decisionMaker = roles.find(r => r.isDecisionMaker)?.name || roles[0]?.name || null;
814
-
815
- // ★ 阶段1:立即构建 session 并通知前端,让 UI 先显示
816
- const session = {
817
- id: sessionId,
818
- projectDir,
819
- sharedDir,
820
- name: name || '',
821
- roles: new Map(roles.map(r => [r.name, r])),
822
- roleStates: new Map(),
823
- decisionMaker,
824
- status: 'initializing', // ← 新增初始化状态
825
- round: 0,
826
- costUsd: 0,
827
- totalInputTokens: 0,
828
- totalOutputTokens: 0,
829
- messageHistory: [], // 群聊消息历史
830
- uiMessages: [], // 精简的 UI 消息历史(用于恢复时重放)
831
- humanMessageQueue: [], // 人的消息排队
832
- waitingHumanContext: null, // { fromRole, reason, message }
833
- pendingRoutes: [], // [{ fromRole, route }] — 暂停时未完成的路由
834
- features: new Map(), // taskId → { taskId, taskTitle, createdAt } — 持久化 feature 列表
835
- _completedTaskIds: new Set(), // 已完成的 taskId 集合(用于检测新完成的任务)
836
- initProgress: null, // 'roles' | 'worktrees' | null — 初始化阶段
837
- userId,
838
- username,
839
- agentId: ctx.CONFIG?.agentName || null,
840
- teamType,
841
- language,
842
- createdAt: Date.now()
843
- };
844
-
845
- crewSessions.set(sessionId, session);
846
-
847
- // 立即通知前端:session 已创建,可以显示 UI
848
- sendCrewMessage({
849
- type: 'crew_session_created',
850
- sessionId,
851
- projectDir,
852
- sharedDir,
853
- name: name || '',
854
- roles: roles.map(r => ({
855
- name: r.name,
856
- displayName: r.displayName,
857
- icon: r.icon,
858
- description: r.description,
859
- isDecisionMaker: r.isDecisionMaker || false,
860
- model: r.model,
861
- roleType: r.roleType,
862
- groupIndex: r.groupIndex
863
- })),
864
- decisionMaker,
865
- userId,
866
- username
867
- });
868
-
869
- sendStatusUpdate(session);
870
-
871
- // ★ 阶段2:异步完成文件系统和 worktree 初始化
872
- try {
873
- // 初始化共享区(角色目录 + CLAUDE.md)
874
- session.initProgress = 'roles';
875
- sendStatusUpdate(session);
876
- await initSharedDir(sharedDir, roles, projectDir, language);
877
-
878
- // 初始化 git worktrees
879
- const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
880
- if (groupIndices.length > 0) {
881
- session.initProgress = 'worktrees';
882
- sendStatusUpdate(session);
883
- }
884
- const worktreeMap = await initWorktrees(projectDir, roles);
885
-
886
- // 回填 workDir
887
- for (const role of roles) {
888
- if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex)) {
889
- role.workDir = worktreeMap.get(role.groupIndex);
890
- await writeRoleClaudeMd(sharedDir, role, language);
891
- }
892
- }
893
-
894
- // 持久化
895
- await upsertCrewIndex(session);
896
- await saveSessionMeta(session);
897
-
898
- // 初始化完成,仅在 initializing 状态下切换到 running(避免覆盖用户手动暂停/停止)
899
- if (session.status === 'initializing') {
900
- session.status = 'running';
901
- }
902
- session.initProgress = null;
903
- sendStatusUpdate(session);
904
- } catch (e) {
905
- console.error('[Crew] Session initialization failed:', e);
906
- if (session.status === 'initializing') {
907
- session.status = 'running';
908
- }
909
- session.initProgress = null;
910
- sendStatusUpdate(session);
911
- sendCrewMessage({
912
- type: 'crew_output',
913
- sessionId,
914
- roleName: 'system',
915
- roleIcon: 'S',
916
- roleDisplayName: '系统',
917
- content: `工作环境初始化失败: ${e.message}`,
918
- isTurnEnd: true
919
- });
920
- }
921
-
922
- return session;
923
- }
924
-
925
- // =====================================================================
926
- // Dynamic Role Management
927
- // =====================================================================
928
-
929
- /**
930
- * 向现有 session 动态添加角色
931
- */
932
- export async function addRoleToSession(msg) {
933
- const { sessionId, role } = msg;
934
- const session = crewSessions.get(sessionId);
935
- if (!session) {
936
- console.warn(`[Crew] Session not found: ${sessionId}`);
937
- return;
938
- }
939
-
940
- // 展开多实例(count > 1 时)
941
- const rolesToAdd = expandRoles([role]);
942
-
943
- for (const r of rolesToAdd) {
944
- if (session.roles.has(r.name)) {
945
- console.warn(`[Crew] Role already exists: ${r.name}`);
946
- continue;
947
- }
948
-
949
- // 添加角色到 session
950
- session.roles.set(r.name, r);
951
-
952
- // 如果还没有决策者且新角色是决策者,更新
953
- if (r.isDecisionMaker) {
954
- session.decisionMaker = r.name;
955
- }
956
- // 如果没有任何决策者,第一个角色作为决策者
957
- if (!session.decisionMaker) {
958
- session.decisionMaker = r.name;
959
- }
960
-
961
- // 初始化角色目录(CLAUDE.md + memory.md)
962
- await initRoleDir(session.sharedDir, r, session.language || 'zh-CN');
963
-
964
- console.log(`[Crew] Role added: ${r.name} (${r.displayName}) to session ${sessionId}`);
965
-
966
- // 通知 Web 端
967
- sendCrewMessage({
968
- type: 'crew_role_added',
969
- sessionId,
970
- role: {
971
- name: r.name,
972
- displayName: r.displayName,
973
- icon: r.icon,
974
- description: r.description,
975
- isDecisionMaker: r.isDecisionMaker || false,
976
- model: r.model,
977
- roleType: r.roleType,
978
- groupIndex: r.groupIndex
979
- },
980
- decisionMaker: session.decisionMaker
981
- });
982
-
983
- // 发送系统消息
984
- sendCrewOutput(session, 'system', 'system', {
985
- type: 'assistant',
986
- message: { role: 'assistant', content: [{ type: 'text', text: `${roleLabel(r)} 加入了群聊` }] }
987
- });
988
- }
989
-
990
- // 更新共享 CLAUDE.md(增量添加新角色信息)
991
- await updateSharedClaudeMd(session);
992
-
993
- sendStatusUpdate(session);
994
- }
995
-
996
- /**
997
- * 从 session 移除角色
998
- */
999
- export async function removeRoleFromSession(msg) {
1000
- const { sessionId, roleName } = msg;
1001
- const session = crewSessions.get(sessionId);
1002
- if (!session) {
1003
- console.warn(`[Crew] Session not found: ${sessionId}`);
1004
- return;
1005
- }
1006
-
1007
- const role = session.roles.get(roleName);
1008
- if (!role) {
1009
- console.warn(`[Crew] Role not found: ${roleName}`);
1010
- return;
1011
- }
1012
-
1013
- // 停止角色的 query(如果正在运行)
1014
- const roleState = session.roleStates.get(roleName);
1015
- if (roleState) {
1016
- // 保存 sessionId 到文件(以便未来恢复)
1017
- if (roleState.claudeSessionId) {
1018
- await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId);
1019
- }
1020
- if (roleState.abortController) {
1021
- roleState.abortController.abort();
1022
- }
1023
- session.roleStates.delete(roleName);
1024
- }
1025
-
1026
- // 从 roles 中移除
1027
- session.roles.delete(roleName);
1028
-
1029
- // 如果移除的是决策者,重新选择
1030
- if (session.decisionMaker === roleName) {
1031
- const remaining = Array.from(session.roles.values());
1032
- const newDM = remaining.find(r => r.isDecisionMaker) || remaining[0];
1033
- session.decisionMaker = newDM?.name || null;
1034
- }
1035
-
1036
- // 更新 CLAUDE.md
1037
- await updateSharedClaudeMd(session);
1038
-
1039
- // Memory 文件保留(不删除,角色可能重新加入)
1040
-
1041
- console.log(`[Crew] Role removed: ${roleName} from session ${sessionId}`);
1042
-
1043
- sendCrewMessage({
1044
- type: 'crew_role_removed',
1045
- sessionId,
1046
- roleName,
1047
- decisionMaker: session.decisionMaker
1048
- });
1049
-
1050
- sendCrewOutput(session, 'system', 'system', {
1051
- type: 'assistant',
1052
- message: { role: 'assistant', content: [{ type: 'text', text: `${roleLabel(role)} 离开了群聊` }] }
1053
- });
1054
-
1055
- sendStatusUpdate(session);
1056
- }
1057
-
1058
- /**
1059
- * 更新 crew session 的 name
1060
- */
1061
- export async function handleUpdateCrewSession(msg) {
1062
- const { sessionId, name } = msg;
1063
- const session = crewSessions.get(sessionId);
1064
- if (!session) {
1065
- console.warn(`[Crew] Session not found for update: ${sessionId}`);
1066
- return;
1067
- }
1068
- if (name !== undefined) session.name = name;
1069
- await saveSessionMeta(session);
1070
- await upsertCrewIndex(session);
1071
- }
1072
-
1073
- // =====================================================================
1074
- // Shared Directory & Memory
1075
- // =====================================================================
1076
-
1077
- /**
1078
- * 初始化共享目录
1079
- * 结构:
1080
- * .crew/
1081
- * ├── CLAUDE.md ← 共享级(团队目标、成员、共享记忆)
1082
- * ├── context/ ← 文档产出
1083
- * ├── sessions/ ← sessionId 持久化
1084
- * └── roles/
1085
- * └── {roleName}/
1086
- * └── CLAUDE.md ← 角色定义 + 个人记忆
1087
- */
1088
- async function initSharedDir(sharedDir, roles, projectDir, language = 'zh-CN') {
1089
- await fs.mkdir(sharedDir, { recursive: true });
1090
- await fs.mkdir(join(sharedDir, 'context'), { recursive: true });
1091
- await fs.mkdir(join(sharedDir, 'sessions'), { recursive: true });
1092
- await fs.mkdir(join(sharedDir, 'roles'), { recursive: true });
1093
-
1094
- // 初始化每个角色的目录
1095
- for (const role of roles) {
1096
- await initRoleDir(sharedDir, role, language);
1097
- }
1098
-
1099
- // 生成 .crew/CLAUDE.md(共享级)
1100
- await writeSharedClaudeMd(sharedDir, roles, projectDir, language);
1101
- }
1102
-
1103
- /**
1104
- * 初始化角色目录: .crew/roles/{roleName}/CLAUDE.md
1105
- */
1106
- async function initRoleDir(sharedDir, role, language = 'zh-CN') {
1107
- const roleDir = join(sharedDir, 'roles', role.name);
1108
- await fs.mkdir(roleDir, { recursive: true });
1109
-
1110
- // 角色 CLAUDE.md(仅首次创建,后续角色自己维护记忆内容)
1111
- const claudeMdPath = join(roleDir, 'CLAUDE.md');
1112
- try {
1113
- await fs.access(claudeMdPath);
1114
- // 已存在,不覆盖(保留角色自己写入的记忆)
1115
- } catch {
1116
- await writeRoleClaudeMd(sharedDir, role, language);
1117
- }
1118
- }
1119
-
1120
- /**
1121
- * 写入 .crew/CLAUDE.md — 共享级(所有角色自动继承)
1122
- * 记忆直接写在 CLAUDE.md 中,Claude Code 会自动加载
1123
- */
1124
- async function writeSharedClaudeMd(sharedDir, roles, projectDir, language = 'zh-CN') {
1125
- const m = getMessages(language);
1126
-
1127
- const claudeMd = `${m.projectGoal}
1128
-
1129
- ${m.projectCodePath}
1130
- ${projectDir}
1131
- ${m.useAbsolutePath}
1132
-
1133
- ${m.teamMembersTitle}
1134
- ${roles.length > 0 ? roles.map(r => `- ${roleLabel(r)}(${r.name}): ${r.description}${r.isDecisionMaker ? ` (${m.decisionMakerTag})` : ''}`).join('\n') : m.noMembers}
1135
-
1136
- ${m.workConventions}
1137
- ${m.workConventionsContent}
1138
-
1139
- ${m.stuckRules}
1140
- ${m.stuckRulesContent}
1141
-
1142
- ${m.worktreeRules}
1143
- ${m.worktreeRulesContent}
1144
-
1145
- ${m.featureRecordShared}
1146
-
1147
- ${m.sharedMemoryTitle}
1148
- ${m.sharedMemoryDefault}
1149
- `;
1150
-
1151
- await fs.writeFile(join(sharedDir, 'CLAUDE.md'), claudeMd);
1152
- }
1153
-
1154
- /**
1155
- * 写入 .crew/roles/{roleName}/CLAUDE.md — 角色级
1156
- * 记忆直接追加在此文件中,Claude Code 自动加载
1157
- */
1158
- async function writeRoleClaudeMd(sharedDir, role, language = 'zh-CN') {
1159
- const roleDir = join(sharedDir, 'roles', role.name);
1160
- const m = getMessages(language);
1161
-
1162
- let claudeMd = `${m.roleTitle(roleLabel(role))}
1163
- ${role.claudeMd || role.description}
1164
- `;
1165
-
1166
- // 有独立 worktree 的角色,覆盖代码工作目录
1167
- if (role.workDir) {
1168
- claudeMd += `
1169
- ${m.codeWorkDir}
1170
- ${role.workDir}
1171
- ${m.codeWorkDirNote}
1172
- `;
1173
- }
1174
-
1175
- claudeMd += `
1176
- ${m.personalMemory}
1177
- ${m.personalMemoryDefault}
1178
- `;
1179
-
1180
- await fs.writeFile(join(roleDir, 'CLAUDE.md'), claudeMd);
1181
- }
1182
-
1183
- /**
1184
- * 角色变动时更新 .crew/CLAUDE.md
1185
- */
1186
- async function updateSharedClaudeMd(session) {
1187
- const roles = Array.from(session.roles.values());
1188
- await writeSharedClaudeMd(session.sharedDir, roles, session.projectDir, session.language || 'zh-CN');
1189
- }
1190
-
1191
- // =====================================================================
1192
- // Task File Management (auto-managed by system)
1193
- // =====================================================================
1194
-
1195
- /**
1196
- * 自动创建 task 进度文件
1197
- * 当 ROUTE 带有 taskId + taskTitle 时,如果文件不存在则自动创建
1198
- */
1199
- async function ensureTaskFile(session, taskId, taskTitle, assignee, summary) {
1200
- const featuresDir = join(session.sharedDir, 'context', 'features');
1201
- const filePath = join(featuresDir, `${taskId}.md`);
1202
-
1203
- try {
1204
- await fs.access(filePath);
1205
- // 文件已存在,不覆盖
1206
- return;
1207
- } catch {
1208
- // 文件不存在,创建
1209
- }
1210
-
1211
- await fs.mkdir(featuresDir, { recursive: true });
1212
-
1213
- const m = getMessages(session.language || 'zh-CN');
1214
- const now = new Date().toISOString();
1215
- const content = `# ${m.featureLabel}: ${taskTitle}
1216
- - task-id: ${taskId}
1217
- - ${m.statusPending}
1218
- - ${m.assigneeLabel}: ${assignee}
1219
- - ${m.createdAtLabel}: ${now}
1220
-
1221
- ${m.requirementDesc}
1222
- ${summary}
1223
-
1224
- ${m.workRecord}
1225
- `;
1226
-
1227
- await fs.writeFile(filePath, content);
1228
-
1229
- // 同步到 session.features
1230
- if (!session.features.has(taskId)) {
1231
- session.features.set(taskId, { taskId, taskTitle, createdAt: Date.now() });
1232
- }
1233
-
1234
- console.log(`[Crew] Task file created: ${taskId} (${taskTitle})`);
1235
-
1236
- // 更新 feature 索引
1237
- updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
1238
- }
1239
-
1240
- /**
1241
- * 追加工作记录到 task 文件
1242
- * 当角色 ROUTE 时,自动将 summary 追加到对应 task 文件
1243
- */
1244
- async function appendTaskRecord(session, taskId, roleName, summary) {
1245
- const filePath = join(session.sharedDir, 'context', 'features', `${taskId}.md`);
1246
-
1247
- try {
1248
- await fs.access(filePath);
1249
- } catch {
1250
- // 文件不存在,跳过(不应该发生,但防御性处理)
1251
- return;
1252
- }
1253
-
1254
- const role = session.roles.get(roleName);
1255
- const label = role ? roleLabel(role) : roleName;
1256
- const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
1257
- const record = `\n### ${label} - ${now}\n${summary}\n`;
1258
-
1259
- await fs.appendFile(filePath, record);
1260
- console.log(`[Crew] Task record appended: ${taskId} by ${roleName}`);
1261
- }
1262
-
1263
- /**
1264
- * 读取 task 文件内容(用于注入上下文)
1265
- */
1266
- async function readTaskFile(session, taskId) {
1267
- const filePath = join(session.sharedDir, 'context', 'features', `${taskId}.md`);
1268
- try {
1269
- return await fs.readFile(filePath, 'utf-8');
1270
- } catch {
1271
- return null;
1272
- }
1273
- }
1274
-
1275
- /**
1276
- * 从 TASKS block 文本中提取已完成任务的 taskId 集合
1277
- */
1278
- function parseCompletedTasks(text) {
1279
- const ids = new Set();
1280
- const match = text.match(/---TASKS---([\s\S]*?)---END_TASKS---/);
1281
- if (!match) return ids;
1282
- for (const line of match[1].split('\n')) {
1283
- const m = line.match(/^-\s*\[[xX]\]\s*.+#(\S+)/);
1284
- if (m) ids.add(m[1]);
1285
- }
1286
- return ids;
1287
- }
1288
-
1289
- /**
1290
- * 更新 feature 索引文件 context/features/index.md
1291
- * 全量重建:根据 session.features 和 session._completedTaskIds 生成分类表格
1292
- */
1293
- async function updateFeatureIndex(session) {
1294
- const featuresDir = join(session.sharedDir, 'context', 'features');
1295
- await fs.mkdir(featuresDir, { recursive: true });
1296
-
1297
- const m = getMessages(session.language || 'zh-CN');
1298
- const completed = session._completedTaskIds || new Set();
1299
- const allFeatures = Array.from(session.features.values());
1300
-
1301
- // 按创建时间排序
1302
- allFeatures.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
1303
-
1304
- const inProgress = allFeatures.filter(f => !completed.has(f.taskId));
1305
- const done = allFeatures.filter(f => completed.has(f.taskId));
1306
-
1307
- const locale = (session.language === 'en') ? 'en-US' : 'zh-CN';
1308
- const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
1309
- let content = `${m.featureIndex}\n> ${m.lastUpdated}: ${now}\n`;
1310
-
1311
- content += `\n${m.inProgressGroup(inProgress.length)}\n`;
1312
- if (inProgress.length > 0) {
1313
- content += `| ${m.colTaskId} | ${m.colTitle} | ${m.colCreatedAt} |\n|---------|------|----------|\n`;
1314
- for (const f of inProgress) {
1315
- const date = f.createdAt ? new Date(f.createdAt).toLocaleDateString(locale) : '-';
1316
- content += `| ${f.taskId} | ${f.taskTitle} | ${date} |\n`;
1317
- }
1318
- }
1319
-
1320
- content += `\n${m.completedGroup(done.length)}\n`;
1321
- if (done.length > 0) {
1322
- content += `| ${m.colTaskId} | ${m.colTitle} | ${m.colCreatedAt} |\n|---------|------|----------|\n`;
1323
- for (const f of done) {
1324
- const date = f.createdAt ? new Date(f.createdAt).toLocaleDateString(locale) : '-';
1325
- content += `| ${f.taskId} | ${f.taskTitle} | ${date} |\n`;
1326
- }
1327
- }
1328
-
1329
- await fs.writeFile(join(featuresDir, 'index.md'), content);
1330
- console.log(`[Crew] Feature index updated: ${inProgress.length} in progress, ${done.length} completed`);
1331
- }
1332
-
1333
- /**
1334
- * 追加完成汇总到 context/changelog.md
1335
- * 从 feature 文件的工作记录中提取最后一条记录作为摘要
1336
- */
1337
- async function appendChangelog(session, taskId, taskTitle) {
1338
- const contextDir = join(session.sharedDir, 'context');
1339
- await fs.mkdir(contextDir, { recursive: true });
1340
- const changelogPath = join(contextDir, 'changelog.md');
1341
-
1342
- const m = getMessages(session.language || 'zh-CN');
1343
-
1344
- // 读取 feature 文件提取最后一条工作记录作为摘要
1345
- const taskContent = await readTaskFile(session, taskId);
1346
- let summaryText = '';
1347
- if (taskContent) {
1348
- // 提取最后一个 ### 块作为摘要
1349
- const records = taskContent.split(/\n### /);
1350
- if (records.length > 1) {
1351
- const lastRecord = records[records.length - 1];
1352
- // 取第一行之后的内容作为摘要(第一行是角色名和时间)
1353
- const lines = lastRecord.split('\n');
1354
- summaryText = lines.slice(1).join('\n').trim();
1355
- }
1356
- }
1357
- if (!summaryText) {
1358
- summaryText = m.noSummary;
1359
- }
1360
-
1361
- // 限制摘要长度
1362
- if (summaryText.length > 500) {
1363
- summaryText = summaryText.substring(0, 497) + '...';
1364
- }
1365
-
1366
- const locale = (session.language === 'en') ? 'en-US' : 'zh-CN';
1367
- const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
1368
- const entry = `\n## ${taskId}: ${taskTitle}\n- ${m.completedAt}: ${now}\n- ${m.summaryLabel}: ${summaryText}\n`;
1369
-
1370
- // 如果文件不存在,先写 header
1371
- let exists = false;
1372
- try {
1373
- await fs.access(changelogPath);
1374
- exists = true;
1375
- } catch {}
1376
-
1377
- if (!exists) {
1378
- await fs.writeFile(changelogPath, `${m.changelogTitle}\n${entry}`);
1379
- } else {
1380
- await fs.appendFile(changelogPath, entry);
1381
- }
1382
-
1383
- console.log(`[Crew] Changelog appended: ${taskId} (${taskTitle})`);
1384
- }
1385
-
1386
- // =====================================================================
1387
- // Session Persistence
1388
- // =====================================================================
1389
-
1390
- /**
1391
- * 保存角色的 claudeSessionId 到文件
1392
- */
1393
- async function saveRoleSessionId(sharedDir, roleName, claudeSessionId) {
1394
- const sessionsDir = join(sharedDir, 'sessions');
1395
- await fs.mkdir(sessionsDir, { recursive: true });
1396
- const filePath = join(sessionsDir, `${roleName}.json`);
1397
- await fs.writeFile(filePath, JSON.stringify({
1398
- claudeSessionId,
1399
- savedAt: Date.now()
1400
- }));
1401
- console.log(`[Crew] Saved sessionId for ${roleName}: ${claudeSessionId}`);
1402
- }
1403
-
1404
- /**
1405
- * 从文件加载角色的 claudeSessionId
1406
- */
1407
- async function loadRoleSessionId(sharedDir, roleName) {
1408
- const filePath = join(sharedDir, 'sessions', `${roleName}.json`);
1409
- try {
1410
- const data = JSON.parse(await fs.readFile(filePath, 'utf-8'));
1411
- return data.claudeSessionId || null;
1412
- } catch {
1413
- return null;
1414
- }
1415
- }
1416
-
1417
-
1418
- /**
1419
- * 清除角色的 savedSessionId(用于强制新建 conversation)
1420
- */
1421
- async function clearRoleSessionId(sharedDir, roleName) {
1422
- const filePath = join(sharedDir, 'sessions', `${roleName}.json`);
1423
- try {
1424
- await fs.unlink(filePath);
1425
- console.log(`[Crew] Cleared sessionId for ${roleName} (force new conversation)`);
1426
- } catch {
1427
- // 文件不存在也正常
1428
- }
1429
- }
1430
-
1431
- /**
1432
- * 判断角色错误是否可恢复
1433
- */
1434
- function classifyRoleError(error) {
1435
- const msg = error.message || '';
1436
- if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
1437
- return { recoverable: true, reason: 'context_exceeded', skipResume: true };
1438
- }
1439
- if (/compact|compress|context.*reduc/i.test(msg)) {
1440
- return { recoverable: true, reason: 'compact_failed', skipResume: true };
1441
- }
1442
- if (/rate.?limit|429|overloaded|503|502|timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) {
1443
- return { recoverable: true, reason: 'transient_api_error', skipResume: false };
1444
- }
1445
- if (/exited with code [1-9]/i.test(msg) && msg.length < 100) {
1446
- return { recoverable: true, reason: 'process_crashed', skipResume: false };
1447
- }
1448
- if (/spawn|ENOENT|not found/i.test(msg)) {
1449
- return { recoverable: false, reason: 'spawn_failed' };
1450
- }
1451
- return { recoverable: true, reason: 'unknown', skipResume: false };
1452
- }
1453
-
1454
- // =====================================================================
1455
- // Role Query Management
1456
- // =====================================================================
1457
-
1458
- /**
1459
- * 为角色创建持久 query 实例
1460
- * 支持 resume:如果角色之前有保存的 sessionId,自动恢复上下文
1461
- */
1462
- async function createRoleQuery(session, roleName) {
1463
- const role = session.roles.get(roleName);
1464
- if (!role) throw new Error(`Role not found: ${roleName}`);
1465
-
1466
- const inputStream = new Stream();
1467
- const abortController = new AbortController();
1468
-
1469
- const systemPrompt = buildRoleSystemPrompt(role, session);
1470
-
1471
- // 尝试加载之前保存的 sessionId
1472
- const savedSessionId = await loadRoleSessionId(session.sharedDir, roleName);
1473
-
1474
- // ★ cwd 设为角色目录,Claude Code 自动加载:
1475
- // 1. .crew/roles/{roleName}/CLAUDE.md(角色定义+个人记忆)
1476
- // 2. .crew/CLAUDE.md(共享目标+团队信息+共享记忆)
1477
- // 3. {projectDir}/CLAUDE.md(项目级,如果有的话)
1478
- const roleCwd = join(session.sharedDir, 'roles', roleName);
1479
-
1480
- const queryOptions = {
1481
- cwd: roleCwd,
1482
- permissionMode: 'bypassPermissions',
1483
- abort: abortController.signal,
1484
- model: role.model || undefined,
1485
- appendSystemPrompt: systemPrompt
1486
- };
1487
-
1488
- // 如果有保存的 sessionId,使用 resume 恢复上下文
1489
- if (savedSessionId) {
1490
- queryOptions.resume = savedSessionId;
1491
- console.log(`[Crew] Resuming ${roleName} with sessionId: ${savedSessionId}`);
1492
- }
1493
-
1494
- const roleQuery = query({
1495
- prompt: inputStream,
1496
- options: queryOptions
1497
- });
1498
-
1499
- const roleState = {
1500
- query: roleQuery,
1501
- inputStream,
1502
- abortController,
1503
- accumulatedText: '',
1504
- turnActive: false,
1505
- claudeSessionId: savedSessionId,
1506
- lastCostUsd: 0,
1507
- lastInputTokens: 0,
1508
- lastOutputTokens: 0,
1509
- consecutiveErrors: 0,
1510
- lastDispatchContent: null,
1511
- lastDispatchFrom: null,
1512
- lastDispatchTaskId: null,
1513
- lastDispatchTaskTitle: null,
1514
- // compact 状态
1515
- _compacting: false, // 是否正在 compact
1516
- _compactSummaryPending: false, // 是否等待过滤 compact summary
1517
- _pendingCompactRoutes: null, // compact 期间缓存的待执行路由 Array|null
1518
- _pendingDispatch: null, // compact 完成后待重派的内容 { content, from, taskId, taskTitle }
1519
- _fromRole: null // 缓存路由的来源角色
1520
- };
1521
-
1522
- session.roleStates.set(roleName, roleState);
1523
-
1524
- // 异步处理角色输出
1525
- processRoleOutput(session, roleName, roleQuery, roleState);
1526
-
1527
- return roleState;
1528
- }
1529
-
1530
- /**
1531
- * 构建角色的 system prompt(精简版)
1532
- * Memory 和工作区信息已通过 CLAUDE.md 自动加载,此处只补充路由规则
1533
- */
1534
- function buildRoleSystemPrompt(role, session) {
1535
- const allRoles = Array.from(session.roles.values());
1536
-
1537
- // 按组裁剪路由目标:
1538
- // - 有 groupIndex > 0 的执行者只看到同组成员 + 管理者(PM/architect/designer)
1539
- // - 管理者(groupIndex === 0)看到所有角色
1540
- let routeTargets;
1541
- if (role.groupIndex > 0) {
1542
- routeTargets = allRoles.filter(r =>
1543
- r.name !== role.name && (r.groupIndex === role.groupIndex || r.groupIndex === 0)
1544
- );
1545
- } else {
1546
- routeTargets = allRoles.filter(r => r.name !== role.name);
1547
- }
1548
-
1549
- const m = getMessages(session.language || 'zh-CN');
1550
-
1551
- let prompt = `${m.teamCollab}
1552
- ${m.teamCollabIntro()}
1553
-
1554
- ${m.teamMembers}
1555
- ${allRoles.map(r => `- ${roleLabel(r)}: ${r.description}${r.isDecisionMaker ? ` (${m.decisionMakerTag})` : ''}`).join('\n')}`;
1556
-
1557
- const hasMultiInstance = allRoles.some(r => r.groupIndex > 0);
1558
-
1559
- if (routeTargets.length > 0) {
1560
- prompt += `\n\n${m.routingRules}
1561
- ${m.routingIntro}
1562
-
1563
- \`\`\`
1564
- ---ROUTE---
1565
- to: <roleName>
1566
- summary: <brief description>
1567
- ---END_ROUTE---
1568
- \`\`\`
1569
-
1570
- ${m.routeTargets}
1571
- ${routeTargets.map(r => `- ${r.name}: ${roleLabel(r)} — ${r.description}`).join('\n')}
1572
- - human: ${m.humanTarget}
1573
-
1574
- ${m.routeNotes(session.decisionMaker)}`;
1575
- }
1576
-
1577
- // 决策者额外 prompt
1578
- if (role.isDecisionMaker) {
1579
- const isDevTeam = session.teamType === 'dev';
1580
-
1581
- prompt += `\n\n${m.toolUsage}
1582
- ${m.toolUsageContent(isDevTeam)}`;
1583
-
1584
- prompt += `\n\n${m.dmRole}
1585
- ${m.dmRoleContent}`;
1586
-
1587
- if (isDevTeam) {
1588
- prompt += m.dmDevExtra;
1589
- }
1590
-
1591
- // 非开发团队:注入讨论模式 prompt
1592
- if (!isDevTeam) {
1593
- prompt += `\n\n${m.collabMode}
1594
- ${m.collabModeContent}`;
1595
- }
1596
-
1597
- // 多实例模式(仅开发团队使用):注入开发组状态和调度规则
1598
- if (isDevTeam && hasMultiInstance) {
1599
- // 构建开发组实时状态
1600
- const maxGroup = Math.max(...allRoles.map(r => r.groupIndex));
1601
- const groupLines = [];
1602
- for (let g = 1; g <= maxGroup; g++) {
1603
- const members = allRoles.filter(r => r.groupIndex === g);
1604
- const memberStrs = members.map(r => {
1605
- const state = session.roleStates.get(r.name);
1606
- const busy = state?.turnActive;
1607
- const task = state?.currentTask;
1608
- if (busy && task) return `${r.name}(${m.groupBusy(task.taskId + ' ' + task.taskTitle)})`;
1609
- if (busy) return `${r.name}(${m.groupBusyShort})`;
1610
- return `${r.name}(${m.groupIdle})`;
1611
- });
1612
- groupLines.push(`${m.groupLabel(g)}: ${memberStrs.join(' ')}`);
1613
- }
1614
-
1615
- prompt += `\n\n${m.execGroupStatus}
1616
- ${groupLines.join(' / ')}
1617
-
1618
- ${m.parallelRules}
1619
- ${m.parallelRulesContent(maxGroup)}
1620
-
1621
- \`\`\`
1622
- ---ROUTE---
1623
- to: dev-1
1624
- task: task-1
1625
- taskTitle: ${m.implLoginPage}
1626
- summary: ${m.implLoginSummary}
1627
- ---END_ROUTE---
1628
-
1629
- ---ROUTE---
1630
- to: dev-2
1631
- task: task-2
1632
- taskTitle: ${m.implRegisterPage}
1633
- summary: ${m.implRegisterSummary}
1634
- ---END_ROUTE---
1635
- \`\`\`
1636
-
1637
- ${m.parallelExample}`;
1638
- }
1639
-
1640
- prompt += `\n
1641
- ${m.workflowEnd}
1642
- ${m.workflowEndContent(isDevTeam)}
1643
-
1644
- ${m.taskList}
1645
- ${m.taskListContent}
1646
-
1647
- \`\`\`
1648
- ${m.taskExample}
1649
- \`\`\`
1650
-
1651
- ${m.taskListNotes}`;
1652
- }
1653
-
1654
- // Feature 进度文件说明(系统自动管理,告知角色即可)
1655
- prompt += `\n\n${m.featureRecordTitle}
1656
- ${m.featureRecordContent}`;
1657
-
1658
- // 执行者角色的组绑定 prompt(count > 1 时)
1659
- if (role.groupIndex > 0 && role.roleType === 'developer') {
1660
- const gi = role.groupIndex;
1661
- const rev = allRoles.find(r => r.roleType === 'reviewer' && r.groupIndex === gi);
1662
- const test = allRoles.find(r => r.roleType === 'tester' && r.groupIndex === gi);
1663
- if (rev && test) {
1664
- prompt += `\n\n${m.devGroupBinding}
1665
- ${m.devGroupBindingContent(gi, roleLabel(rev), rev.name, roleLabel(test), test.name)}
1666
-
1667
- \`\`\`
1668
- ---ROUTE---
1669
- to: ${rev.name}
1670
- summary: ${m.reviewCode}
1671
- ---END_ROUTE---
1672
-
1673
- ---ROUTE---
1674
- to: ${test.name}
1675
- summary: ${m.testFeature}
1676
- ---END_ROUTE---
1677
- \`\`\`
1678
-
1679
- ${m.devGroupBindingNote}`;
1680
- }
1681
- }
1682
-
1683
- // Language instruction
1684
- if (session.language === 'en') {
1685
- prompt += `\n\n# Language
1686
- Always respond in English. Use English for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.`;
1687
- } else {
1688
- prompt += `\n\n# Language
1689
- Always respond in 中文. Use 中文 for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.`;
1690
- }
1691
-
1692
- return prompt;
1693
- }
1694
-
1695
- // =====================================================================
1696
- // Role Output Processing
1697
- // =====================================================================
1698
-
1699
- // Context 使用率阈值常量
1700
- const MAX_CONTEXT = 128000; // API max_prompt_tokens 限制
1701
- const COMPACT_THRESHOLD = 0.85; // 85% → 触发预防性 compact
1702
- const CLEAR_THRESHOLD = 0.95; // 95% → compact 后仍超限则 clear + rebuild
1703
-
1704
- /**
1705
- * 处理角色的流式输出
1706
- */
1707
- async function processRoleOutput(session, roleName, roleQuery, roleState) {
1708
- try {
1709
- for await (const message of roleQuery) {
1710
- // 检查 session 是否已停止或暂停
1711
- if (session.status === 'stopped' || session.status === 'paused') break;
1712
-
1713
- if (message.type === 'system' && message.subtype === 'init') {
1714
- roleState.claudeSessionId = message.session_id;
1715
- console.log(`[Crew] ${roleName} session: ${message.session_id}`);
1716
- continue;
1717
- }
1718
-
1719
- // ★ compact 消息过滤(compact 期间只放行 result,其余过滤)
1720
- if (roleState._compacting && message.type !== 'result') {
1721
- if (message.type === 'system') {
1722
- if (message.subtype === 'compact_boundary') {
1723
- roleState._compactSummaryPending = true;
1724
- }
1725
- continue; // 过滤所有 compact 期间的 system 消息
1726
- }
1727
- if (message.type === 'user' && roleState._compactSummaryPending) {
1728
- roleState._compactSummaryPending = false;
1729
- continue; // 过滤 compact summary
1730
- }
1731
- // 其他消息(assistant 等)在 compact 期间也过滤
1732
- continue;
1733
- }
1734
-
1735
- if (message.type === 'assistant') {
1736
- // 累积文本用于路由解析
1737
- const content = message.message?.content;
1738
- if (content) {
1739
- if (typeof content === 'string') {
1740
- roleState.accumulatedText += content;
1741
- // 转发流式文本到 Web
1742
- sendCrewOutput(session, roleName, 'text', message);
1743
- } else if (Array.isArray(content)) {
1744
- let hasText = false;
1745
- for (const block of content) {
1746
- if (block.type === 'text') {
1747
- roleState.accumulatedText += block.text;
1748
- hasText = true;
1749
- } else if (block.type === 'tool_use') {
1750
- // ★ 修复5: tool_use 时结束该角色前一条 streaming 文本
1751
- endRoleStreaming(session, roleName);
1752
- roleState.currentTool = block.name;
1753
- sendCrewOutput(session, roleName, 'tool_use', message);
1754
- }
1755
- }
1756
- if (hasText) {
1757
- sendCrewOutput(session, roleName, 'text', message);
1758
- }
1759
- }
1760
- }
1761
- } else if (message.type === 'user') {
1762
- // tool results — clear currentTool
1763
- roleState.currentTool = null;
1764
- sendCrewOutput(session, roleName, 'tool_result', message);
1765
- } else if (message.type === 'result') {
1766
- // ★ Turn 完成!
1767
- console.log(`[Crew] ${roleName} turn completed`);
1768
- roleState.consecutiveErrors = 0;
1769
-
1770
- // ★ 修复2: 反向搜索该角色最后一条 streaming 消息并结束
1771
- endRoleStreaming(session, roleName);
1772
-
1773
- // 更新费用(差值计算:每个角色独立进程,total_cost_usd 是该角色的累计值)
1774
- if (message.total_cost_usd != null) {
1775
- const costDelta = message.total_cost_usd - roleState.lastCostUsd;
1776
- if (costDelta > 0) session.costUsd += costDelta;
1777
- roleState.lastCostUsd = message.total_cost_usd;
1778
- }
1779
- // 更新 token 用量(差值计算:usage 是 query 实例级累计值)
1780
- if (message.usage) {
1781
- const inputDelta = (message.usage.input_tokens || 0) - (roleState.lastInputTokens || 0);
1782
- const outputDelta = (message.usage.output_tokens || 0) - (roleState.lastOutputTokens || 0);
1783
- if (inputDelta > 0) session.totalInputTokens += inputDelta;
1784
- if (outputDelta > 0) session.totalOutputTokens += outputDelta;
1785
- roleState.lastInputTokens = message.usage.input_tokens || 0;
1786
- roleState.lastOutputTokens = message.usage.output_tokens || 0;
1787
- }
1788
-
1789
- // ★ compact turn 完成的处理
1790
- if (roleState._compacting) {
1791
- roleState._compacting = false;
1792
- const postCompactTokens = message.usage?.input_tokens || 0;
1793
- const postCompactPercentage = postCompactTokens / MAX_CONTEXT;
1794
- console.log(`[Crew] ${roleName} compact completed, context now at ${Math.round(postCompactPercentage * 100)}%`);
1795
-
1796
- sendCrewMessage({
1797
- type: 'crew_role_compact',
1798
- sessionId: session.id,
1799
- role: roleName,
1800
- contextPercentage: Math.round(postCompactPercentage * 100),
1801
- status: 'completed'
1802
- });
1803
-
1804
- // Layer 2: compact 后仍超 95% → clear + rebuild
1805
- if (postCompactPercentage >= CLEAR_THRESHOLD) {
1806
- console.warn(`[Crew] ${roleName} still at ${Math.round(postCompactPercentage * 100)}% after compact, escalating to clear`);
1807
-
1808
- await clearRoleSessionId(session.sharedDir, roleName);
1809
- roleState.claudeSessionId = null;
1810
-
1811
- if (roleState.abortController) roleState.abortController.abort();
1812
- roleState.query = null;
1813
- roleState.inputStream = null;
1814
-
1815
- sendCrewMessage({
1816
- type: 'crew_role_compact',
1817
- sessionId: session.id,
1818
- role: roleName,
1819
- status: 'cleared'
1820
- });
1821
-
1822
- // 重新 dispatch 缓存的路由(用新会话)
1823
- if (roleState._pendingCompactRoutes) {
1824
- const routes = roleState._pendingCompactRoutes;
1825
- const fromRole = roleState._fromRole;
1826
- roleState._pendingCompactRoutes = null;
1827
- roleState._fromRole = null;
1828
- session.round++;
1829
- const results = await Promise.allSettled(routes.map(route =>
1830
- executeRoute(session, fromRole, route)
1831
- ));
1832
- for (const r of results) {
1833
- if (r.status === 'rejected') {
1834
- console.warn(`[Crew] Route execution failed:`, r.reason);
1835
- }
1836
- }
1837
- } else if (roleState._pendingDispatch) {
1838
- const pd = roleState._pendingDispatch;
1839
- roleState._pendingDispatch = null;
1840
- await dispatchToRole(session, roleName, pd.content, pd.from, pd.taskId, pd.taskTitle);
1841
- }
1842
- return; // abort 后 query 已清空,退出 processRoleOutput
1843
- }
1844
-
1845
- // 执行之前缓存的路由
1846
- if (roleState._pendingCompactRoutes) {
1847
- const routes = roleState._pendingCompactRoutes;
1848
- const fromRole = roleState._fromRole;
1849
- roleState._pendingCompactRoutes = null;
1850
- roleState._fromRole = null;
1851
- session.round++;
1852
- const results = await Promise.allSettled(routes.map(route =>
1853
- executeRoute(session, fromRole, route)
1854
- ));
1855
- for (const r of results) {
1856
- if (r.status === 'rejected') {
1857
- console.warn(`[Crew] Route execution failed:`, r.reason);
1858
- }
1859
- }
1860
- } else if (roleState._pendingDispatch) {
1861
- const pd = roleState._pendingDispatch;
1862
- roleState._pendingDispatch = null;
1863
- await dispatchToRole(session, roleName, pd.content, pd.from, pd.taskId, pd.taskTitle);
1864
- }
1865
- continue; // 不要重复处理这个 compact result
1866
- }
1867
-
1868
- // ★ 持久化 sessionId(每次 turn 完成后保存,用于后续 resume)
1869
- if (roleState.claudeSessionId) {
1870
- saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
1871
- .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
1872
- }
1873
-
1874
- // ★ context 使用率监控
1875
- const inputTokens = message.usage?.input_tokens || 0;
1876
- if (inputTokens > 0) {
1877
- sendCrewMessage({
1878
- type: 'crew_context_usage',
1879
- sessionId: session.id,
1880
- role: roleName,
1881
- inputTokens,
1882
- maxTokens: MAX_CONTEXT,
1883
- percentage: Math.min(100, Math.round((inputTokens / MAX_CONTEXT) * 100))
1884
- });
1885
- }
1886
-
1887
- const contextPercentage = inputTokens / MAX_CONTEXT;
1888
- const needCompact = contextPercentage >= COMPACT_THRESHOLD;
1889
-
1890
- // 解析路由(支持多 ROUTE 块)
1891
- const routes = parseRoutes(roleState.accumulatedText);
1892
-
1893
- // ★ 决策者 turn 完成:检测 TASKS block 中新完成的任务
1894
- const roleConfig = session.roles.get(roleName);
1895
- if (roleConfig?.isDecisionMaker) {
1896
- const nowCompleted = parseCompletedTasks(roleState.accumulatedText);
1897
- if (nowCompleted.size > 0) {
1898
- const prev = session._completedTaskIds || new Set();
1899
- const newlyDone = [];
1900
- for (const tid of nowCompleted) {
1901
- if (!prev.has(tid)) {
1902
- prev.add(tid);
1903
- newlyDone.push(tid);
1904
- }
1905
- }
1906
- session._completedTaskIds = prev;
1907
- if (newlyDone.length > 0) {
1908
- // 更新索引 + 追加 changelog(fire-and-forget)
1909
- updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
1910
- for (const tid of newlyDone) {
1911
- const feature = session.features.get(tid);
1912
- const title = feature?.taskTitle || tid;
1913
- appendChangelog(session, tid, title).catch(e => console.warn(`[Crew] Failed to append changelog for ${tid}:`, e.message));
1914
- }
1915
- }
1916
- }
1917
- }
1918
-
1919
- roleState.accumulatedText = '';
1920
- roleState.turnActive = false;
1921
-
1922
- // 通知 turn 完成
1923
- sendCrewMessage({
1924
- type: 'crew_turn_completed',
1925
- sessionId: session.id,
1926
- role: roleName
1927
- });
1928
-
1929
- // 发送状态更新
1930
- sendStatusUpdate(session);
1931
-
1932
- // ★ 需要 compact:缓存路由,先执行 compact
1933
- if (needCompact) {
1934
- console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, compacting before next dispatch`);
1935
-
1936
- roleState._pendingCompactRoutes = routes.length > 0 ? routes : null;
1937
- roleState._compacting = true;
1938
- roleState._compactSummaryPending = false;
1939
- roleState._fromRole = roleName;
1940
-
1941
- // task 继承
1942
- const currentTask = roleState.currentTask;
1943
- if (roleState._pendingCompactRoutes) {
1944
- for (const route of roleState._pendingCompactRoutes) {
1945
- if (!route.taskId && currentTask) {
1946
- route.taskId = currentTask.taskId;
1947
- route.taskTitle = currentTask.taskTitle;
1948
- }
1949
- }
1950
- }
1951
-
1952
- // 发送 /compact
1953
- roleState.inputStream.enqueue({
1954
- type: 'user',
1955
- message: { role: 'user', content: '/compact' }
1956
- });
1957
-
1958
- sendCrewMessage({
1959
- type: 'crew_role_compact',
1960
- sessionId: session.id,
1961
- role: roleName,
1962
- contextPercentage: Math.round(contextPercentage * 100),
1963
- status: 'compacting'
1964
- });
1965
-
1966
- continue; // 等 compact turn 完成
1967
- }
1968
-
1969
- // 执行路由(无需 compact 时)
1970
- if (routes.length > 0) {
1971
- // ★ 修复1: 多 ROUTE 只增 1 轮(round++ 从 executeRoute 移到这里)
1972
- session.round++;
1973
-
1974
- // task 继承:如果路由没有指定 taskId,从当前角色继承
1975
- const currentTask = roleState.currentTask;
1976
- for (const route of routes) {
1977
- if (!route.taskId && currentTask) {
1978
- route.taskId = currentTask.taskId;
1979
- route.taskTitle = currentTask.taskTitle;
1980
- }
1981
- }
1982
-
1983
- // 并行执行所有路由(allSettled 保证单个失败不中断其他)
1984
- const results = await Promise.allSettled(routes.map(route =>
1985
- executeRoute(session, roleName, route)
1986
- ));
1987
- for (const r of results) {
1988
- if (r.status === 'rejected') {
1989
- console.warn(`[Crew] Route execution failed:`, r.reason);
1990
- }
1991
- }
1992
- } else {
1993
- // 没有路由,检查是否有人的消息在排队
1994
- await processHumanQueue(session);
1995
- }
1996
- }
1997
- }
1998
- } catch (error) {
1999
- if (error.name === 'AbortError') {
2000
- console.log(`[Crew] ${roleName} aborted`);
2001
- // 暂停时:检查已累积的文本中是否有 route,保存为 pendingRoutes
2002
- if (session.status === 'paused' && roleState.accumulatedText) {
2003
- const routes = parseRoutes(roleState.accumulatedText);
2004
- if (routes.length > 0 && session.pendingRoutes.length === 0) {
2005
- session.pendingRoutes = routes.map(route => ({ fromRole: roleName, route }));
2006
- console.log(`[Crew] Saved ${routes.length} pending route(s) from aborted ${roleName}`);
2007
- }
2008
- roleState.accumulatedText = '';
2009
- }
2010
- } else {
2011
- console.error(`[Crew] ${roleName} error:`, error.message);
2012
-
2013
- // Step 1: 清理 roleState(防止后续写入死进程)
2014
- endRoleStreaming(session, roleName);
2015
- roleState.query = null;
2016
- roleState.inputStream = null;
2017
- roleState.turnActive = false;
2018
- roleState.accumulatedText = '';
2019
- // 重置 compact 状态(防止 compact 期间出错导致后续消息被永久过滤)
2020
- roleState._compacting = false;
2021
- roleState._compactSummaryPending = false;
2022
-
2023
- // Step 2: 错误分类
2024
- const classification = classifyRoleError(error);
2025
- roleState.consecutiveErrors++;
2026
-
2027
- // Step 3: 通知前端
2028
- sendCrewMessage({
2029
- type: 'crew_role_error',
2030
- sessionId: session.id,
2031
- role: roleName,
2032
- error: error.message.substring(0, 500),
2033
- reason: classification.reason,
2034
- recoverable: classification.recoverable,
2035
- retryCount: roleState.consecutiveErrors
2036
- });
2037
- sendStatusUpdate(session);
2038
-
2039
- // Step 4: 判断是否重试
2040
- const MAX_RETRIES = 3;
2041
- if (!classification.recoverable || roleState.consecutiveErrors > MAX_RETRIES) {
2042
- const exhausted = roleState.consecutiveErrors > MAX_RETRIES;
2043
- const errDetail = exhausted
2044
- ? `角色 ${roleName} 连续 ${MAX_RETRIES} 次错误后停止重试。最后错误: ${error.message}`
2045
- : `角色 ${roleName} 不可恢复错误: ${error.message}`;
2046
- if (roleName !== session.decisionMaker) {
2047
- await dispatchToRole(session, session.decisionMaker, errDetail, 'system');
2048
- } else {
2049
- session.status = 'waiting_human';
2050
- sendCrewMessage({
2051
- type: 'crew_human_needed',
2052
- sessionId: session.id,
2053
- fromRole: roleName,
2054
- reason: 'error',
2055
- message: errDetail
2056
- });
2057
- sendStatusUpdate(session);
2058
- }
2059
- return;
2060
- }
2061
-
2062
- // Step 5: 可恢复 → 自动重建并重试
2063
- console.log(`[Crew] ${roleName} attempting recovery (${classification.reason}), retry ${roleState.consecutiveErrors}/${MAX_RETRIES}`);
2064
-
2065
- sendCrewOutput(session, 'system', 'system', {
2066
- type: 'assistant',
2067
- message: { role: 'assistant', content: [{
2068
- type: 'text',
2069
- text: `${roleName} 遇到 ${classification.reason},正在自动恢复 (${roleState.consecutiveErrors}/${MAX_RETRIES})...`
2070
- }] }
2071
- });
2072
-
2073
- if (roleState.lastDispatchContent) {
2074
- // ★ context_exceeded: clear sessionId → rebuild query → /compact → 重派
2075
- if (classification.reason === 'context_exceeded') {
2076
- await clearRoleSessionId(session.sharedDir, roleName);
2077
- const newState = await createRoleQuery(session, roleName);
2078
-
2079
- // 缓存待重派内容,compact 完成后自动发送
2080
- newState._pendingDispatch = {
2081
- content: roleState.lastDispatchContent,
2082
- from: roleState.lastDispatchFrom || 'system',
2083
- taskId: roleState.lastDispatchTaskId,
2084
- taskTitle: roleState.lastDispatchTaskTitle
2085
- };
2086
- newState._compacting = true;
2087
- newState._compactSummaryPending = false;
2088
- newState.consecutiveErrors = roleState.consecutiveErrors;
2089
-
2090
- newState.inputStream.enqueue({
2091
- type: 'user',
2092
- message: { role: 'user', content: '/compact' }
2093
- });
2094
-
2095
- sendCrewMessage({
2096
- type: 'crew_role_compact',
2097
- sessionId: session.id,
2098
- role: roleName,
2099
- status: 'compacting'
2100
- });
2101
- } else {
2102
- // 其他可恢复错误:原有重派逻辑
2103
- if (classification.skipResume) {
2104
- await clearRoleSessionId(session.sharedDir, roleName);
2105
- }
2106
- await dispatchToRole(
2107
- session, roleName,
2108
- roleState.lastDispatchContent,
2109
- roleState.lastDispatchFrom || 'system',
2110
- roleState.lastDispatchTaskId,
2111
- roleState.lastDispatchTaskTitle
2112
- );
2113
- }
2114
- } else {
2115
- const msg = `角色 ${roleName} 已恢复(${classification.reason}),但无待重试消息。`;
2116
- if (roleName !== session.decisionMaker) {
2117
- await dispatchToRole(session, session.decisionMaker, msg, 'system');
2118
- }
2119
- }
2120
- }
2121
- }
2122
- }
2123
-
2124
- /**
2125
- * 结束指定角色的最后一条 streaming 消息(反向搜索)
2126
- */
2127
- function endRoleStreaming(session, roleName) {
2128
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
2129
- if (session.uiMessages[i].role === roleName && session.uiMessages[i]._streaming) {
2130
- delete session.uiMessages[i]._streaming;
2131
- break;
2132
- }
2133
- }
2134
- }
2135
-
2136
- // =====================================================================
2137
- // Route Parsing & Execution
2138
- // =====================================================================
2139
-
2140
- /**
2141
- * 从累积文本中解析所有 ROUTE 块(支持多 ROUTE + task 字段)
2142
- * @returns {Array<{ to, summary, taskId, taskTitle }>}
2143
- */
2144
- function parseRoutes(text) {
2145
- const routes = [];
2146
- const regex = /---ROUTE---\s*\n([\s\S]*?)---END_ROUTE---/g;
2147
- let match;
2148
-
2149
- while ((match = regex.exec(text)) !== null) {
2150
- const block = match[1];
2151
- const toMatch = block.match(/to:\s*(.+)/i);
2152
- if (!toMatch) continue;
2153
-
2154
- const summaryMatch = block.match(/summary:\s*([\s\S]+)/i);
2155
- const taskMatch = block.match(/^task:\s*(.+)/im);
2156
- const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
2157
-
2158
- routes.push({
2159
- to: toMatch[1].trim().toLowerCase(),
2160
- summary: summaryMatch ? summaryMatch[1].trim() : '',
2161
- taskId: taskMatch ? taskMatch[1].trim() : null,
2162
- taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
2163
- });
2164
- }
2165
-
2166
- return routes;
2167
- }
2168
-
2169
- /**
2170
- * 执行路由
2171
- */
2172
- async function executeRoute(session, fromRole, route) {
2173
- const { to, summary, taskId, taskTitle } = route;
2174
-
2175
- // ★ round++ 已移到 processRoleOutput 中(多 ROUTE 只增 1 轮)
2176
-
2177
- // 如果 session 已暂停或停止,保存为 pendingRoutes
2178
- if (session.status === 'paused' || session.status === 'stopped') {
2179
- session.pendingRoutes.push({ fromRole, route });
2180
- console.log(`[Crew] Session ${session.status}, route saved as pending: ${fromRole} -> ${to}`);
2181
- return;
2182
- }
2183
-
2184
- // ★ Task 文件自动管理(fire-and-forget,不阻塞路由执行)
2185
- if (taskId && summary) {
2186
- // 如果是决策者发出的 ROUTE(分配任务),自动创建 task 文件
2187
- const fromRoleConfig = session.roles.get(fromRole);
2188
- if (fromRoleConfig?.isDecisionMaker && taskTitle && to !== 'human') {
2189
- ensureTaskFile(session, taskId, taskTitle, to, summary)
2190
- .catch(e => console.warn(`[Crew] Failed to create task file ${taskId}:`, e.message));
2191
- }
2192
- // 任何角色的 ROUTE 都追加工作记录
2193
- appendTaskRecord(session, taskId, fromRole, summary)
2194
- .catch(e => console.warn(`[Crew] Failed to append task record ${taskId}:`, e.message));
2195
- }
2196
-
2197
- // 发送路由消息(UI 显示 → @xxx)
2198
- sendCrewOutput(session, fromRole, 'route', null, { routeTo: to, routeSummary: summary });
2199
-
2200
- // 路由到 human
2201
- if (to === 'human') {
2202
- session.status = 'waiting_human';
2203
- session.waitingHumanContext = {
2204
- fromRole,
2205
- reason: 'requested',
2206
- message: summary
2207
- };
2208
- sendCrewMessage({
2209
- type: 'crew_human_needed',
2210
- sessionId: session.id,
2211
- fromRole,
2212
- reason: 'requested',
2213
- message: summary
2214
- });
2215
- sendStatusUpdate(session);
2216
- return;
2217
- }
2218
-
2219
- // 路由到指定角色
2220
- if (session.roles.has(to)) {
2221
- // task 信息通过 dispatchToRole 内部设置(createRoleQuery 之后 roleState 才存在)
2222
-
2223
- // 先检查是否有人的消息在排队
2224
- if (session.humanMessageQueue.length > 0) {
2225
- // 人的消息优先
2226
- await processHumanQueue(session);
2227
- } else {
2228
- const taskPrompt = buildRoutePrompt(fromRole, summary, session);
2229
- await dispatchToRole(session, to, taskPrompt, fromRole, taskId, taskTitle);
2230
- }
2231
- } else {
2232
- console.warn(`[Crew] Unknown route target: ${to}`);
2233
- // 转给决策者
2234
- const errorMsg = `路由目标 "${to}" 不存在。来自 ${fromRole} 的消息: ${summary}`;
2235
- await dispatchToRole(session, session.decisionMaker, errorMsg, 'system');
2236
- }
2237
- }
2238
-
2239
- /**
2240
- * 构建路由转发的 prompt
2241
- */
2242
- function buildRoutePrompt(fromRole, summary, session) {
2243
- const fromRoleConfig = session.roles.get(fromRole);
2244
- const fromName = fromRoleConfig ? roleLabel(fromRoleConfig) : fromRole;
2245
- return `来自 ${fromName} 的消息:\n${summary}\n\n请开始你的工作。完成后通过 ROUTE 块传递给下一个角色。`;
2246
- }
2247
-
2248
- // =====================================================================
2249
- // Message Dispatching
2250
- // =====================================================================
2251
-
2252
- /**
2253
- * 向角色发送消息
2254
- */
2255
- async function dispatchToRole(session, roleName, content, fromSource, taskId, taskTitle) {
2256
- if (session.status === 'paused' || session.status === 'stopped' || session.status === 'initializing') {
2257
- console.log(`[Crew] Session ${session.status}, skipping dispatch to ${roleName}`);
2258
- return;
2259
- }
2260
-
2261
- let roleState = session.roleStates.get(roleName);
2262
-
2263
- // 如果角色没有 query 实例,创建一个(支持 resume)
2264
- if (!roleState || !roleState.query || !roleState.inputStream) {
2265
- roleState = await createRoleQuery(session, roleName);
2266
- }
2267
-
2268
- // 设置 task(createRoleQuery 之后 roleState 一定存在)
2269
- if (taskId) {
2270
- roleState.currentTask = { taskId, taskTitle };
2271
- }
2272
-
2273
- // ★ Task 上下文注入:如果有 taskId,读取 task 文件注入到消息中
2274
- const effectiveTaskId = taskId || roleState.currentTask?.taskId;
2275
- if (effectiveTaskId && typeof content === 'string') {
2276
- const taskContent = await readTaskFile(session, effectiveTaskId);
2277
- if (taskContent) {
2278
- content = `${content}\n\n---\n<task-context file="context/features/${effectiveTaskId}.md">\n${taskContent}\n</task-context>`;
2279
- }
2280
- }
2281
-
2282
- // 记录消息历史
2283
- session.messageHistory.push({
2284
- from: fromSource,
2285
- to: roleName,
2286
- content: typeof content === 'string' ? content.substring(0, 200) : '...',
2287
- taskId: taskId || roleState.currentTask?.taskId || null,
2288
- timestamp: Date.now()
2289
- });
2290
-
2291
- // 发送
2292
- roleState.lastDispatchContent = content;
2293
- roleState.lastDispatchFrom = fromSource;
2294
- roleState.lastDispatchTaskId = taskId || null;
2295
- roleState.lastDispatchTaskTitle = taskTitle || null;
2296
- roleState.turnActive = true;
2297
- roleState.accumulatedText = '';
2298
- roleState.inputStream.enqueue({
2299
- type: 'user',
2300
- message: { role: 'user', content }
2301
- });
2302
-
2303
- sendStatusUpdate(session);
2304
- console.log(`[Crew] Dispatched to ${roleName} from ${fromSource}${taskId ? ` (task: ${taskId})` : ''}`);
2305
- }
2306
-
2307
- // =====================================================================
2308
- // Human Interaction
2309
- // =====================================================================
2310
-
2311
- /**
2312
- * 处理人的输入
2313
- */
2314
- export async function handleCrewHumanInput(msg) {
2315
- const { sessionId, content, targetRole, files } = msg;
2316
- const session = crewSessions.get(sessionId);
2317
- if (!session) {
2318
- console.warn(`[Crew] Session not found: ${sessionId}`);
2319
- return;
2320
- }
2321
-
2322
- // Build dispatch content (supports image attachments)
2323
- function buildHumanContent(prefix, text) {
2324
- if (files && files.length > 0) {
2325
- const blocks = [];
2326
- for (const file of files) {
2327
- if (file.isImage || file.mimeType?.startsWith('image/')) {
2328
- blocks.push({
2329
- type: 'image',
2330
- source: { type: 'base64', media_type: file.mimeType, data: file.data }
2331
- });
2332
- }
2333
- }
2334
- blocks.push({ type: 'text', text: `${prefix}\n${text}` });
2335
- return blocks;
2336
- }
2337
- return `${prefix}\n${text}`;
2338
- }
2339
-
2340
- // 注意:不在这里发送人的消息到 Web(前端已本地添加,避免重复)
2341
- // 但需要记录到 uiMessages 用于恢复时重放
2342
- session.uiMessages.push({
2343
- role: 'human', roleIcon: '', roleName: '你',
2344
- type: 'text', content,
2345
- timestamp: Date.now()
2346
- });
2347
-
2348
- // 如果在等待人工介入
2349
- if (session.status === 'waiting_human') {
2350
- const waitingContext = session.waitingHumanContext;
2351
- session.status = 'running';
2352
- session.waitingHumanContext = null;
2353
- sendStatusUpdate(session);
2354
-
2355
- // 发给请求人工介入的角色,或指定的目标角色
2356
- const target = targetRole || waitingContext?.fromRole || session.decisionMaker;
2357
- await dispatchToRole(session, target, buildHumanContent('人工回复:', content), 'human');
2358
- return;
2359
- }
2360
-
2361
- // 解析 @role 指令(支持 name 和 displayName)
2362
- const atMatch = content.match(/^@(\S+)\s*([\s\S]*)/);
2363
- if (atMatch) {
2364
- const atTarget = atMatch[1];
2365
- const message = atMatch[2].trim() || content;
2366
-
2367
- // 先精确匹配 role.name,再匹配 displayName
2368
- let target = null;
2369
- for (const [name, role] of session.roles) {
2370
- if (name === atTarget.toLowerCase()) {
2371
- target = name;
2372
- break;
2373
- }
2374
- if (role.displayName === atTarget) {
2375
- target = name;
2376
- break;
2377
- }
2378
- }
2379
-
2380
- if (target) {
2381
- await dispatchToRole(session, target, buildHumanContent('人工消息:', message), 'human');
2382
- return;
2383
- }
2384
- }
2385
-
2386
- // 没有 @ 指定目标,默认发给决策者(PM)
2387
- const target = targetRole || session.decisionMaker;
2388
-
2389
- await dispatchToRole(session, target, buildHumanContent('人工消息:', content), 'human');
2390
- }
2391
-
2392
- /**
2393
- * 处理排队的人的消息
2394
- */
2395
- async function processHumanQueue(session) {
2396
- if (session.humanMessageQueue.length === 0) return;
2397
- if (session._processingHumanQueue) return;
2398
- session._processingHumanQueue = true;
2399
- try {
2400
- const msgs = session.humanMessageQueue.splice(0);
2401
- if (msgs.length === 1) {
2402
- const humanPrompt = `人工消息:\n${msgs[0].content}`;
2403
- await dispatchToRole(session, msgs[0].target, humanPrompt, 'human');
2404
- } else {
2405
- // 按 target 分组,合并发送
2406
- const byTarget = new Map();
2407
- for (const m of msgs) {
2408
- if (!byTarget.has(m.target)) byTarget.set(m.target, []);
2409
- byTarget.get(m.target).push(m.content);
2410
- }
2411
- for (const [target, contents] of byTarget) {
2412
- const combined = contents.join('\n\n---\n\n');
2413
- const humanPrompt = `人工消息:\n你有 ${contents.length} 条待处理消息,请一并分析并用多个 ROUTE 块并行分配:\n\n${combined}`;
2414
- await dispatchToRole(session, target, humanPrompt, 'human');
2415
- }
2416
- }
2417
- } finally {
2418
- session._processingHumanQueue = false;
2419
- }
2420
- }
2421
-
2422
- /**
2423
- * 找到当前活跃的角色(最近一个 turnActive 的)
2424
- */
2425
- function findActiveRole(session) {
2426
- for (const [name, state] of session.roleStates) {
2427
- if (state.turnActive) return name;
2428
- }
2429
- return null;
2430
- }
2431
-
2432
- // =====================================================================
2433
- // Control Operations
2434
- // =====================================================================
2435
-
2436
- /**
2437
- * 清空单个角色的对话(重置为全新状态)
2438
- */
2439
- async function clearSingleRole(session, roleName) {
2440
- const roleState = session.roleStates.get(roleName);
2441
-
2442
- // 如果角色正在 streaming,先 abort
2443
- if (roleState) {
2444
- if (roleState.abortController) {
2445
- roleState.abortController.abort();
2446
- }
2447
- roleState.query = null;
2448
- roleState.inputStream = null;
2449
- roleState.turnActive = false;
2450
- roleState._compacting = false;
2451
- roleState._compactSummaryPending = false;
2452
- roleState._pendingCompactRoutes = null;
2453
- roleState._pendingDispatch = null;
2454
- roleState._fromRole = null;
2455
- roleState.claudeSessionId = null;
2456
- roleState.consecutiveErrors = 0;
2457
- roleState.accumulatedText = '';
2458
- roleState.lastDispatchContent = null;
2459
- roleState.lastDispatchFrom = null;
2460
- roleState.lastDispatchTaskId = null;
2461
- roleState.lastDispatchTaskTitle = null;
2462
- }
2463
-
2464
- // 清除持久化的 sessionId
2465
- await clearRoleSessionId(session.sharedDir, roleName);
2466
-
2467
- sendCrewMessage({
2468
- type: 'crew_role_compact',
2469
- sessionId: session.id,
2470
- role: roleName,
2471
- status: 'cleared'
2472
- });
2473
-
2474
- sendCrewOutput(session, 'system', 'system', {
2475
- type: 'assistant',
2476
- message: { role: 'assistant', content: [{ type: 'text', text: `${roleName} 对话已清空` }] }
2477
- });
2478
- sendStatusUpdate(session);
2479
- console.log(`[Crew] Role ${roleName} cleared`);
2480
- }
2481
-
2482
- /**
2483
- * 手动压缩指定角色的上下文
2484
- * - 无活跃 query → 重建 query 后发 /compact
2485
- * - 有 query 且非 turnActive → 直接发 /compact
2486
- * - turnActive → 提示用户先停止该角色
2487
- */
2488
- async function compactRole(session, roleName) {
2489
- const roleState = session.roleStates.get(roleName);
2490
-
2491
- // Case 1: 角色正在 streaming,不能 compact
2492
- if (roleState?.turnActive) {
2493
- sendCrewMessage({
2494
- type: 'crew_role_compact',
2495
- sessionId: session.id,
2496
- role: roleName,
2497
- status: 'rejected',
2498
- reason: '角色正在工作中,请先停止该角色再压缩上下文'
2499
- });
2500
- return;
2501
- }
2502
-
2503
- // Case 2: 已经在 compacting
2504
- if (roleState?._compacting) {
2505
- sendCrewMessage({
2506
- type: 'crew_role_compact',
2507
- sessionId: session.id,
2508
- role: roleName,
2509
- status: 'rejected',
2510
- reason: '该角色正在压缩中,请等待完成'
2511
- });
2512
- return;
2513
- }
2514
-
2515
- // Case 3: 无活跃 query → 重建
2516
- let state = roleState;
2517
- if (!state || !state.query || !state.inputStream) {
2518
- console.log(`[Crew] ${roleName} has no active query, rebuilding for compact`);
2519
- state = await createRoleQuery(session, roleName);
2520
- }
2521
-
2522
- // 发送 /compact
2523
- console.log(`[Crew] Manual compact requested for ${roleName}`);
2524
- state._compacting = true;
2525
- state._compactSummaryPending = false;
2526
- state._pendingCompactRoutes = null;
2527
- state._fromRole = null;
2528
-
2529
- state.inputStream.enqueue({
2530
- type: 'user',
2531
- message: { role: 'user', content: '/compact' }
2532
- });
2533
-
2534
- sendCrewMessage({
2535
- type: 'crew_role_compact',
2536
- sessionId: session.id,
2537
- role: roleName,
2538
- status: 'compacting'
2539
- });
2540
- }
2541
-
2542
- /**
2543
- * 处理控制命令
2544
- */
2545
- export async function handleCrewControl(msg) {
2546
- const { sessionId, action, targetRole } = msg;
2547
- const session = crewSessions.get(sessionId);
2548
- if (!session) {
2549
- console.warn(`[Crew] Session not found: ${sessionId}`);
2550
- return;
2551
- }
2552
-
2553
- switch (action) {
2554
- case 'pause':
2555
- await pauseAll(session);
2556
- break;
2557
- case 'resume':
2558
- await resumeSession(session);
2559
- break;
2560
- case 'stop_role':
2561
- if (targetRole) await stopRole(session, targetRole);
2562
- break;
2563
- case 'interrupt_role':
2564
- if (targetRole && msg.content) {
2565
- await interruptRole(session, targetRole, msg.content, 'human');
2566
- }
2567
- break;
2568
- case 'abort_role':
2569
- if (targetRole) await abortRole(session, targetRole);
2570
- break;
2571
- case 'compact_role':
2572
- if (targetRole) await compactRole(session, targetRole);
2573
- break;
2574
- case 'clear_role':
2575
- if (targetRole) await clearSingleRole(session, targetRole);
2576
- break;
2577
- case 'stop_all':
2578
- await stopAll(session);
2579
- break;
2580
- case 'clear':
2581
- await clearSession(session);
2582
- break;
2583
- default:
2584
- console.warn(`[Crew] Unknown control action: ${action}`);
2585
- }
2586
- }
2587
-
2588
- /**
2589
- * 暂停所有角色
2590
- * abort 运行中的 query 并保存 sessionId,恢复时 resume
2591
- */
2592
- async function pauseAll(session) {
2593
- session.status = 'paused';
2594
-
2595
- // abort 所有运行中的角色,保存 sessionId 以便 resume
2596
- for (const [roleName, roleState] of session.roleStates) {
2597
- if (roleState.claudeSessionId) {
2598
- await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
2599
- .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
2600
- }
2601
- if (roleState.abortController) {
2602
- roleState.abortController.abort();
2603
- }
2604
- // 记录哪些角色在暂停时正在工作
2605
- roleState.wasActive = roleState.turnActive;
2606
- roleState.turnActive = false;
2607
- roleState.query = null;
2608
- roleState.inputStream = null;
2609
- }
2610
-
2611
- console.log(`[Crew] Session ${session.id} paused, all active queries aborted`);
2612
-
2613
- sendCrewOutput(session, 'system', 'system', {
2614
- type: 'assistant',
2615
- message: { role: 'assistant', content: [{ type: 'text', text: 'Session 已暂停' }] }
2616
- });
2617
- sendStatusUpdate(session);
2618
-
2619
- // 显式 await 保存,确保暂停状态落盘
2620
- await saveSessionMeta(session);
2621
- }
2622
-
2623
- /**
2624
- * 恢复 session
2625
- * 重新执行被暂停时保存的 pendingRoutes
2626
- */
2627
- async function resumeSession(session) {
2628
- if (session.status !== 'paused') return;
2629
-
2630
- session.status = 'running';
2631
- console.log(`[Crew] Session ${session.id} resumed`);
2632
-
2633
- sendCrewOutput(session, 'system', 'system', {
2634
- type: 'assistant',
2635
- message: { role: 'assistant', content: [{ type: 'text', text: 'Session 已恢复' }] }
2636
- });
2637
- sendStatusUpdate(session);
2638
-
2639
- // 恢复被中断的路由(可能有多条)
2640
- if (session.pendingRoutes.length > 0) {
2641
- const pending = session.pendingRoutes.slice();
2642
- session.pendingRoutes = [];
2643
- console.log(`[Crew] Replaying ${pending.length} pending route(s)`);
2644
- const results = await Promise.allSettled(pending.map(({ fromRole, route }) =>
2645
- executeRoute(session, fromRole, route)
2646
- ));
2647
- for (const r of results) {
2648
- if (r.status === 'rejected') {
2649
- console.warn(`[Crew] Pending route replay failed:`, r.reason);
2650
- }
2651
- }
2652
- return;
2653
- }
2654
-
2655
- // 没有 pendingRoutes,检查排队的人的消息
2656
- await processHumanQueue(session);
2657
- }
2658
-
2659
- /**
2660
- * 停止单个角色
2661
- */
2662
- /**
2663
- * 中断角色当前 turn 并发送新消息
2664
- */
2665
- async function interruptRole(session, roleName, newContent, fromSource = 'human') {
2666
- const roleState = session.roleStates.get(roleName);
2667
- if (!roleState) {
2668
- console.warn(`[Crew] Cannot interrupt ${roleName}: no roleState`);
2669
- return;
2670
- }
2671
-
2672
- console.log(`[Crew] Interrupting ${roleName}`);
2673
-
2674
- // 结束 streaming 状态
2675
- endRoleStreaming(session, roleName);
2676
-
2677
- // 保存 sessionId 用于 resume 上下文连续性
2678
- if (roleState.claudeSessionId) {
2679
- await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
2680
- .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
2681
- }
2682
-
2683
- // Abort 当前 query
2684
- if (roleState.abortController) {
2685
- roleState.abortController.abort();
2686
- }
2687
-
2688
- // 清理旧状态
2689
- roleState.query = null;
2690
- roleState.inputStream = null;
2691
- roleState.turnActive = false;
2692
- roleState.accumulatedText = '';
2693
-
2694
- // 通知前端中断
2695
- sendCrewMessage({
2696
- type: 'crew_turn_completed',
2697
- sessionId: session.id,
2698
- role: roleName,
2699
- interrupted: true
2700
- });
2701
-
2702
- sendStatusUpdate(session);
2703
-
2704
- // 系统消息记录
2705
- sendCrewOutput(session, 'system', 'system', {
2706
- type: 'assistant',
2707
- message: { role: 'assistant', content: [{ type: 'text', text: `${roleName} 被中断` }] }
2708
- });
2709
-
2710
- // 创建新 query 并 dispatch
2711
- await dispatchToRole(session, roleName, newContent, fromSource);
2712
- }
2713
-
2714
- /**
2715
- * 中止角色当前 turn(不删除角色状态,不注入新内容)
2716
- * 与 stopRole 区别:stopRole 会 delete roleState,abortRole 只中断当前 query
2717
- * 与 interruptRole 区别:interruptRole 中断后会 dispatch 新消息,abortRole 不会
2718
- */
2719
- async function abortRole(session, roleName) {
2720
- const roleState = session.roleStates.get(roleName);
2721
- if (!roleState) {
2722
- console.warn(`[Crew] Cannot abort ${roleName}: no roleState`);
2723
- return;
2724
- }
2725
-
2726
- if (!roleState.turnActive) {
2727
- console.log(`[Crew] ${roleName} is not active, nothing to abort`);
2728
- return;
2729
- }
2730
-
2731
- console.log(`[Crew] Aborting ${roleName}`);
2732
-
2733
- // 结束 streaming 状态
2734
- endRoleStreaming(session, roleName);
2735
-
2736
- // 保存 sessionId 以便后续继续对话
2737
- if (roleState.claudeSessionId) {
2738
- await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
2739
- .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
2740
- }
2741
-
2742
- // Abort 当前 query
2743
- if (roleState.abortController) {
2744
- roleState.abortController.abort();
2745
- }
2746
-
2747
- // 清理 turn 状态,角色变为 idle
2748
- roleState.query = null;
2749
- roleState.inputStream = null;
2750
- roleState.turnActive = false;
2751
- roleState.accumulatedText = '';
2752
-
2753
- // 通知前端 turn 已完成(中断方式)
2754
- sendCrewMessage({
2755
- type: 'crew_turn_completed',
2756
- sessionId: session.id,
2757
- role: roleName,
2758
- interrupted: true
2759
- });
2760
-
2761
- sendStatusUpdate(session);
2762
-
2763
- sendCrewOutput(session, 'system', 'system', {
2764
- type: 'assistant',
2765
- message: { role: 'assistant', content: [{ type: 'text', text: `${roleName} 已中止` }] }
2766
- });
2767
- }
2768
-
2769
- async function stopRole(session, roleName) {
2770
- const roleState = session.roleStates.get(roleName);
2771
- if (roleState) {
2772
- // 保存 sessionId
2773
- if (roleState.claudeSessionId) {
2774
- await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
2775
- .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
2776
- }
2777
- if (roleState.abortController) {
2778
- roleState.abortController.abort();
2779
- }
2780
- roleState.query = null;
2781
- roleState.inputStream = null;
2782
- roleState.turnActive = false;
2783
- session.roleStates.delete(roleName);
2784
- }
2785
-
2786
- sendCrewOutput(session, 'system', 'system', {
2787
- type: 'assistant',
2788
- message: { role: 'assistant', content: [{ type: 'text', text: `${roleName} 已停止` }] }
2789
- });
2790
- sendStatusUpdate(session);
2791
- console.log(`[Crew] Role ${roleName} stopped`);
2792
- }
2793
-
2794
- /**
2795
- * 终止整个 session
2796
- */
2797
- async function stopAll(session) {
2798
- session.status = 'stopped';
2799
-
2800
- // 保存所有角色的 sessionId
2801
- for (const [roleName, roleState] of session.roleStates) {
2802
- if (roleState.claudeSessionId) {
2803
- await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
2804
- .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
2805
- }
2806
- if (roleState.abortController) {
2807
- roleState.abortController.abort();
2808
- }
2809
- console.log(`[Crew] Stopping role: ${roleName}`);
2810
- }
2811
- session.roleStates.clear();
2812
-
2813
- sendCrewOutput(session, 'system', 'system', {
2814
- type: 'assistant',
2815
- message: { role: 'assistant', content: [{ type: 'text', text: 'Session 已终止' }] }
2816
- });
2817
- sendStatusUpdate(session);
2818
-
2819
- // 清理 git worktrees
2820
- await cleanupWorktrees(session.projectDir);
2821
-
2822
- // 显式 await 保存,确保 session.json 落盘后再从内存中移除
2823
- await saveSessionMeta(session);
2824
- await upsertCrewIndex(session);
2825
-
2826
- // 从活跃 sessions 中移除
2827
- crewSessions.delete(session.id);
2828
- console.log(`[Crew] Session ${session.id} stopped`);
2829
- }
2830
-
2831
- /**
2832
- * 清空 session:保留角色配置,重置所有对话
2833
- * 每个角色创建全新的 Claude conversation
2834
- */
2835
- async function clearSession(session) {
2836
- // 1. Abort 所有运行中的 queries
2837
- for (const [roleName, roleState] of session.roleStates) {
2838
- if (roleState.abortController) {
2839
- roleState.abortController.abort();
2840
- }
2841
- console.log(`[Crew] Clearing role: ${roleName}`);
2842
- }
2843
- session.roleStates.clear();
2844
-
2845
- // 2. 删除所有角色的 savedSessionId(强制新建 conversation)
2846
- for (const [roleName] of session.roles) {
2847
- await clearRoleSessionId(session.sharedDir, roleName);
2848
- }
2849
-
2850
- // 3. 清空消息历史
2851
- session.messageHistory = [];
2852
- session.uiMessages = [];
2853
- session.humanMessageQueue = [];
2854
- session.waitingHumanContext = null;
2855
- session.pendingRoutes = [];
2856
-
2857
- // 4. 重置计数
2858
- session.round = 0;
2859
-
2860
- // 5. 清空磁盘上的 messages.json 和所有分片
2861
- const messagesPath = join(session.sharedDir, 'messages.json');
2862
- await fs.writeFile(messagesPath, '[]').catch(() => {});
2863
- await cleanupMessageShards(session.sharedDir);
2864
-
2865
- // 6. 恢复运行状态
2866
- session.status = 'running';
2867
-
2868
- // 7. 通知前端
2869
- sendCrewMessage({
2870
- type: 'crew_session_cleared',
2871
- sessionId: session.id
2872
- });
2873
-
2874
- sendCrewOutput(session, 'system', 'system', {
2875
- type: 'assistant',
2876
- message: { role: 'assistant', content: [{ type: 'text', text: '会话已清空,所有角色将使用全新对话' }] }
2877
- });
2878
- sendStatusUpdate(session);
2879
-
2880
- // 8. 保存 meta
2881
- await saveSessionMeta(session);
2882
-
2883
- console.log(`[Crew] Session ${session.id} cleared`);
2884
- }
2885
-
2886
- // =====================================================================
2887
- // Message Helpers
2888
- // =====================================================================
2889
-
2890
- /**
2891
- * 发送 crew 消息到 server(透传到 Web)
2892
- */
2893
- function sendCrewMessage(msg) {
2894
- if (ctx.sendToServer) {
2895
- ctx.sendToServer(msg);
2896
- }
2897
- }
2898
-
2899
- /**
2900
- * 发送角色输出到 Web
2901
- */
2902
- function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
2903
- const role = session.roles.get(roleName);
2904
- const roleIcon = role?.icon || '';
2905
- const displayName = role?.displayName || roleName;
2906
-
2907
- // 从 roleState 获取当前 task 信息
2908
- const roleState = session.roleStates.get(roleName);
2909
- const taskId = roleState?.currentTask?.taskId || null;
2910
- const taskTitle = roleState?.currentTask?.taskTitle || null;
2911
-
2912
- sendCrewMessage({
2913
- type: 'crew_output',
2914
- sessionId: session.id,
2915
- role: roleName,
2916
- roleIcon,
2917
- roleName: displayName,
2918
- outputType, // 'text' | 'tool_use' | 'tool_result' | 'route' | 'system'
2919
- data: rawMessage,
2920
- taskId,
2921
- taskTitle,
2922
- ...extra
2923
- });
2924
-
2925
- // ★ 累积 feature 到持久化列表
2926
- if (taskId && taskTitle && !session.features.has(taskId)) {
2927
- session.features.set(taskId, { taskId, taskTitle, createdAt: Date.now() });
2928
- }
2929
-
2930
- // ★ 记录精简 UI 消息用于恢复(跳过 tool_use/tool_result,只记录可见内容)
2931
- if (outputType === 'text') {
2932
- const content = rawMessage?.message?.content;
2933
- let text = '';
2934
- if (typeof content === 'string') {
2935
- text = content;
2936
- } else if (Array.isArray(content)) {
2937
- text = content.filter(b => b.type === 'text').map(b => b.text).join('');
2938
- }
2939
- if (!text) return;
2940
- // ★ 修复2: 反向搜索该角色最后一条 _streaming 消息
2941
- let found = false;
2942
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
2943
- const msg = session.uiMessages[i];
2944
- if (msg.role === roleName && msg.type === 'text' && msg._streaming) {
2945
- msg.content += text;
2946
- found = true;
2947
- break;
2948
- }
2949
- }
2950
- if (!found) {
2951
- session.uiMessages.push({
2952
- role: roleName, roleIcon, roleName: displayName,
2953
- type: 'text', content: text, _streaming: true,
2954
- taskId, taskTitle,
2955
- timestamp: Date.now()
2956
- });
2957
- }
2958
- } else if (outputType === 'route') {
2959
- // 结束该角色前一条 streaming
2960
- endRoleStreaming(session, roleName);
2961
- session.uiMessages.push({
2962
- role: roleName, roleIcon, roleName: displayName,
2963
- type: 'route', routeTo: extra.routeTo,
2964
- routeSummary: extra.routeSummary || '',
2965
- content: `→ @${extra.routeTo} ${extra.routeSummary || ''}`,
2966
- taskId, taskTitle,
2967
- timestamp: Date.now()
2968
- });
2969
- } else if (outputType === 'system') {
2970
- const content = rawMessage?.message?.content;
2971
- let text = '';
2972
- if (typeof content === 'string') {
2973
- text = content;
2974
- } else if (Array.isArray(content)) {
2975
- text = content.filter(b => b.type === 'text').map(b => b.text).join('');
2976
- }
2977
- if (!text) return;
2978
- session.uiMessages.push({
2979
- role: roleName, roleIcon, roleName: displayName,
2980
- type: 'system', content: text,
2981
- timestamp: Date.now()
2982
- });
2983
- } else if (outputType === 'tool_use') {
2984
- // 结束该角色前一条 streaming
2985
- endRoleStreaming(session, roleName);
2986
- const content = rawMessage?.message?.content;
2987
- if (Array.isArray(content)) {
2988
- for (const block of content) {
2989
- if (block.type === 'tool_use') {
2990
- // Save trimmed toolInput for restore — only key fields, skip large content
2991
- // TodoWrite: preserve full input (todos array is small and needed for sticky banner)
2992
- const input = block.input || {};
2993
- let savedInput;
2994
- if (block.name === 'TodoWrite') {
2995
- savedInput = input;
2996
- } else {
2997
- const trimmedInput = {};
2998
- if (input.file_path) trimmedInput.file_path = input.file_path;
2999
- if (input.command) trimmedInput.command = input.command.substring(0, 200);
3000
- if (input.pattern) trimmedInput.pattern = input.pattern;
3001
- if (input.old_string) trimmedInput.old_string = input.old_string.substring(0, 100);
3002
- if (input.new_string) trimmedInput.new_string = input.new_string.substring(0, 100);
3003
- if (input.url) trimmedInput.url = input.url;
3004
- if (input.query) trimmedInput.query = input.query;
3005
- savedInput = Object.keys(trimmedInput).length > 0 ? trimmedInput : null;
3006
- }
3007
- session.uiMessages.push({
3008
- role: roleName, roleIcon, roleName: displayName,
3009
- type: 'tool',
3010
- toolName: block.name,
3011
- toolId: block.id,
3012
- toolInput: savedInput,
3013
- content: `${block.name} ${block.input?.file_path || block.input?.command?.substring(0, 60) || ''}`,
3014
- hasResult: false,
3015
- taskId, taskTitle,
3016
- timestamp: Date.now()
3017
- });
3018
- }
3019
- }
3020
- }
3021
- } else if (outputType === 'tool_result') {
3022
- // 标记对应 tool 的 hasResult
3023
- const toolId = rawMessage?.message?.tool_use_id;
3024
- if (toolId) {
3025
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
3026
- if (session.uiMessages[i].type === 'tool' && session.uiMessages[i].toolId === toolId) {
3027
- session.uiMessages[i].hasResult = true;
3028
- break;
3029
- }
3030
- }
3031
- }
3032
- // Check for image blocks in tool_result content
3033
- const resultContent = rawMessage?.message?.content;
3034
- if (Array.isArray(resultContent)) {
3035
- for (const item of resultContent) {
3036
- if (item.type === 'image' && item.source?.type === 'base64') {
3037
- sendCrewMessage({
3038
- type: 'crew_image',
3039
- sessionId: session.id,
3040
- role: roleName,
3041
- roleIcon,
3042
- roleName: displayName,
3043
- toolId: toolId || '',
3044
- mimeType: item.source.media_type,
3045
- data: item.source.data,
3046
- taskId, taskTitle
3047
- });
3048
- session.uiMessages.push({
3049
- role: roleName, roleIcon, roleName: displayName,
3050
- type: 'image', toolId: toolId || '',
3051
- mimeType: item.source.media_type,
3052
- taskId, taskTitle,
3053
- timestamp: Date.now()
3054
- });
3055
- }
3056
- }
3057
- }
3058
- }
3059
- // tool 只保存精简信息(toolName + 摘要),不存完整 toolInput/toolResult
3060
- }
3061
-
3062
- /**
3063
- * 发送 session 状态更新
3064
- */
3065
- function sendStatusUpdate(session) {
3066
- const currentRole = findActiveRole(session);
3067
-
3068
- sendCrewMessage({
3069
- type: 'crew_status',
3070
- sessionId: session.id,
3071
- status: session.status,
3072
- currentRole,
3073
- round: session.round,
3074
- costUsd: session.costUsd,
3075
- totalInputTokens: session.totalInputTokens,
3076
- totalOutputTokens: session.totalOutputTokens,
3077
- roles: Array.from(session.roles.values()).map(r => ({
3078
- name: r.name,
3079
- displayName: r.displayName,
3080
- icon: r.icon,
3081
- description: r.description,
3082
- isDecisionMaker: r.isDecisionMaker || false,
3083
- model: r.model,
3084
- roleType: r.roleType,
3085
- groupIndex: r.groupIndex
3086
- })),
3087
- activeRoles: Array.from(session.roleStates.entries())
3088
- .filter(([, s]) => s.turnActive)
3089
- .map(([name]) => name),
3090
- currentToolByRole: Object.fromEntries(
3091
- Array.from(session.roleStates.entries())
3092
- .filter(([, s]) => s.turnActive && s.currentTool)
3093
- .map(([name, s]) => [name, s.currentTool])
3094
- ),
3095
- features: Array.from(session.features.values()),
3096
- initProgress: session.initProgress || null
3097
- });
3098
-
3099
- // 异步更新持久化
3100
- upsertCrewIndex(session).catch(e => console.warn('[Crew] Failed to update index:', e.message));
3101
- saveSessionMeta(session).catch(e => console.warn('[Crew] Failed to save session meta:', e.message));
3102
- }
14
+ * 本文件为入口模块,聚合并重新导出各子模块的公共 API。
15
+ */
16
+
17
+ // Session 核心
18
+ export {
19
+ crewSessions,
20
+ createCrewSession,
21
+ resumeCrewSession,
22
+ handleListCrewSessions,
23
+ handleCheckCrewExists,
24
+ handleDeleteCrewDir,
25
+ handleUpdateCrewSession
26
+ } from './crew/session.js';
27
+
28
+ // 持久化
29
+ export {
30
+ loadCrewIndex,
31
+ removeFromCrewIndex,
32
+ handleLoadCrewHistory
33
+ } from './crew/persistence.js';
34
+
35
+ // 控制操作
36
+ export { handleCrewControl } from './crew/control.js';
37
+
38
+ // 人工交互
39
+ export { handleCrewHumanInput } from './crew/human-interaction.js';
40
+
41
+ // 角色管理
42
+ export {
43
+ addRoleToSession,
44
+ removeRoleFromSession
45
+ } from './crew/role-management.js';