@yeaft/webchat-agent 0.0.99 → 0.0.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crew.js +496 -115
- package/package.json +1 -1
package/crew.js
CHANGED
|
@@ -16,8 +16,12 @@ import { query, Stream } from './sdk/index.js';
|
|
|
16
16
|
import { promises as fs } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { homedir } from 'os';
|
|
19
|
+
import { execFile as execFileCb } from 'child_process';
|
|
20
|
+
import { promisify } from 'util';
|
|
19
21
|
import ctx from './context.js';
|
|
20
22
|
|
|
23
|
+
const execFile = promisify(execFileCb);
|
|
24
|
+
|
|
21
25
|
// =====================================================================
|
|
22
26
|
// Data Structures
|
|
23
27
|
// =====================================================================
|
|
@@ -28,6 +32,188 @@ const crewSessions = new Map();
|
|
|
28
32
|
// 导出供 connection.js / conversation.js 使用
|
|
29
33
|
export { crewSessions };
|
|
30
34
|
|
|
35
|
+
// =====================================================================
|
|
36
|
+
// Role Multi-Instance Expansion
|
|
37
|
+
// =====================================================================
|
|
38
|
+
|
|
39
|
+
// 短前缀映射:用于 count > 1 时生成实例名
|
|
40
|
+
const SHORT_PREFIX = {
|
|
41
|
+
developer: 'dev',
|
|
42
|
+
tester: 'test',
|
|
43
|
+
reviewer: 'rev'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 只有执行者角色支持多实例
|
|
47
|
+
const EXPANDABLE_ROLES = new Set(['developer', 'tester', 'reviewer']);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 展开角色列表:count > 1 的执行者角色展开为多个实例
|
|
51
|
+
* count === 1 或管理者角色保持原样(向后兼容)
|
|
52
|
+
*
|
|
53
|
+
* @param {Array} roles - 原始角色配置
|
|
54
|
+
* @returns {Array} 展开后的角色列表
|
|
55
|
+
*/
|
|
56
|
+
function expandRoles(roles) {
|
|
57
|
+
// 找到 developer 的 count,reviewer/tester 自动跟随
|
|
58
|
+
const devRole = roles.find(r => r.name === 'developer');
|
|
59
|
+
const devCount = devRole?.count > 1 ? devRole.count : 1;
|
|
60
|
+
|
|
61
|
+
const expanded = [];
|
|
62
|
+
for (const role of roles) {
|
|
63
|
+
const isExpandable = EXPANDABLE_ROLES.has(role.name);
|
|
64
|
+
// reviewer/tester 跟随 developer 的 count
|
|
65
|
+
const count = isExpandable ? devCount : 1;
|
|
66
|
+
|
|
67
|
+
if (count <= 1 || !isExpandable) {
|
|
68
|
+
// 单实例:保持原样,添加元数据
|
|
69
|
+
expanded.push({
|
|
70
|
+
...role,
|
|
71
|
+
roleType: role.name,
|
|
72
|
+
groupIndex: 0
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
// 多实例展开
|
|
76
|
+
const prefix = SHORT_PREFIX[role.name] || role.name;
|
|
77
|
+
for (let i = 1; i <= count; i++) {
|
|
78
|
+
expanded.push({
|
|
79
|
+
...role,
|
|
80
|
+
name: `${prefix}-${i}`,
|
|
81
|
+
displayName: `${role.displayName}-${i}`,
|
|
82
|
+
roleType: role.name,
|
|
83
|
+
groupIndex: i,
|
|
84
|
+
count: undefined // 展开后不再需要 count
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return expanded;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =====================================================================
|
|
93
|
+
// Git Worktree Management
|
|
94
|
+
// =====================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 为多实例开发组创建 git worktree
|
|
98
|
+
* 每个 groupIndex 对应一个 worktree,同组的 dev/rev/test 共享
|
|
99
|
+
* count=1 时不创建(向后兼容)
|
|
100
|
+
*
|
|
101
|
+
* @param {string} projectDir - 主项目目录
|
|
102
|
+
* @param {Array} roles - 展开后的角色列表
|
|
103
|
+
* @returns {Map<number, string>} groupIndex → worktree 路径
|
|
104
|
+
*/
|
|
105
|
+
async function initWorktrees(projectDir, roles) {
|
|
106
|
+
const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
|
|
107
|
+
if (groupIndices.length === 0) return new Map();
|
|
108
|
+
|
|
109
|
+
const worktreeBase = join(projectDir, '.worktrees');
|
|
110
|
+
await fs.mkdir(worktreeBase, { recursive: true });
|
|
111
|
+
|
|
112
|
+
// 获取 git 已知的 worktree 列表
|
|
113
|
+
let knownWorktrees = new Set();
|
|
114
|
+
try {
|
|
115
|
+
const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], { cwd: projectDir });
|
|
116
|
+
for (const line of stdout.split('\n')) {
|
|
117
|
+
if (line.startsWith('worktree ')) {
|
|
118
|
+
knownWorktrees.add(line.slice('worktree '.length).trim());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// git worktree list 失败,视为空集
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const worktreeMap = new Map();
|
|
126
|
+
|
|
127
|
+
for (const idx of groupIndices) {
|
|
128
|
+
const wtDir = join(worktreeBase, `dev-${idx}`);
|
|
129
|
+
const branch = `crew/dev-${idx}`;
|
|
130
|
+
|
|
131
|
+
// 检查目录是否存在
|
|
132
|
+
let dirExists = false;
|
|
133
|
+
try {
|
|
134
|
+
await fs.access(wtDir);
|
|
135
|
+
dirExists = true;
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
if (dirExists) {
|
|
139
|
+
if (knownWorktrees.has(wtDir)) {
|
|
140
|
+
// 目录存在且 git 记录中也有,直接复用
|
|
141
|
+
console.log(`[Crew] Worktree already exists: ${wtDir}`);
|
|
142
|
+
worktreeMap.set(idx, wtDir);
|
|
143
|
+
continue;
|
|
144
|
+
} else {
|
|
145
|
+
// 孤立目录:目录存在但 git 不认识,先删除再重建
|
|
146
|
+
console.warn(`[Crew] Orphaned worktree dir, removing: ${wtDir}`);
|
|
147
|
+
await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// 创建分支(如果不存在)
|
|
153
|
+
try {
|
|
154
|
+
await execFile('git', ['branch', branch], { cwd: projectDir });
|
|
155
|
+
} catch {
|
|
156
|
+
// 分支已存在,忽略
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 创建 worktree
|
|
160
|
+
await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: projectDir });
|
|
161
|
+
console.log(`[Crew] Created worktree: ${wtDir} on branch ${branch}`);
|
|
162
|
+
worktreeMap.set(idx, wtDir);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error(`[Crew] Failed to create worktree for group ${idx}:`, e.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return worktreeMap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 清理 session 的 git worktrees
|
|
173
|
+
* @param {string} projectDir - 主项目目录
|
|
174
|
+
*/
|
|
175
|
+
async function cleanupWorktrees(projectDir) {
|
|
176
|
+
const worktreeBase = join(projectDir, '.worktrees');
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(worktreeBase);
|
|
180
|
+
} catch {
|
|
181
|
+
return; // .worktrees 目录不存在,无需清理
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const entries = await fs.readdir(worktreeBase);
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (!entry.startsWith('dev-')) continue;
|
|
188
|
+
const wtDir = join(worktreeBase, entry);
|
|
189
|
+
const branch = `crew/${entry}`;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: projectDir });
|
|
193
|
+
console.log(`[Crew] Removed worktree: ${wtDir}`);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.warn(`[Crew] Failed to remove worktree ${wtDir}:`, e.message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await execFile('git', ['branch', '-D', branch], { cwd: projectDir });
|
|
200
|
+
console.log(`[Crew] Deleted branch: ${branch}`);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.warn(`[Crew] Failed to delete branch ${branch}:`, e.message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 尝试删除 .worktrees 目录(如果已空)
|
|
207
|
+
try {
|
|
208
|
+
await fs.rmdir(worktreeBase);
|
|
209
|
+
} catch {
|
|
210
|
+
// 目录不空或其他原因,忽略
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error(`[Crew] Failed to cleanup worktrees:`, e.message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
31
217
|
// =====================================================================
|
|
32
218
|
// Crew Session Index (~/.claude/crew-sessions.json)
|
|
33
219
|
// =====================================================================
|
|
@@ -247,7 +433,7 @@ export async function resumeCrewSession(msg) {
|
|
|
247
433
|
uiMessages: [], // will be loaded from messages.json
|
|
248
434
|
humanMessageQueue: [],
|
|
249
435
|
waitingHumanContext: null,
|
|
250
|
-
|
|
436
|
+
pendingRoutes: [],
|
|
251
437
|
userId: userId || meta.userId,
|
|
252
438
|
username: username || meta.username,
|
|
253
439
|
createdAt: meta.createdAt || Date.now()
|
|
@@ -297,12 +483,15 @@ export async function createCrewSession(msg) {
|
|
|
297
483
|
projectDir,
|
|
298
484
|
sharedDir: sharedDirRel,
|
|
299
485
|
goal,
|
|
300
|
-
roles = [], // [{ name, displayName, icon, description, claudeMd, model, budget, isDecisionMaker }]
|
|
486
|
+
roles: rawRoles = [], // [{ name, displayName, icon, description, claudeMd, model, budget, isDecisionMaker, count }]
|
|
301
487
|
maxRounds = 20,
|
|
302
488
|
userId,
|
|
303
489
|
username
|
|
304
490
|
} = msg;
|
|
305
491
|
|
|
492
|
+
// 展开多实例角色(count > 1 的执行者角色)
|
|
493
|
+
const roles = expandRoles(rawRoles);
|
|
494
|
+
|
|
306
495
|
// 解析共享目录(相对路径相对于 projectDir)
|
|
307
496
|
const sharedDir = sharedDirRel?.startsWith('/')
|
|
308
497
|
? sharedDirRel
|
|
@@ -311,6 +500,17 @@ export async function createCrewSession(msg) {
|
|
|
311
500
|
// 初始化共享区
|
|
312
501
|
await initSharedDir(sharedDir, goal, roles, projectDir);
|
|
313
502
|
|
|
503
|
+
// 初始化 git worktrees(仅多实例时)
|
|
504
|
+
const worktreeMap = await initWorktrees(projectDir, roles);
|
|
505
|
+
// 回填 workDir:同组的 dev-N/rev-N/test-N 共享同一个 worktree
|
|
506
|
+
for (const role of roles) {
|
|
507
|
+
if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex)) {
|
|
508
|
+
role.workDir = worktreeMap.get(role.groupIndex);
|
|
509
|
+
// 重新写入 CLAUDE.md(加入工作目录信息)
|
|
510
|
+
await writeRoleClaudeMd(sharedDir, role);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
314
514
|
// 找到决策者
|
|
315
515
|
const decisionMaker = roles.find(r => r.isDecisionMaker)?.name || roles[0]?.name || null;
|
|
316
516
|
|
|
@@ -332,7 +532,7 @@ export async function createCrewSession(msg) {
|
|
|
332
532
|
uiMessages: [], // 精简的 UI 消息历史(用于恢复时重放)
|
|
333
533
|
humanMessageQueue: [], // 人的消息排队
|
|
334
534
|
waitingHumanContext: null, // { fromRole, reason, message }
|
|
335
|
-
|
|
535
|
+
pendingRoutes: [], // [{ fromRole, route }] — 暂停时未完成的路由
|
|
336
536
|
userId,
|
|
337
537
|
username,
|
|
338
538
|
createdAt: Date.now()
|
|
@@ -353,7 +553,9 @@ export async function createCrewSession(msg) {
|
|
|
353
553
|
icon: r.icon,
|
|
354
554
|
description: r.description,
|
|
355
555
|
isDecisionMaker: r.isDecisionMaker || false,
|
|
356
|
-
model: r.model
|
|
556
|
+
model: r.model,
|
|
557
|
+
roleType: r.roleType,
|
|
558
|
+
groupIndex: r.groupIndex
|
|
357
559
|
})),
|
|
358
560
|
decisionMaker,
|
|
359
561
|
maxRounds,
|
|
@@ -395,51 +597,58 @@ export async function addRoleToSession(msg) {
|
|
|
395
597
|
return;
|
|
396
598
|
}
|
|
397
599
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
600
|
+
// 展开多实例(count > 1 时)
|
|
601
|
+
const rolesToAdd = expandRoles([role]);
|
|
402
602
|
|
|
403
|
-
|
|
404
|
-
|
|
603
|
+
for (const r of rolesToAdd) {
|
|
604
|
+
if (session.roles.has(r.name)) {
|
|
605
|
+
console.warn(`[Crew] Role already exists: ${r.name}`);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
405
608
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
session.decisionMaker = role.name;
|
|
409
|
-
}
|
|
410
|
-
// 如果没有任何决策者,第一个角色作为决策者
|
|
411
|
-
if (!session.decisionMaker) {
|
|
412
|
-
session.decisionMaker = role.name;
|
|
413
|
-
}
|
|
609
|
+
// 添加角色到 session
|
|
610
|
+
session.roles.set(r.name, r);
|
|
414
611
|
|
|
415
|
-
|
|
416
|
-
|
|
612
|
+
// 如果还没有决策者且新角色是决策者,更新
|
|
613
|
+
if (r.isDecisionMaker) {
|
|
614
|
+
session.decisionMaker = r.name;
|
|
615
|
+
}
|
|
616
|
+
// 如果没有任何决策者,第一个角色作为决策者
|
|
617
|
+
if (!session.decisionMaker) {
|
|
618
|
+
session.decisionMaker = r.name;
|
|
619
|
+
}
|
|
417
620
|
|
|
418
|
-
|
|
419
|
-
|
|
621
|
+
// 初始化角色目录(CLAUDE.md + memory.md)
|
|
622
|
+
await initRoleDir(session.sharedDir, r);
|
|
420
623
|
|
|
421
|
-
|
|
624
|
+
console.log(`[Crew] Role added: ${r.name} (${r.displayName}) to session ${sessionId}`);
|
|
422
625
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
626
|
+
// 通知 Web 端
|
|
627
|
+
sendCrewMessage({
|
|
628
|
+
type: 'crew_role_added',
|
|
629
|
+
sessionId,
|
|
630
|
+
role: {
|
|
631
|
+
name: r.name,
|
|
632
|
+
displayName: r.displayName,
|
|
633
|
+
icon: r.icon,
|
|
634
|
+
description: r.description,
|
|
635
|
+
isDecisionMaker: r.isDecisionMaker || false,
|
|
636
|
+
model: r.model,
|
|
637
|
+
roleType: r.roleType,
|
|
638
|
+
groupIndex: r.groupIndex
|
|
639
|
+
},
|
|
640
|
+
decisionMaker: session.decisionMaker
|
|
641
|
+
});
|
|
437
642
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
643
|
+
// 发送系统消息
|
|
644
|
+
sendCrewOutput(session, 'system', 'system', {
|
|
645
|
+
type: 'assistant',
|
|
646
|
+
message: { role: 'assistant', content: [{ type: 'text', text: `${r.icon} ${r.displayName} 加入了群聊` }] }
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 更新共享 CLAUDE.md(增量添加新角色信息)
|
|
651
|
+
await updateSharedClaudeMd(session);
|
|
443
652
|
|
|
444
653
|
sendStatusUpdate(session);
|
|
445
654
|
}
|
|
@@ -587,9 +796,20 @@ _团队共同维护,记录重要的共识、决策和信息。_
|
|
|
587
796
|
async function writeRoleClaudeMd(sharedDir, role) {
|
|
588
797
|
const roleDir = join(sharedDir, 'roles', role.name);
|
|
589
798
|
|
|
590
|
-
|
|
799
|
+
let claudeMd = `# 角色: ${role.icon} ${role.displayName}
|
|
591
800
|
${role.claudeMd || role.description}
|
|
801
|
+
`;
|
|
802
|
+
|
|
803
|
+
// 有独立 worktree 的角色,覆盖代码工作目录
|
|
804
|
+
if (role.workDir) {
|
|
805
|
+
claudeMd += `
|
|
806
|
+
# 代码工作目录
|
|
807
|
+
${role.workDir}
|
|
808
|
+
所有代码操作请使用此路径。不要使用项目主目录。
|
|
809
|
+
`;
|
|
810
|
+
}
|
|
592
811
|
|
|
812
|
+
claudeMd += `
|
|
593
813
|
# 个人记忆
|
|
594
814
|
_在这里记录重要的信息、决策、进展和待办事项。_
|
|
595
815
|
`;
|
|
@@ -704,7 +924,18 @@ async function createRoleQuery(session, roleName) {
|
|
|
704
924
|
*/
|
|
705
925
|
function buildRoleSystemPrompt(role, session) {
|
|
706
926
|
const allRoles = Array.from(session.roles.values());
|
|
707
|
-
|
|
927
|
+
|
|
928
|
+
// 按组裁剪路由目标:
|
|
929
|
+
// - 有 groupIndex > 0 的执行者只看到同组成员 + 管理者(PM/architect/designer)
|
|
930
|
+
// - 管理者(groupIndex === 0)看到所有角色
|
|
931
|
+
let routeTargets;
|
|
932
|
+
if (role.groupIndex > 0) {
|
|
933
|
+
routeTargets = allRoles.filter(r =>
|
|
934
|
+
r.name !== role.name && (r.groupIndex === role.groupIndex || r.groupIndex === 0)
|
|
935
|
+
);
|
|
936
|
+
} else {
|
|
937
|
+
routeTargets = allRoles.filter(r => r.name !== role.name);
|
|
938
|
+
}
|
|
708
939
|
|
|
709
940
|
let prompt = `# 团队协作
|
|
710
941
|
你正在一个 AI 团队中工作。${session.goal ? `项目目标是: ${session.goal}` : '等待用户提出任务或问题。'}
|
|
@@ -712,7 +943,10 @@ function buildRoleSystemPrompt(role, session) {
|
|
|
712
943
|
团队成员:
|
|
713
944
|
${allRoles.map(r => `- ${r.icon} ${r.displayName}: ${r.description}${r.isDecisionMaker ? ' (决策者)' : ''}`).join('\n')}`;
|
|
714
945
|
|
|
715
|
-
|
|
946
|
+
const hasMultiInstance = allRoles.some(r => r.groupIndex > 0);
|
|
947
|
+
|
|
948
|
+
if (routeTargets.length > 0) {
|
|
949
|
+
const multiRouteAllowed = role.isDecisionMaker && hasMultiInstance;
|
|
716
950
|
prompt += `\n\n# 路由规则
|
|
717
951
|
当你完成当前任务并需要将结果传递给其他角色时,在你的回复最末尾添加一个 ROUTE 块:
|
|
718
952
|
|
|
@@ -724,14 +958,14 @@ summary: <简要说明要传递什么>
|
|
|
724
958
|
\`\`\`
|
|
725
959
|
|
|
726
960
|
可用的路由目标:
|
|
727
|
-
${
|
|
961
|
+
${routeTargets.map(r => `- ${r.name}: ${r.icon} ${r.displayName} — ${r.description}`).join('\n')}
|
|
728
962
|
- human: 人工(只在决策者也无法决定时使用)
|
|
729
963
|
|
|
730
964
|
注意:
|
|
731
965
|
- 如果你的工作还没完成,不需要添加 ROUTE 块
|
|
732
966
|
- 如果你遇到不确定的问题,@ 决策者 "${session.decisionMaker}",而不是直接 @ human
|
|
733
967
|
- 如果你是决策者且遇到需要人类判断的问题,才 @ human
|
|
734
|
-
- 每次回复最多只能有一个 ROUTE 块
|
|
968
|
+
${multiRouteAllowed ? '- 决策者可以一次发多个 ROUTE 块来并行分配任务' : '- 每次回复最多只能有一个 ROUTE 块'}
|
|
735
969
|
- ROUTE 块必须在回复的最末尾
|
|
736
970
|
- 当你的任务已完成且不需要其他角色继续时,ROUTE 回决策者 "${session.decisionMaker}" 做总结
|
|
737
971
|
- 在正文中可用 @角色name 提及某个角色(如 @developer),但这不会触发路由,仅供阅读`;
|
|
@@ -739,14 +973,63 @@ ${otherRoles.map(r => `- ${r.name}: ${r.icon} ${r.displayName} — ${r.descripti
|
|
|
739
973
|
|
|
740
974
|
// 决策者额外 prompt
|
|
741
975
|
if (role.isDecisionMaker) {
|
|
976
|
+
|
|
742
977
|
prompt += `\n\n# 决策者职责
|
|
743
978
|
你是团队的决策者。其他角色遇到不确定的情况会请求你的决策。
|
|
744
979
|
- 如果你有足够的信息做出决策,直接决定并 @相关角色执行
|
|
745
980
|
- 如果你需要更多信息,@具体角色请求补充
|
|
746
981
|
- 如果问题超出你的能力范围或需要业务判断,@human 请人类决定
|
|
747
982
|
- 你可以随时审查其他角色的工作并给出反馈
|
|
748
|
-
- PM 拥有 commit + push + tag 的自主权。只要修改没有大的 regression 影响(测试全通过),PM 可以自行决定 commit、push 和 tag
|
|
983
|
+
- PM 拥有 commit + push + tag 的自主权。只要修改没有大的 regression 影响(测试全通过),PM 可以自行决定 commit、push 和 tag,无需等待人工确认。只有当改动会直接影响对话交互逻辑时,才需要人工介入审核。`;
|
|
984
|
+
|
|
985
|
+
// 多实例模式:注入开发组状态和调度规则
|
|
986
|
+
if (hasMultiInstance) {
|
|
987
|
+
// 构建开发组实时状态
|
|
988
|
+
const maxGroup = Math.max(...allRoles.map(r => r.groupIndex));
|
|
989
|
+
const groupLines = [];
|
|
990
|
+
for (let g = 1; g <= maxGroup; g++) {
|
|
991
|
+
const members = allRoles.filter(r => r.groupIndex === g);
|
|
992
|
+
const memberStrs = members.map(r => {
|
|
993
|
+
const state = session.roleStates.get(r.name);
|
|
994
|
+
const busy = state?.turnActive;
|
|
995
|
+
const task = state?.currentTask;
|
|
996
|
+
if (busy && task) return `${r.name}(忙:${task.taskId} ${task.taskTitle})`;
|
|
997
|
+
if (busy) return `${r.name}(忙)`;
|
|
998
|
+
return `${r.name}(空闲)`;
|
|
999
|
+
});
|
|
1000
|
+
groupLines.push(`组${g}: ${memberStrs.join(' ')}`);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
prompt += `\n\n# 执行组状态
|
|
1004
|
+
${groupLines.join(' / ')}
|
|
1005
|
+
|
|
1006
|
+
# 并行任务调度规则
|
|
1007
|
+
你有 ${maxGroup} 个开发组可以并行工作。拆分任务时:
|
|
1008
|
+
1. 每个子任务分配 task-id(如 task-1)和 taskTitle(如 "实现登录页面")
|
|
1009
|
+
2. 优先分配给**空闲**的开发组,避免给忙碌的 dev 发新任务
|
|
1010
|
+
3. 一次可以发**多个 ROUTE 块**来并行分配任务:
|
|
749
1011
|
|
|
1012
|
+
\`\`\`
|
|
1013
|
+
---ROUTE---
|
|
1014
|
+
to: dev-1
|
|
1015
|
+
task: task-1
|
|
1016
|
+
taskTitle: 实现登录页面
|
|
1017
|
+
summary: 请实现登录页面,包括表单验证和API调用
|
|
1018
|
+
---END_ROUTE---
|
|
1019
|
+
|
|
1020
|
+
---ROUTE---
|
|
1021
|
+
to: dev-2
|
|
1022
|
+
task: task-2
|
|
1023
|
+
taskTitle: 实现注册页面
|
|
1024
|
+
summary: 请实现注册页面,包括邮箱验证
|
|
1025
|
+
---END_ROUTE---
|
|
1026
|
+
\`\`\`
|
|
1027
|
+
|
|
1028
|
+
4. 每个 dev 完成后会独立经过 reviewer 和 tester 审核,最后 ROUTE 回你
|
|
1029
|
+
5. 等待**所有子任务完成**后再做汇总报告`;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
prompt += `\n
|
|
750
1033
|
# 工作流终结点
|
|
751
1034
|
团队的工作流有明确的结束条件。当以下任一条件满足时,你应该给出总结并结束当前工作流:
|
|
752
1035
|
1. **代码已提交** - 所有代码修改已经 commit(如需要,可让 developer 执行 git commit)
|
|
@@ -772,6 +1055,35 @@ ${otherRoles.map(r => `- ${r.name}: ${r.icon} ${r.displayName} — ${r.descripti
|
|
|
772
1055
|
- TASKS 块不需要在回复最末尾,可以放在任意位置`;
|
|
773
1056
|
}
|
|
774
1057
|
|
|
1058
|
+
// 执行者角色的组绑定 prompt(count > 1 时)
|
|
1059
|
+
if (role.groupIndex > 0 && role.roleType === 'developer') {
|
|
1060
|
+
const gi = role.groupIndex;
|
|
1061
|
+
const rev = allRoles.find(r => r.roleType === 'reviewer' && r.groupIndex === gi);
|
|
1062
|
+
const test = allRoles.find(r => r.roleType === 'tester' && r.groupIndex === gi);
|
|
1063
|
+
if (rev && test) {
|
|
1064
|
+
prompt += `\n\n# 开发组绑定
|
|
1065
|
+
你属于开发组 ${gi}。你的搭档:
|
|
1066
|
+
- 审查者: ${rev.icon} ${rev.name}
|
|
1067
|
+
- 测试: ${test.icon} ${test.name}
|
|
1068
|
+
|
|
1069
|
+
开发完成后,请同时发两个 ROUTE 块分别给 ${rev.name} 和 ${test.name}:
|
|
1070
|
+
|
|
1071
|
+
\`\`\`
|
|
1072
|
+
---ROUTE---
|
|
1073
|
+
to: ${rev.name}
|
|
1074
|
+
summary: 请审查代码变更
|
|
1075
|
+
---END_ROUTE---
|
|
1076
|
+
|
|
1077
|
+
---ROUTE---
|
|
1078
|
+
to: ${test.name}
|
|
1079
|
+
summary: 请测试功能
|
|
1080
|
+
---END_ROUTE---
|
|
1081
|
+
\`\`\`
|
|
1082
|
+
|
|
1083
|
+
两者会并行工作,各自完成后独立 ROUTE 回 PM。`;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
775
1087
|
return prompt;
|
|
776
1088
|
}
|
|
777
1089
|
|
|
@@ -810,24 +1122,29 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
810
1122
|
}
|
|
811
1123
|
|
|
812
1124
|
if (message.type === 'assistant') {
|
|
813
|
-
// 转发流式输出到 Web
|
|
814
|
-
sendCrewOutput(session, roleName, 'text', message);
|
|
815
|
-
|
|
816
1125
|
// 累积文本用于路由解析
|
|
817
1126
|
const content = message.message?.content;
|
|
818
1127
|
if (content) {
|
|
819
1128
|
if (typeof content === 'string') {
|
|
820
1129
|
roleState.accumulatedText += content;
|
|
1130
|
+
// 转发流式文本到 Web
|
|
1131
|
+
sendCrewOutput(session, roleName, 'text', message);
|
|
821
1132
|
} else if (Array.isArray(content)) {
|
|
1133
|
+
let hasText = false;
|
|
822
1134
|
for (const block of content) {
|
|
823
1135
|
if (block.type === 'text') {
|
|
824
1136
|
roleState.accumulatedText += block.text;
|
|
1137
|
+
hasText = true;
|
|
825
1138
|
} else if (block.type === 'tool_use') {
|
|
826
|
-
//
|
|
1139
|
+
// ★ 修复5: tool_use 时结束该角色前一条 streaming 文本
|
|
1140
|
+
endRoleStreaming(session, roleName);
|
|
827
1141
|
roleState.currentTool = block.name;
|
|
828
1142
|
sendCrewOutput(session, roleName, 'tool_use', message);
|
|
829
1143
|
}
|
|
830
1144
|
}
|
|
1145
|
+
if (hasText) {
|
|
1146
|
+
sendCrewOutput(session, roleName, 'text', message);
|
|
1147
|
+
}
|
|
831
1148
|
}
|
|
832
1149
|
}
|
|
833
1150
|
} else if (message.type === 'user') {
|
|
@@ -838,9 +1155,8 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
838
1155
|
// ★ Turn 完成!
|
|
839
1156
|
console.log(`[Crew] ${roleName} turn completed`);
|
|
840
1157
|
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
if (lastUi && lastUi._streaming) delete lastUi._streaming;
|
|
1158
|
+
// ★ 修复2: 反向搜索该角色最后一条 streaming 消息并结束
|
|
1159
|
+
endRoleStreaming(session, roleName);
|
|
844
1160
|
|
|
845
1161
|
// 更新费用
|
|
846
1162
|
if (message.total_cost_usd) {
|
|
@@ -858,8 +1174,8 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
858
1174
|
.catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
|
|
859
1175
|
}
|
|
860
1176
|
|
|
861
|
-
//
|
|
862
|
-
const
|
|
1177
|
+
// 解析路由(支持多 ROUTE 块)
|
|
1178
|
+
const routes = parseRoutes(roleState.accumulatedText);
|
|
863
1179
|
roleState.accumulatedText = '';
|
|
864
1180
|
roleState.turnActive = false;
|
|
865
1181
|
|
|
@@ -874,11 +1190,30 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
874
1190
|
sendStatusUpdate(session);
|
|
875
1191
|
|
|
876
1192
|
// 执行路由
|
|
877
|
-
if (
|
|
878
|
-
|
|
1193
|
+
if (routes.length > 0) {
|
|
1194
|
+
// ★ 修复1: 多 ROUTE 只增 1 轮(round++ 从 executeRoute 移到这里)
|
|
1195
|
+
session.round++;
|
|
1196
|
+
|
|
1197
|
+
// task 继承:如果路由没有指定 taskId,从当前角色继承
|
|
1198
|
+
const currentTask = roleState.currentTask;
|
|
1199
|
+
for (const route of routes) {
|
|
1200
|
+
if (!route.taskId && currentTask) {
|
|
1201
|
+
route.taskId = currentTask.taskId;
|
|
1202
|
+
route.taskTitle = currentTask.taskTitle;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// 并行执行所有路由(allSettled 保证单个失败不中断其他)
|
|
1207
|
+
const results = await Promise.allSettled(routes.map(route =>
|
|
1208
|
+
executeRoute(session, roleName, route)
|
|
1209
|
+
));
|
|
1210
|
+
for (const r of results) {
|
|
1211
|
+
if (r.status === 'rejected') {
|
|
1212
|
+
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
879
1215
|
} else {
|
|
880
|
-
//
|
|
881
|
-
// 检查是否有人的消息在排队
|
|
1216
|
+
// 没有路由,检查是否有人的消息在排队
|
|
882
1217
|
await processHumanQueue(session);
|
|
883
1218
|
}
|
|
884
1219
|
}
|
|
@@ -886,12 +1221,12 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
886
1221
|
} catch (error) {
|
|
887
1222
|
if (error.name === 'AbortError') {
|
|
888
1223
|
console.log(`[Crew] ${roleName} aborted`);
|
|
889
|
-
// 暂停时:检查已累积的文本中是否有 route,保存为
|
|
1224
|
+
// 暂停时:检查已累积的文本中是否有 route,保存为 pendingRoutes
|
|
890
1225
|
if (session.status === 'paused' && roleState.accumulatedText) {
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
893
|
-
session.
|
|
894
|
-
console.log(`[Crew] Saved pending route from aborted ${roleName}
|
|
1226
|
+
const routes = parseRoutes(roleState.accumulatedText);
|
|
1227
|
+
if (routes.length > 0 && session.pendingRoutes.length === 0) {
|
|
1228
|
+
session.pendingRoutes = routes.map(route => ({ fromRole: roleName, route }));
|
|
1229
|
+
console.log(`[Crew] Saved ${routes.length} pending route(s) from aborted ${roleName}`);
|
|
895
1230
|
}
|
|
896
1231
|
roleState.accumulatedText = '';
|
|
897
1232
|
}
|
|
@@ -917,52 +1252,62 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
917
1252
|
}
|
|
918
1253
|
}
|
|
919
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* 结束指定角色的最后一条 streaming 消息(反向搜索)
|
|
1257
|
+
*/
|
|
1258
|
+
function endRoleStreaming(session, roleName) {
|
|
1259
|
+
for (let i = session.uiMessages.length - 1; i >= 0; i--) {
|
|
1260
|
+
if (session.uiMessages[i].role === roleName && session.uiMessages[i]._streaming) {
|
|
1261
|
+
delete session.uiMessages[i]._streaming;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
920
1267
|
// =====================================================================
|
|
921
1268
|
// Route Parsing & Execution
|
|
922
1269
|
// =====================================================================
|
|
923
1270
|
|
|
924
1271
|
/**
|
|
925
|
-
*
|
|
1272
|
+
* 从累积文本中解析所有 ROUTE 块(支持多 ROUTE + task 字段)
|
|
1273
|
+
* @returns {Array<{ to, summary, taskId, taskTitle }>}
|
|
926
1274
|
*/
|
|
927
|
-
function
|
|
928
|
-
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
return {
|
|
932
|
-
to: match[1].trim().toLowerCase(),
|
|
933
|
-
summary: match[2].trim()
|
|
934
|
-
};
|
|
935
|
-
}
|
|
1275
|
+
function parseRoutes(text) {
|
|
1276
|
+
const routes = [];
|
|
1277
|
+
const regex = /---ROUTE---\s*\n([\s\S]*?)---END_ROUTE---/g;
|
|
1278
|
+
let match;
|
|
936
1279
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (altMatch) {
|
|
940
|
-
const block = altMatch[1];
|
|
1280
|
+
while ((match = regex.exec(text)) !== null) {
|
|
1281
|
+
const block = match[1];
|
|
941
1282
|
const toMatch = block.match(/to:\s*(.+)/i);
|
|
1283
|
+
if (!toMatch) continue;
|
|
1284
|
+
|
|
942
1285
|
const summaryMatch = block.match(/summary:\s*(.+)/i);
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1286
|
+
const taskMatch = block.match(/^task:\s*(.+)/im);
|
|
1287
|
+
const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
|
|
1288
|
+
|
|
1289
|
+
routes.push({
|
|
1290
|
+
to: toMatch[1].trim().toLowerCase(),
|
|
1291
|
+
summary: summaryMatch ? summaryMatch[1].trim() : '',
|
|
1292
|
+
taskId: taskMatch ? taskMatch[1].trim() : null,
|
|
1293
|
+
taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
|
|
1294
|
+
});
|
|
949
1295
|
}
|
|
950
1296
|
|
|
951
|
-
return
|
|
1297
|
+
return routes;
|
|
952
1298
|
}
|
|
953
1299
|
|
|
954
1300
|
/**
|
|
955
1301
|
* 执行路由
|
|
956
1302
|
*/
|
|
957
1303
|
async function executeRoute(session, fromRole, route) {
|
|
958
|
-
const { to, summary } = route;
|
|
1304
|
+
const { to, summary, taskId, taskTitle } = route;
|
|
959
1305
|
|
|
960
|
-
//
|
|
961
|
-
session.round++;
|
|
1306
|
+
// ★ round++ 已移到 processRoleOutput 中(多 ROUTE 只增 1 轮)
|
|
962
1307
|
|
|
963
|
-
// 如果 session
|
|
1308
|
+
// 如果 session 已暂停或停止,保存为 pendingRoutes
|
|
964
1309
|
if (session.status === 'paused' || session.status === 'stopped') {
|
|
965
|
-
session.
|
|
1310
|
+
session.pendingRoutes.push({ fromRole, route });
|
|
966
1311
|
console.log(`[Crew] Session ${session.status}, route saved as pending: ${fromRole} -> ${to}`);
|
|
967
1312
|
return;
|
|
968
1313
|
}
|
|
@@ -991,13 +1336,15 @@ async function executeRoute(session, fromRole, route) {
|
|
|
991
1336
|
|
|
992
1337
|
// 路由到指定角色
|
|
993
1338
|
if (session.roles.has(to)) {
|
|
1339
|
+
// task 信息通过 dispatchToRole 内部设置(createRoleQuery 之后 roleState 才存在)
|
|
1340
|
+
|
|
994
1341
|
// 先检查是否有人的消息在排队
|
|
995
1342
|
if (session.humanMessageQueue.length > 0) {
|
|
996
1343
|
// 人的消息优先
|
|
997
1344
|
await processHumanQueue(session);
|
|
998
1345
|
} else {
|
|
999
1346
|
const taskPrompt = buildRoutePrompt(fromRole, summary, session);
|
|
1000
|
-
await dispatchToRole(session, to, taskPrompt, fromRole);
|
|
1347
|
+
await dispatchToRole(session, to, taskPrompt, fromRole, taskId, taskTitle);
|
|
1001
1348
|
}
|
|
1002
1349
|
} else {
|
|
1003
1350
|
console.warn(`[Crew] Unknown route target: ${to}`);
|
|
@@ -1023,7 +1370,7 @@ function buildRoutePrompt(fromRole, summary, session) {
|
|
|
1023
1370
|
/**
|
|
1024
1371
|
* 向角色发送消息
|
|
1025
1372
|
*/
|
|
1026
|
-
async function dispatchToRole(session, roleName, content, fromSource) {
|
|
1373
|
+
async function dispatchToRole(session, roleName, content, fromSource, taskId, taskTitle) {
|
|
1027
1374
|
if (session.status === 'paused' || session.status === 'stopped') {
|
|
1028
1375
|
console.log(`[Crew] Session ${session.status}, skipping dispatch to ${roleName}`);
|
|
1029
1376
|
return;
|
|
@@ -1036,11 +1383,17 @@ async function dispatchToRole(session, roleName, content, fromSource) {
|
|
|
1036
1383
|
roleState = await createRoleQuery(session, roleName);
|
|
1037
1384
|
}
|
|
1038
1385
|
|
|
1386
|
+
// 设置 task(createRoleQuery 之后 roleState 一定存在)
|
|
1387
|
+
if (taskId) {
|
|
1388
|
+
roleState.currentTask = { taskId, taskTitle };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1039
1391
|
// 记录消息历史
|
|
1040
1392
|
session.messageHistory.push({
|
|
1041
1393
|
from: fromSource,
|
|
1042
1394
|
to: roleName,
|
|
1043
1395
|
content: typeof content === 'string' ? content.substring(0, 200) : '...',
|
|
1396
|
+
taskId: taskId || roleState.currentTask?.taskId || null,
|
|
1044
1397
|
timestamp: Date.now()
|
|
1045
1398
|
});
|
|
1046
1399
|
|
|
@@ -1052,7 +1405,7 @@ async function dispatchToRole(session, roleName, content, fromSource) {
|
|
|
1052
1405
|
message: { role: 'user', content }
|
|
1053
1406
|
});
|
|
1054
1407
|
|
|
1055
|
-
console.log(`[Crew] Dispatched to ${roleName} from ${fromSource}`);
|
|
1408
|
+
console.log(`[Crew] Dispatched to ${roleName} from ${fromSource}${taskId ? ` (task: ${taskId})` : ''}`);
|
|
1056
1409
|
}
|
|
1057
1410
|
|
|
1058
1411
|
// =====================================================================
|
|
@@ -1134,10 +1487,15 @@ export async function handleCrewHumanInput(msg) {
|
|
|
1134
1487
|
*/
|
|
1135
1488
|
async function processHumanQueue(session) {
|
|
1136
1489
|
if (session.humanMessageQueue.length === 0) return;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1490
|
+
if (session._processingHumanQueue) return;
|
|
1491
|
+
session._processingHumanQueue = true;
|
|
1492
|
+
try {
|
|
1493
|
+
const msg = session.humanMessageQueue.shift();
|
|
1494
|
+
const humanPrompt = `人工消息:\n${msg.content}`;
|
|
1495
|
+
await dispatchToRole(session, msg.target, humanPrompt, 'human');
|
|
1496
|
+
} finally {
|
|
1497
|
+
session._processingHumanQueue = false;
|
|
1498
|
+
}
|
|
1141
1499
|
}
|
|
1142
1500
|
|
|
1143
1501
|
/**
|
|
@@ -1217,7 +1575,7 @@ async function pauseAll(session) {
|
|
|
1217
1575
|
|
|
1218
1576
|
/**
|
|
1219
1577
|
* 恢复 session
|
|
1220
|
-
* 重新执行被暂停时保存的
|
|
1578
|
+
* 重新执行被暂停时保存的 pendingRoutes
|
|
1221
1579
|
*/
|
|
1222
1580
|
async function resumeSession(session) {
|
|
1223
1581
|
if (session.status !== 'paused') return;
|
|
@@ -1231,16 +1589,23 @@ async function resumeSession(session) {
|
|
|
1231
1589
|
});
|
|
1232
1590
|
sendStatusUpdate(session);
|
|
1233
1591
|
|
|
1234
|
-
//
|
|
1235
|
-
if (session.
|
|
1236
|
-
const
|
|
1237
|
-
session.
|
|
1238
|
-
console.log(`[Crew] Replaying
|
|
1239
|
-
await
|
|
1592
|
+
// 恢复被中断的路由(可能有多条)
|
|
1593
|
+
if (session.pendingRoutes.length > 0) {
|
|
1594
|
+
const pending = session.pendingRoutes.slice();
|
|
1595
|
+
session.pendingRoutes = [];
|
|
1596
|
+
console.log(`[Crew] Replaying ${pending.length} pending route(s)`);
|
|
1597
|
+
const results = await Promise.allSettled(pending.map(({ fromRole, route }) =>
|
|
1598
|
+
executeRoute(session, fromRole, route)
|
|
1599
|
+
));
|
|
1600
|
+
for (const r of results) {
|
|
1601
|
+
if (r.status === 'rejected') {
|
|
1602
|
+
console.warn(`[Crew] Pending route replay failed:`, r.reason);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1240
1605
|
return;
|
|
1241
1606
|
}
|
|
1242
1607
|
|
|
1243
|
-
// 没有
|
|
1608
|
+
// 没有 pendingRoutes,检查排队的人的消息
|
|
1244
1609
|
await processHumanQueue(session);
|
|
1245
1610
|
}
|
|
1246
1611
|
|
|
@@ -1297,6 +1662,9 @@ async function stopAll(session) {
|
|
|
1297
1662
|
});
|
|
1298
1663
|
sendStatusUpdate(session);
|
|
1299
1664
|
|
|
1665
|
+
// 清理 git worktrees
|
|
1666
|
+
await cleanupWorktrees(session.projectDir);
|
|
1667
|
+
|
|
1300
1668
|
// 从活跃 sessions 中移除
|
|
1301
1669
|
crewSessions.delete(session.id);
|
|
1302
1670
|
console.log(`[Crew] Session ${session.id} stopped`);
|
|
@@ -1323,6 +1691,11 @@ function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
|
|
|
1323
1691
|
const roleIcon = role?.icon || (roleName === 'human' ? 'H' : roleName === 'system' ? 'S' : 'A');
|
|
1324
1692
|
const displayName = role?.displayName || roleName;
|
|
1325
1693
|
|
|
1694
|
+
// 从 roleState 获取当前 task 信息
|
|
1695
|
+
const roleState = session.roleStates.get(roleName);
|
|
1696
|
+
const taskId = roleState?.currentTask?.taskId || null;
|
|
1697
|
+
const taskTitle = roleState?.currentTask?.taskTitle || null;
|
|
1698
|
+
|
|
1326
1699
|
sendCrewMessage({
|
|
1327
1700
|
type: 'crew_output',
|
|
1328
1701
|
sessionId: session.id,
|
|
@@ -1331,6 +1704,8 @@ function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
|
|
|
1331
1704
|
roleName: displayName,
|
|
1332
1705
|
outputType, // 'text' | 'tool_use' | 'tool_result' | 'route' | 'system'
|
|
1333
1706
|
data: rawMessage,
|
|
1707
|
+
taskId,
|
|
1708
|
+
taskTitle,
|
|
1334
1709
|
...extra
|
|
1335
1710
|
});
|
|
1336
1711
|
|
|
@@ -1344,21 +1719,27 @@ function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
|
|
|
1344
1719
|
text = content.filter(b => b.type === 'text').map(b => b.text).join('');
|
|
1345
1720
|
}
|
|
1346
1721
|
if (!text) return;
|
|
1347
|
-
//
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1722
|
+
// ★ 修复2: 反向搜索该角色最后一条 _streaming 消息
|
|
1723
|
+
let found = false;
|
|
1724
|
+
for (let i = session.uiMessages.length - 1; i >= 0; i--) {
|
|
1725
|
+
const msg = session.uiMessages[i];
|
|
1726
|
+
if (msg.role === roleName && msg.type === 'text' && msg._streaming) {
|
|
1727
|
+
msg.content += text;
|
|
1728
|
+
found = true;
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (!found) {
|
|
1352
1733
|
session.uiMessages.push({
|
|
1353
1734
|
role: roleName, roleIcon, roleName: displayName,
|
|
1354
1735
|
type: 'text', content: text, _streaming: true,
|
|
1736
|
+
taskId, taskTitle,
|
|
1355
1737
|
timestamp: Date.now()
|
|
1356
1738
|
});
|
|
1357
1739
|
}
|
|
1358
1740
|
} else if (outputType === 'route') {
|
|
1359
|
-
//
|
|
1360
|
-
|
|
1361
|
-
if (last && last._streaming) delete last._streaming;
|
|
1741
|
+
// 结束该角色前一条 streaming
|
|
1742
|
+
endRoleStreaming(session, roleName);
|
|
1362
1743
|
session.uiMessages.push({
|
|
1363
1744
|
role: roleName, roleIcon, roleName: displayName,
|
|
1364
1745
|
type: 'route', routeTo: extra.routeTo,
|