@yeaft/webchat-agent 0.0.233 → 0.0.235
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/connection/buffer.js +87 -0
- package/connection/heartbeat.js +47 -0
- package/connection/index.js +89 -0
- package/connection/message-router.js +271 -0
- package/connection/upgrade-worker-template.js +103 -0
- package/connection/upgrade.js +294 -0
- package/connection.js +14 -777
- package/crew/control.js +364 -0
- package/crew/human-interaction.js +115 -0
- package/crew/persistence.js +287 -0
- package/crew/role-management.js +131 -0
- package/crew/role-output.js +315 -0
- package/crew/role-query.js +309 -0
- package/crew/routing.js +194 -0
- package/crew/session.js +474 -0
- package/crew/shared-dir.js +116 -0
- package/crew/task-files.js +370 -0
- package/crew/ui-messages.js +246 -0
- package/crew/worktree.js +130 -0
- package/package.json +6 -2
- package/service/config.js +133 -0
- package/service/index.js +99 -0
- package/service/linux.js +111 -0
- package/service/macos.js +137 -0
- package/service/windows.js +181 -0
- package/service.js +23 -624
- package/workbench/file-ops.js +436 -0
- package/workbench/file-search.js +66 -0
- package/workbench/git-ops.js +313 -0
- package/workbench/transfer.js +99 -0
- package/workbench/utils.js +41 -0
- package/workbench.js +15 -938
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 角色输出处理
|
|
3
|
+
* processRoleOutput(核心流式输出处理循环)
|
|
4
|
+
*/
|
|
5
|
+
import { sendCrewMessage, sendCrewOutput, sendStatusUpdate, endRoleStreaming } from './ui-messages.js';
|
|
6
|
+
import { saveRoleSessionId, clearRoleSessionId, classifyRoleError, createRoleQuery } from './role-query.js';
|
|
7
|
+
import { parseRoutes, executeRoute, dispatchToRole } from './routing.js';
|
|
8
|
+
import { parseCompletedTasks, updateFeatureIndex, appendChangelog, saveRoleWorkSummary, updateKanban } from './task-files.js';
|
|
9
|
+
|
|
10
|
+
// Context 使用率阈值常量
|
|
11
|
+
const MAX_CONTEXT = 128000; // API max_prompt_tokens 限制
|
|
12
|
+
const CLEAR_THRESHOLD = 0.85; // 85% → 直接 clear + rebuild(不再走 compact)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 处理角色的流式输出
|
|
16
|
+
*/
|
|
17
|
+
export async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
18
|
+
try {
|
|
19
|
+
for await (const message of roleQuery) {
|
|
20
|
+
// 检查 session 是否已停止或暂停
|
|
21
|
+
if (session.status === 'stopped' || session.status === 'paused') break;
|
|
22
|
+
|
|
23
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
24
|
+
roleState.claudeSessionId = message.session_id;
|
|
25
|
+
console.log(`[Crew] ${roleName} session: ${message.session_id}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (message.type === 'assistant') {
|
|
30
|
+
const content = message.message?.content;
|
|
31
|
+
if (content) {
|
|
32
|
+
if (typeof content === 'string') {
|
|
33
|
+
roleState.accumulatedText += content;
|
|
34
|
+
sendCrewOutput(session, roleName, 'text', message);
|
|
35
|
+
} else if (Array.isArray(content)) {
|
|
36
|
+
let hasText = false;
|
|
37
|
+
for (const block of content) {
|
|
38
|
+
if (block.type === 'text') {
|
|
39
|
+
roleState.accumulatedText += block.text;
|
|
40
|
+
hasText = true;
|
|
41
|
+
} else if (block.type === 'tool_use') {
|
|
42
|
+
endRoleStreaming(session, roleName);
|
|
43
|
+
roleState.currentTool = block.name;
|
|
44
|
+
sendCrewOutput(session, roleName, 'tool_use', message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (hasText) {
|
|
48
|
+
sendCrewOutput(session, roleName, 'text', message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else if (message.type === 'user') {
|
|
53
|
+
roleState.currentTool = null;
|
|
54
|
+
sendCrewOutput(session, roleName, 'tool_result', message);
|
|
55
|
+
} else if (message.type === 'result') {
|
|
56
|
+
// Turn 完成
|
|
57
|
+
console.log(`[Crew] ${roleName} turn completed`);
|
|
58
|
+
roleState.consecutiveErrors = 0;
|
|
59
|
+
|
|
60
|
+
endRoleStreaming(session, roleName);
|
|
61
|
+
|
|
62
|
+
// 更新费用(差值计算)
|
|
63
|
+
if (message.total_cost_usd != null) {
|
|
64
|
+
const costDelta = message.total_cost_usd - roleState.lastCostUsd;
|
|
65
|
+
if (costDelta > 0) session.costUsd += costDelta;
|
|
66
|
+
roleState.lastCostUsd = message.total_cost_usd;
|
|
67
|
+
}
|
|
68
|
+
if (message.usage) {
|
|
69
|
+
const inputDelta = (message.usage.input_tokens || 0) - (roleState.lastInputTokens || 0);
|
|
70
|
+
const outputDelta = (message.usage.output_tokens || 0) - (roleState.lastOutputTokens || 0);
|
|
71
|
+
if (inputDelta > 0) session.totalInputTokens += inputDelta;
|
|
72
|
+
if (outputDelta > 0) session.totalOutputTokens += outputDelta;
|
|
73
|
+
roleState.lastInputTokens = message.usage.input_tokens || 0;
|
|
74
|
+
roleState.lastOutputTokens = message.usage.output_tokens || 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 持久化 sessionId
|
|
78
|
+
if (roleState.claudeSessionId) {
|
|
79
|
+
saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
|
|
80
|
+
.catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// context 使用率监控
|
|
84
|
+
const inputTokens = message.usage?.input_tokens || 0;
|
|
85
|
+
if (inputTokens > 0) {
|
|
86
|
+
sendCrewMessage({
|
|
87
|
+
type: 'crew_context_usage',
|
|
88
|
+
sessionId: session.id,
|
|
89
|
+
role: roleName,
|
|
90
|
+
inputTokens,
|
|
91
|
+
maxTokens: MAX_CONTEXT,
|
|
92
|
+
percentage: Math.min(100, Math.round((inputTokens / MAX_CONTEXT) * 100))
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const contextPercentage = inputTokens / MAX_CONTEXT;
|
|
97
|
+
const needClear = contextPercentage >= CLEAR_THRESHOLD;
|
|
98
|
+
|
|
99
|
+
// 解析路由
|
|
100
|
+
const routes = parseRoutes(roleState.accumulatedText);
|
|
101
|
+
|
|
102
|
+
// 决策者 turn 完成:检测 TASKS block 中新完成的任务
|
|
103
|
+
const roleConfig = session.roles.get(roleName);
|
|
104
|
+
if (roleConfig?.isDecisionMaker) {
|
|
105
|
+
const nowCompleted = parseCompletedTasks(roleState.accumulatedText);
|
|
106
|
+
if (nowCompleted.size > 0) {
|
|
107
|
+
const prev = session._completedTaskIds || new Set();
|
|
108
|
+
const newlyDone = [];
|
|
109
|
+
for (const tid of nowCompleted) {
|
|
110
|
+
if (!prev.has(tid)) {
|
|
111
|
+
prev.add(tid);
|
|
112
|
+
newlyDone.push(tid);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
session._completedTaskIds = prev;
|
|
116
|
+
if (newlyDone.length > 0) {
|
|
117
|
+
updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
|
|
118
|
+
for (const tid of newlyDone) {
|
|
119
|
+
const feature = session.features.get(tid);
|
|
120
|
+
const title = feature?.taskTitle || tid;
|
|
121
|
+
appendChangelog(session, tid, title).catch(e => console.warn(`[Crew] Failed to append changelog for ${tid}:`, e.message));
|
|
122
|
+
updateKanban(session, { taskId: tid, completed: true }).catch(e => console.warn(`[Crew] Failed to update kanban for ${tid}:`, e.message));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 保存 accumulatedText 供后续 saveRoleWorkSummary 使用(清空前)
|
|
129
|
+
const turnText = roleState.accumulatedText;
|
|
130
|
+
roleState.accumulatedText = '';
|
|
131
|
+
roleState.turnActive = false;
|
|
132
|
+
|
|
133
|
+
sendCrewMessage({
|
|
134
|
+
type: 'crew_turn_completed',
|
|
135
|
+
sessionId: session.id,
|
|
136
|
+
role: roleName
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
sendStatusUpdate(session);
|
|
140
|
+
|
|
141
|
+
// Context 超限:保存工作摘要后 clear + rebuild
|
|
142
|
+
if (needClear) {
|
|
143
|
+
console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, clearing and rebuilding`);
|
|
144
|
+
|
|
145
|
+
// 保存工作摘要到 feature 文件
|
|
146
|
+
await saveRoleWorkSummary(session, roleName, turnText).catch(e =>
|
|
147
|
+
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
148
|
+
|
|
149
|
+
// Clear 角色
|
|
150
|
+
await clearRoleSessionId(session.sharedDir, roleName);
|
|
151
|
+
roleState.claudeSessionId = null;
|
|
152
|
+
|
|
153
|
+
if (roleState.abortController) roleState.abortController.abort();
|
|
154
|
+
roleState.query = null;
|
|
155
|
+
roleState.inputStream = null;
|
|
156
|
+
|
|
157
|
+
sendCrewMessage({
|
|
158
|
+
type: 'crew_role_cleared',
|
|
159
|
+
sessionId: session.id,
|
|
160
|
+
role: roleName,
|
|
161
|
+
contextPercentage: Math.round(contextPercentage * 100),
|
|
162
|
+
reason: 'context_limit'
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 继承 task 到路由(如有)
|
|
166
|
+
const currentTask = roleState.currentTask;
|
|
167
|
+
if (routes.length > 0) {
|
|
168
|
+
for (const route of routes) {
|
|
169
|
+
if (!route.taskId && currentTask) {
|
|
170
|
+
route.taskId = currentTask.taskId;
|
|
171
|
+
route.taskTitle = currentTask.taskTitle;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 执行路由
|
|
177
|
+
if (routes.length > 0) {
|
|
178
|
+
session.round++;
|
|
179
|
+
const results = await Promise.allSettled(routes.map(route =>
|
|
180
|
+
executeRoute(session, roleName, route)
|
|
181
|
+
));
|
|
182
|
+
for (const r of results) {
|
|
183
|
+
if (r.status === 'rejected') {
|
|
184
|
+
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return; // query 已清空,退出
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 执行路由(无需 clear 时)
|
|
192
|
+
if (routes.length > 0) {
|
|
193
|
+
session.round++;
|
|
194
|
+
|
|
195
|
+
const currentTask = roleState.currentTask;
|
|
196
|
+
for (const route of routes) {
|
|
197
|
+
if (!route.taskId && currentTask) {
|
|
198
|
+
route.taskId = currentTask.taskId;
|
|
199
|
+
route.taskTitle = currentTask.taskTitle;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const results = await Promise.allSettled(routes.map(route =>
|
|
204
|
+
executeRoute(session, roleName, route)
|
|
205
|
+
));
|
|
206
|
+
for (const r of results) {
|
|
207
|
+
if (r.status === 'rejected') {
|
|
208
|
+
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
const { processHumanQueue } = await import('./human-interaction.js');
|
|
213
|
+
await processHumanQueue(session);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error.name === 'AbortError') {
|
|
219
|
+
console.log(`[Crew] ${roleName} aborted`);
|
|
220
|
+
if (session.status === 'paused' && roleState.accumulatedText) {
|
|
221
|
+
const routes = parseRoutes(roleState.accumulatedText);
|
|
222
|
+
if (routes.length > 0 && session.pendingRoutes.length === 0) {
|
|
223
|
+
session.pendingRoutes = routes.map(route => ({ fromRole: roleName, route }));
|
|
224
|
+
console.log(`[Crew] Saved ${routes.length} pending route(s) from aborted ${roleName}`);
|
|
225
|
+
}
|
|
226
|
+
roleState.accumulatedText = '';
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
console.error(`[Crew] ${roleName} error:`, error.message);
|
|
230
|
+
|
|
231
|
+
// Step 1: 清理 roleState
|
|
232
|
+
endRoleStreaming(session, roleName);
|
|
233
|
+
const errorTurnText = roleState.accumulatedText;
|
|
234
|
+
roleState.query = null;
|
|
235
|
+
roleState.inputStream = null;
|
|
236
|
+
roleState.turnActive = false;
|
|
237
|
+
roleState.accumulatedText = '';
|
|
238
|
+
|
|
239
|
+
// Step 2: 错误分类
|
|
240
|
+
const classification = classifyRoleError(error);
|
|
241
|
+
roleState.consecutiveErrors++;
|
|
242
|
+
|
|
243
|
+
// Step 3: 通知前端
|
|
244
|
+
sendCrewMessage({
|
|
245
|
+
type: 'crew_role_error',
|
|
246
|
+
sessionId: session.id,
|
|
247
|
+
role: roleName,
|
|
248
|
+
error: error.message.substring(0, 500),
|
|
249
|
+
reason: classification.reason,
|
|
250
|
+
recoverable: classification.recoverable,
|
|
251
|
+
retryCount: roleState.consecutiveErrors
|
|
252
|
+
});
|
|
253
|
+
sendStatusUpdate(session);
|
|
254
|
+
|
|
255
|
+
// Step 4: 判断是否重试
|
|
256
|
+
const MAX_RETRIES = 3;
|
|
257
|
+
if (!classification.recoverable || roleState.consecutiveErrors > MAX_RETRIES) {
|
|
258
|
+
const exhausted = roleState.consecutiveErrors > MAX_RETRIES;
|
|
259
|
+
const errDetail = exhausted
|
|
260
|
+
? `角色 ${roleName} 连续 ${MAX_RETRIES} 次错误后停止重试。最后错误: ${error.message}`
|
|
261
|
+
: `角色 ${roleName} 不可恢复错误: ${error.message}`;
|
|
262
|
+
if (roleName !== session.decisionMaker) {
|
|
263
|
+
await dispatchToRole(session, session.decisionMaker, errDetail, 'system');
|
|
264
|
+
} else {
|
|
265
|
+
session.status = 'waiting_human';
|
|
266
|
+
sendCrewMessage({
|
|
267
|
+
type: 'crew_human_needed',
|
|
268
|
+
sessionId: session.id,
|
|
269
|
+
fromRole: roleName,
|
|
270
|
+
reason: 'error',
|
|
271
|
+
message: errDetail
|
|
272
|
+
});
|
|
273
|
+
sendStatusUpdate(session);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Step 5: 可恢复 → 保存摘要后 clear + 重建重试
|
|
279
|
+
console.log(`[Crew] ${roleName} attempting recovery (${classification.reason}), retry ${roleState.consecutiveErrors}/${MAX_RETRIES}`);
|
|
280
|
+
|
|
281
|
+
sendCrewOutput(session, 'system', 'system', {
|
|
282
|
+
type: 'assistant',
|
|
283
|
+
message: { role: 'assistant', content: [{
|
|
284
|
+
type: 'text',
|
|
285
|
+
text: `${roleName} 遇到 ${classification.reason},正在自动恢复 (${roleState.consecutiveErrors}/${MAX_RETRIES})...`
|
|
286
|
+
}] }
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (roleState.lastDispatchContent) {
|
|
290
|
+
// 保存工作摘要
|
|
291
|
+
await saveRoleWorkSummary(session, roleName, errorTurnText).catch(e =>
|
|
292
|
+
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
293
|
+
|
|
294
|
+
// 所有可恢复错误统一 clear + rebuild
|
|
295
|
+
await clearRoleSessionId(session.sharedDir, roleName);
|
|
296
|
+
const consecutiveErrors = roleState.consecutiveErrors;
|
|
297
|
+
await dispatchToRole(
|
|
298
|
+
session, roleName,
|
|
299
|
+
roleState.lastDispatchContent,
|
|
300
|
+
roleState.lastDispatchFrom || 'system',
|
|
301
|
+
roleState.lastDispatchTaskId,
|
|
302
|
+
roleState.lastDispatchTaskTitle
|
|
303
|
+
);
|
|
304
|
+
// 保持错误计数
|
|
305
|
+
const newState = session.roleStates.get(roleName);
|
|
306
|
+
if (newState) newState.consecutiveErrors = consecutiveErrors;
|
|
307
|
+
} else {
|
|
308
|
+
const msg = `角色 ${roleName} 已恢复(${classification.reason}),但无待重试消息。`;
|
|
309
|
+
if (roleName !== session.decisionMaker) {
|
|
310
|
+
await dispatchToRole(session, session.decisionMaker, msg, 'system');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 角色 Query 管理
|
|
3
|
+
* createRoleQuery, buildRoleSystemPrompt, sessionId 持久化, 错误分类
|
|
4
|
+
*/
|
|
5
|
+
import { query, Stream } from '../sdk/index.js';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { getMessages } from '../crew-i18n.js';
|
|
9
|
+
|
|
10
|
+
/** Format role label */
|
|
11
|
+
function roleLabel(r) {
|
|
12
|
+
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// =====================================================================
|
|
16
|
+
// Session Persistence (role sessionId)
|
|
17
|
+
// =====================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 保存角色的 claudeSessionId 到文件
|
|
21
|
+
*/
|
|
22
|
+
export async function saveRoleSessionId(sharedDir, roleName, claudeSessionId) {
|
|
23
|
+
const sessionsDir = join(sharedDir, 'sessions');
|
|
24
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
25
|
+
const filePath = join(sessionsDir, `${roleName}.json`);
|
|
26
|
+
await fs.writeFile(filePath, JSON.stringify({
|
|
27
|
+
claudeSessionId,
|
|
28
|
+
savedAt: Date.now()
|
|
29
|
+
}));
|
|
30
|
+
console.log(`[Crew] Saved sessionId for ${roleName}: ${claudeSessionId}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 从文件加载角色的 claudeSessionId
|
|
35
|
+
*/
|
|
36
|
+
export async function loadRoleSessionId(sharedDir, roleName) {
|
|
37
|
+
const filePath = join(sharedDir, 'sessions', `${roleName}.json`);
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
40
|
+
return data.claudeSessionId || null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 清除角色的 savedSessionId(用于强制新建 conversation)
|
|
48
|
+
*/
|
|
49
|
+
export async function clearRoleSessionId(sharedDir, roleName) {
|
|
50
|
+
const filePath = join(sharedDir, 'sessions', `${roleName}.json`);
|
|
51
|
+
try {
|
|
52
|
+
await fs.unlink(filePath);
|
|
53
|
+
console.log(`[Crew] Cleared sessionId for ${roleName} (force new conversation)`);
|
|
54
|
+
} catch {
|
|
55
|
+
// 文件不存在也正常
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 判断角色错误是否可恢复
|
|
61
|
+
*/
|
|
62
|
+
export function classifyRoleError(error) {
|
|
63
|
+
const msg = error.message || '';
|
|
64
|
+
if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
|
|
65
|
+
return { recoverable: true, reason: 'context_exceeded' };
|
|
66
|
+
}
|
|
67
|
+
if (/compact|compress|context.*reduc/i.test(msg)) {
|
|
68
|
+
return { recoverable: true, reason: 'compact_failed' };
|
|
69
|
+
}
|
|
70
|
+
if (/rate.?limit|429|overloaded|503|502|timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) {
|
|
71
|
+
return { recoverable: true, reason: 'transient_api_error' };
|
|
72
|
+
}
|
|
73
|
+
if (/exited with code [1-9]/i.test(msg) && msg.length < 100) {
|
|
74
|
+
return { recoverable: true, reason: 'process_crashed' };
|
|
75
|
+
}
|
|
76
|
+
if (/spawn|ENOENT|not found/i.test(msg)) {
|
|
77
|
+
return { recoverable: false, reason: 'spawn_failed' };
|
|
78
|
+
}
|
|
79
|
+
return { recoverable: true, reason: 'unknown' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// =====================================================================
|
|
83
|
+
// Role Query Management
|
|
84
|
+
// =====================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 为角色创建持久 query 实例
|
|
88
|
+
*/
|
|
89
|
+
export async function createRoleQuery(session, roleName) {
|
|
90
|
+
const role = session.roles.get(roleName);
|
|
91
|
+
if (!role) throw new Error(`Role not found: ${roleName}`);
|
|
92
|
+
|
|
93
|
+
// Lazy import to avoid circular dependency
|
|
94
|
+
const { processRoleOutput } = await import('./role-output.js');
|
|
95
|
+
|
|
96
|
+
const inputStream = new Stream();
|
|
97
|
+
const abortController = new AbortController();
|
|
98
|
+
|
|
99
|
+
const systemPrompt = buildRoleSystemPrompt(role, session);
|
|
100
|
+
|
|
101
|
+
// 尝试加载之前保存的 sessionId
|
|
102
|
+
const savedSessionId = await loadRoleSessionId(session.sharedDir, roleName);
|
|
103
|
+
|
|
104
|
+
// cwd 设为角色目录
|
|
105
|
+
const roleCwd = join(session.sharedDir, 'roles', roleName);
|
|
106
|
+
|
|
107
|
+
const queryOptions = {
|
|
108
|
+
cwd: roleCwd,
|
|
109
|
+
permissionMode: 'bypassPermissions',
|
|
110
|
+
abort: abortController.signal,
|
|
111
|
+
model: role.model || undefined,
|
|
112
|
+
appendSystemPrompt: systemPrompt
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (savedSessionId) {
|
|
116
|
+
queryOptions.resume = savedSessionId;
|
|
117
|
+
console.log(`[Crew] Resuming ${roleName} with sessionId: ${savedSessionId}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const roleQuery = query({
|
|
121
|
+
prompt: inputStream,
|
|
122
|
+
options: queryOptions
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const roleState = {
|
|
126
|
+
query: roleQuery,
|
|
127
|
+
inputStream,
|
|
128
|
+
abortController,
|
|
129
|
+
accumulatedText: '',
|
|
130
|
+
turnActive: false,
|
|
131
|
+
claudeSessionId: savedSessionId,
|
|
132
|
+
lastCostUsd: 0,
|
|
133
|
+
lastInputTokens: 0,
|
|
134
|
+
lastOutputTokens: 0,
|
|
135
|
+
consecutiveErrors: 0,
|
|
136
|
+
lastDispatchContent: null,
|
|
137
|
+
lastDispatchFrom: null,
|
|
138
|
+
lastDispatchTaskId: null,
|
|
139
|
+
lastDispatchTaskTitle: null
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
session.roleStates.set(roleName, roleState);
|
|
143
|
+
|
|
144
|
+
// 异步处理角色输出
|
|
145
|
+
processRoleOutput(session, roleName, roleQuery, roleState);
|
|
146
|
+
|
|
147
|
+
return roleState;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 构建角色的 system prompt
|
|
152
|
+
*/
|
|
153
|
+
export function buildRoleSystemPrompt(role, session) {
|
|
154
|
+
const allRoles = Array.from(session.roles.values());
|
|
155
|
+
|
|
156
|
+
let routeTargets;
|
|
157
|
+
if (role.groupIndex > 0) {
|
|
158
|
+
routeTargets = allRoles.filter(r =>
|
|
159
|
+
r.name !== role.name && (r.groupIndex === role.groupIndex || r.groupIndex === 0)
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
routeTargets = allRoles.filter(r => r.name !== role.name);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const m = getMessages(session.language || 'zh-CN');
|
|
166
|
+
|
|
167
|
+
let prompt = `${m.teamCollab}
|
|
168
|
+
${m.teamCollabIntro()}
|
|
169
|
+
|
|
170
|
+
${m.teamMembers}
|
|
171
|
+
${allRoles.map(r => `- ${roleLabel(r)}: ${r.description}${r.isDecisionMaker ? ` (${m.decisionMakerTag})` : ''}`).join('\n')}`;
|
|
172
|
+
|
|
173
|
+
const hasMultiInstance = allRoles.some(r => r.groupIndex > 0);
|
|
174
|
+
|
|
175
|
+
if (routeTargets.length > 0) {
|
|
176
|
+
prompt += `\n\n${m.routingRules}
|
|
177
|
+
${m.routingIntro}
|
|
178
|
+
|
|
179
|
+
\`\`\`
|
|
180
|
+
---ROUTE---
|
|
181
|
+
to: <roleName>
|
|
182
|
+
summary: <brief description>
|
|
183
|
+
---END_ROUTE---
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
${m.routeTargets}
|
|
187
|
+
${routeTargets.map(r => `- ${r.name}: ${roleLabel(r)} — ${r.description}`).join('\n')}
|
|
188
|
+
- human: ${m.humanTarget}
|
|
189
|
+
|
|
190
|
+
${m.routeNotes(session.decisionMaker)}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 决策者额外 prompt
|
|
194
|
+
if (role.isDecisionMaker) {
|
|
195
|
+
const isDevTeam = session.teamType === 'dev';
|
|
196
|
+
|
|
197
|
+
prompt += `\n\n${m.toolUsage}
|
|
198
|
+
${m.toolUsageContent(isDevTeam)}`;
|
|
199
|
+
|
|
200
|
+
prompt += `\n\n${m.dmRole}
|
|
201
|
+
${m.dmRoleContent}`;
|
|
202
|
+
|
|
203
|
+
if (isDevTeam) {
|
|
204
|
+
prompt += m.dmDevExtra;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!isDevTeam) {
|
|
208
|
+
prompt += `\n\n${m.collabMode}
|
|
209
|
+
${m.collabModeContent}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isDevTeam && hasMultiInstance) {
|
|
213
|
+
const maxGroup = Math.max(...allRoles.map(r => r.groupIndex));
|
|
214
|
+
const groupLines = [];
|
|
215
|
+
for (let g = 1; g <= maxGroup; g++) {
|
|
216
|
+
const members = allRoles.filter(r => r.groupIndex === g);
|
|
217
|
+
const memberStrs = members.map(r => {
|
|
218
|
+
const state = session.roleStates.get(r.name);
|
|
219
|
+
const busy = state?.turnActive;
|
|
220
|
+
const task = state?.currentTask;
|
|
221
|
+
if (busy && task) return `${r.name}(${m.groupBusy(task.taskId + ' ' + task.taskTitle)})`;
|
|
222
|
+
if (busy) return `${r.name}(${m.groupBusyShort})`;
|
|
223
|
+
return `${r.name}(${m.groupIdle})`;
|
|
224
|
+
});
|
|
225
|
+
groupLines.push(`${m.groupLabel(g)}: ${memberStrs.join(' ')}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
prompt += `\n\n${m.execGroupStatus}
|
|
229
|
+
${groupLines.join(' / ')}
|
|
230
|
+
|
|
231
|
+
${m.parallelRules}
|
|
232
|
+
${m.parallelRulesContent(maxGroup)}
|
|
233
|
+
|
|
234
|
+
\`\`\`
|
|
235
|
+
---ROUTE---
|
|
236
|
+
to: dev-1
|
|
237
|
+
task: task-1
|
|
238
|
+
taskTitle: ${m.implLoginPage}
|
|
239
|
+
summary: ${m.implLoginSummary}
|
|
240
|
+
---END_ROUTE---
|
|
241
|
+
|
|
242
|
+
---ROUTE---
|
|
243
|
+
to: dev-2
|
|
244
|
+
task: task-2
|
|
245
|
+
taskTitle: ${m.implRegisterPage}
|
|
246
|
+
summary: ${m.implRegisterSummary}
|
|
247
|
+
---END_ROUTE---
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
${m.parallelExample}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
prompt += `\n
|
|
254
|
+
${m.workflowEnd}
|
|
255
|
+
${m.workflowEndContent(isDevTeam)}
|
|
256
|
+
|
|
257
|
+
${m.taskList}
|
|
258
|
+
${m.taskListContent}
|
|
259
|
+
|
|
260
|
+
\`\`\`
|
|
261
|
+
${m.taskExample}
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
${m.taskListNotes}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Feature 进度文件说明
|
|
268
|
+
prompt += `\n\n${m.featureRecordTitle}
|
|
269
|
+
${m.featureRecordContent}
|
|
270
|
+
|
|
271
|
+
${m.contextRestartTitle}
|
|
272
|
+
${m.contextRestartContent}`;
|
|
273
|
+
|
|
274
|
+
// 执行者角色的组绑定 prompt
|
|
275
|
+
if (role.groupIndex > 0 && role.roleType === 'developer') {
|
|
276
|
+
const gi = role.groupIndex;
|
|
277
|
+
const rev = allRoles.find(r => r.roleType === 'reviewer' && r.groupIndex === gi);
|
|
278
|
+
const test = allRoles.find(r => r.roleType === 'tester' && r.groupIndex === gi);
|
|
279
|
+
if (rev && test) {
|
|
280
|
+
prompt += `\n\n${m.devGroupBinding}
|
|
281
|
+
${m.devGroupBindingContent(gi, roleLabel(rev), rev.name, roleLabel(test), test.name)}
|
|
282
|
+
|
|
283
|
+
\`\`\`
|
|
284
|
+
---ROUTE---
|
|
285
|
+
to: ${rev.name}
|
|
286
|
+
summary: ${m.reviewCode}
|
|
287
|
+
---END_ROUTE---
|
|
288
|
+
|
|
289
|
+
---ROUTE---
|
|
290
|
+
to: ${test.name}
|
|
291
|
+
summary: ${m.testFeature}
|
|
292
|
+
---END_ROUTE---
|
|
293
|
+
\`\`\`
|
|
294
|
+
|
|
295
|
+
${m.devGroupBindingNote}`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Language instruction
|
|
300
|
+
if (session.language === 'en') {
|
|
301
|
+
prompt += `\n\n# Language
|
|
302
|
+
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.`;
|
|
303
|
+
} else {
|
|
304
|
+
prompt += `\n\n# Language
|
|
305
|
+
Always respond in 中文. Use 中文 for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return prompt;
|
|
309
|
+
}
|