@yeaft/webchat-agent 1.0.76 → 1.0.78
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 +2 -6
- package/connection/message-router.js +1 -64
- package/conversation.js +4 -32
- package/history.js +2 -2
- package/index.js +0 -6
- package/package.json +1 -1
- package/providers/claude-code.js +1 -1
- package/yeaft/attachments.js +2 -2
- package/yeaft/config.js +1 -1
- package/yeaft/conversation/persist.js +458 -112
- package/yeaft/conversation/search.js +51 -15
- package/yeaft/session.js +27 -18
- package/yeaft/web-bridge.js +27 -15
- package/crew/builtin-actions.js +0 -154
- package/crew/context-loader.js +0 -171
- package/crew/control.js +0 -444
- package/crew/human-interaction.js +0 -195
- package/crew/persistence.js +0 -295
- package/crew/role-management.js +0 -182
- package/crew/role-output.js +0 -461
- package/crew/role-query.js +0 -406
- package/crew/role-states.js +0 -180
- package/crew/routing-fallback.js +0 -64
- package/crew/routing-metrics.js +0 -215
- package/crew/routing.js +0 -951
- package/crew/session.js +0 -648
- package/crew/shared-dir.js +0 -266
- package/crew/task-files.js +0 -554
- package/crew/ui-messages.js +0 -274
- package/crew/worktree.js +0 -130
- package/crew-i18n.js +0 -579
- package/crew.js +0 -48
package/crew/role-output.js
DELETED
|
@@ -1,461 +0,0 @@
|
|
|
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
|
-
import { debouncedSaveSessionMeta, saveSessionMeta } from './persistence.js';
|
|
10
|
-
import { recordRoutingEvent } from './routing-metrics.js';
|
|
11
|
-
import { resolveFallbackTarget } from './routing-fallback.js';
|
|
12
|
-
import ctx from '../context.js';
|
|
13
|
-
|
|
14
|
-
// Context 使用率常量(运行时从 ctx.CONFIG 读取)
|
|
15
|
-
const getMaxContext = () => ctx.CONFIG?.maxContextTokens || 128000;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Detect routing intent in text that lacks a proper ROUTE block.
|
|
19
|
-
* Returns true if keywords suggest the role intended to route to someone.
|
|
20
|
-
* @param {string} text
|
|
21
|
-
* @returns {boolean}
|
|
22
|
-
*/
|
|
23
|
-
export function _detectRouteIntent(text) {
|
|
24
|
-
if (!text || text.length < 10) return false;
|
|
25
|
-
// Check only the last 1000 chars (routing intent is usually at the end)
|
|
26
|
-
const tail = text.slice(-1000);
|
|
27
|
-
// Chinese patterns: 提交给/交给/请.*审查/转给/发给/route to
|
|
28
|
-
// English patterns: route to/submit to/forward to/hand off to/pass to
|
|
29
|
-
const intentPatterns = [
|
|
30
|
-
/提交给\s*\S+/,
|
|
31
|
-
/交给\s*\S+/,
|
|
32
|
-
/请\s*\S+\s*审查/,
|
|
33
|
-
/转给\s*\S+/,
|
|
34
|
-
/发给\s*\S+/,
|
|
35
|
-
/请\s*(?:rev|test)-\d+/i, // 请 rev-1/test-2 (explicit role mention with request)
|
|
36
|
-
/PR\s*#\d+.*(?:请|review|审查)/i, // PR #123 + review request
|
|
37
|
-
/(?:rev|test)-\d+\s*(?:审查|review|check|测试)/i, // rev-1 审查 / test-2 测试
|
|
38
|
-
/route\s+to\s+\S+/i,
|
|
39
|
-
/submit\s+to\s+\S+/i,
|
|
40
|
-
/forward\s+to\s+\S+/i,
|
|
41
|
-
/hand\s*off\s+to\s+\S+/i,
|
|
42
|
-
/pass\s+to\s+\S+/i,
|
|
43
|
-
/ROUTE[→:]\s*\S+/, // shorthand that parseRoutes might have already caught, but as safety net
|
|
44
|
-
];
|
|
45
|
-
return intentPatterns.some(p => p.test(tail));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 处理角色的流式输出
|
|
50
|
-
*/
|
|
51
|
-
export async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
52
|
-
// 辅助函数:将 lastSeenUsage 结算到 session(用于 abort/error 场景,避免丢失 token)
|
|
53
|
-
function settleLastSeenUsage() {
|
|
54
|
-
if (!roleState.lastSeenUsage) return;
|
|
55
|
-
const { totalCostUsd, inputTokens, outputTokens } = roleState.lastSeenUsage;
|
|
56
|
-
if (totalCostUsd != null) {
|
|
57
|
-
const costDelta = totalCostUsd - roleState.lastCostUsd;
|
|
58
|
-
if (costDelta > 0) session.costUsd += costDelta;
|
|
59
|
-
roleState.lastCostUsd = totalCostUsd;
|
|
60
|
-
}
|
|
61
|
-
if (inputTokens != null || outputTokens != null) {
|
|
62
|
-
const inputDelta = (inputTokens || 0) - (roleState.lastInputTokens || 0);
|
|
63
|
-
const outputDelta = (outputTokens || 0) - (roleState.lastOutputTokens || 0);
|
|
64
|
-
if (inputDelta > 0) session.totalInputTokens += inputDelta;
|
|
65
|
-
if (outputDelta > 0) session.totalOutputTokens += outputDelta;
|
|
66
|
-
roleState.lastInputTokens = inputTokens || 0;
|
|
67
|
-
roleState.lastOutputTokens = outputTokens || 0;
|
|
68
|
-
}
|
|
69
|
-
roleState.lastSeenUsage = null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
for await (const message of roleQuery) {
|
|
74
|
-
// 检查 session 是否已停止或暂停
|
|
75
|
-
if (session.status === 'stopped' || session.status === 'paused') break;
|
|
76
|
-
|
|
77
|
-
// 每次收到带 usage/cost 的消息,暂存到 lastSeenUsage(供 abort/error 结算)
|
|
78
|
-
if (message.total_cost_usd != null || message.usage) {
|
|
79
|
-
roleState.lastSeenUsage = {
|
|
80
|
-
totalCostUsd: message.total_cost_usd,
|
|
81
|
-
inputTokens: message.usage?.input_tokens,
|
|
82
|
-
outputTokens: message.usage?.output_tokens
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (message.type === 'system' && message.subtype === 'init') {
|
|
87
|
-
roleState.claudeSessionId = message.session_id;
|
|
88
|
-
console.log(`[Crew] ${roleName} session: ${message.session_id}`);
|
|
89
|
-
|
|
90
|
-
// Decision maker 的 system init 中捕获 slash_commands,发给前端用于 autocomplete
|
|
91
|
-
const roleConfig = session.roles.get(roleName);
|
|
92
|
-
if (roleConfig?.isDecisionMaker && message.slash_commands?.length > 0) {
|
|
93
|
-
console.log(`[Crew] ${roleName} slash commands: ${message.slash_commands.join(', ')}`);
|
|
94
|
-
sendCrewMessage({
|
|
95
|
-
type: 'slash_commands_update',
|
|
96
|
-
conversationId: session.id,
|
|
97
|
-
slashCommands: message.slash_commands
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (message.type === 'assistant') {
|
|
104
|
-
const content = message.message?.content;
|
|
105
|
-
if (content) {
|
|
106
|
-
if (typeof content === 'string') {
|
|
107
|
-
roleState.accumulatedText += content;
|
|
108
|
-
sendCrewOutput(session, roleName, 'text', message);
|
|
109
|
-
} else if (Array.isArray(content)) {
|
|
110
|
-
let hasText = false;
|
|
111
|
-
for (const block of content) {
|
|
112
|
-
if (block.type === 'text') {
|
|
113
|
-
roleState.accumulatedText += block.text;
|
|
114
|
-
hasText = true;
|
|
115
|
-
} else if (block.type === 'tool_use') {
|
|
116
|
-
endRoleStreaming(session, roleName);
|
|
117
|
-
roleState.currentTool = block.name;
|
|
118
|
-
sendCrewOutput(session, roleName, 'tool_use', message);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (hasText) {
|
|
122
|
-
sendCrewOutput(session, roleName, 'text', message);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
} else if (message.type === 'user') {
|
|
127
|
-
roleState.currentTool = null;
|
|
128
|
-
sendCrewOutput(session, roleName, 'tool_result', message);
|
|
129
|
-
} else if (message.type === 'result') {
|
|
130
|
-
// Turn 完成
|
|
131
|
-
console.log(`[Crew] ${roleName} turn completed`);
|
|
132
|
-
roleState.consecutiveErrors = 0;
|
|
133
|
-
|
|
134
|
-
endRoleStreaming(session, roleName);
|
|
135
|
-
|
|
136
|
-
// 更新费用(通过 settleLastSeenUsage 统一处理,避免重复逻辑)
|
|
137
|
-
settleLastSeenUsage();
|
|
138
|
-
|
|
139
|
-
// 持久化 sessionId
|
|
140
|
-
if (roleState.claudeSessionId) {
|
|
141
|
-
saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
|
|
142
|
-
.catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// context 使用率监控
|
|
146
|
-
const inputTokens = message.usage?.input_tokens || 0;
|
|
147
|
-
if (inputTokens > 0) {
|
|
148
|
-
sendCrewMessage({
|
|
149
|
-
type: 'crew_context_usage',
|
|
150
|
-
sessionId: session.id,
|
|
151
|
-
role: roleName,
|
|
152
|
-
inputTokens,
|
|
153
|
-
maxTokens: getMaxContext(),
|
|
154
|
-
percentage: Math.min(100, Math.round((inputTokens / getMaxContext()) * 100))
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 解析路由 — task-328: parseRoutes returns a decorated Array
|
|
159
|
-
// (`.routes`/`.displayBody`/`.strippedRanges`). We keep treating it as
|
|
160
|
-
// an Array for routing iteration, but use `.displayBody` whenever we
|
|
161
|
-
// need the role's prose with ROUTE blocks accurately removed.
|
|
162
|
-
const parseResult = parseRoutes(roleState.accumulatedText);
|
|
163
|
-
const routes = parseResult;
|
|
164
|
-
const displayBody = parseResult.displayBody || roleState.accumulatedText;
|
|
165
|
-
// task-330b §B item 1: detect "parse-fail" — text mentions a ROUTE
|
|
166
|
-
// opener but nothing parseable came out. Counts as a separate metric
|
|
167
|
-
// from plain missing-route so dashboards can split malformed-block
|
|
168
|
-
// (likely template/prompt drift) from forgot-to-write-block.
|
|
169
|
-
if (routes.length === 0 && /---\s*ROUTE\s*---/i.test(roleState.accumulatedText || '')) {
|
|
170
|
-
recordRoutingEvent(session, 'parse-fail', {
|
|
171
|
-
fromRole: roleName,
|
|
172
|
-
taskId: roleState.currentTask?.taskId || null,
|
|
173
|
-
note: 'ROUTE opener present but no parseable block',
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
// Fallback: 如果 route summary 仍为空占位符,用 displayBody 末尾 500 字符
|
|
177
|
-
// task-330c: source switched from `accumulatedText.slice(-500)` to
|
|
178
|
-
// `displayBody.slice(-500)` — accumulatedText still contains
|
|
179
|
-
// ---ROUTE--- markers; using it would re-strip already-stripped text
|
|
180
|
-
// and risk truncating mid-marker. displayBody is the parser's
|
|
181
|
-
// post-strip prose and is the single source of truth for "what the
|
|
182
|
-
// role said outside its routing blocks".
|
|
183
|
-
// ⚠️ DO NOT add any further `.replace(/---ROUTE---.../g, '')` on
|
|
184
|
-
// `displayBody` here or downstream — it is already strip-clean.
|
|
185
|
-
for (const route of routes) {
|
|
186
|
-
if (route.summary === '[该角色未提供消息摘要]' && displayBody) {
|
|
187
|
-
const tail = displayBody.slice(-500).trim();
|
|
188
|
-
if (tail) route.summary = `[auto-extracted]\n${tail}`;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 决策者 turn 完成:检测 TASKS block 中新完成的任务
|
|
193
|
-
const roleConfig = session.roles.get(roleName);
|
|
194
|
-
if (roleConfig?.isDecisionMaker) {
|
|
195
|
-
const knownTaskIds = Array.from(session.features.keys());
|
|
196
|
-
const nowCompleted = parseCompletedTasks(roleState.accumulatedText, knownTaskIds);
|
|
197
|
-
if (nowCompleted.size > 0) {
|
|
198
|
-
const prev = session._completedTaskIds || new Set();
|
|
199
|
-
const newlyDone = [];
|
|
200
|
-
for (const tid of nowCompleted) {
|
|
201
|
-
if (!prev.has(tid)) {
|
|
202
|
-
prev.add(tid);
|
|
203
|
-
newlyDone.push(tid);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
session._completedTaskIds = prev;
|
|
207
|
-
if (newlyDone.length > 0) {
|
|
208
|
-
updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
|
|
209
|
-
for (const tid of newlyDone) {
|
|
210
|
-
const feature = session.features.get(tid);
|
|
211
|
-
const title = feature?.taskTitle || tid;
|
|
212
|
-
appendChangelog(session, tid, title).catch(e => console.warn(`[Crew] Failed to append changelog for ${tid}:`, e.message));
|
|
213
|
-
updateKanban(session, { taskId: tid, completed: true }).catch(e => console.warn(`[Crew] Failed to update kanban for ${tid}:`, e.message));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 保存本 turn 文本(供 routing.js 预检时 saveRoleWorkSummary 使用)
|
|
220
|
-
// task-328: lastTurnText keeps the raw transcript (consumers may need
|
|
221
|
-
// ROUTE blocks for analysis); lastTurnDisplayBody is the parser-clean
|
|
222
|
-
// body used by auto-forward/UI logic to avoid sending broken ROUTE
|
|
223
|
-
// residue downstream.
|
|
224
|
-
roleState.lastTurnText = roleState.accumulatedText;
|
|
225
|
-
roleState.lastTurnDisplayBody = displayBody;
|
|
226
|
-
roleState.accumulatedText = '';
|
|
227
|
-
roleState.turnActive = false;
|
|
228
|
-
|
|
229
|
-
// Mark pending tool messages as completed before notifying frontend
|
|
230
|
-
for (const m of session.uiMessages) {
|
|
231
|
-
if (m.role === roleName && m.type === 'tool' && !m.hasResult) {
|
|
232
|
-
m.hasResult = true;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
sendCrewMessage({
|
|
237
|
-
type: 'crew_turn_completed',
|
|
238
|
-
sessionId: session.id,
|
|
239
|
-
role: roleName
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
sendStatusUpdate(session);
|
|
243
|
-
// Cost/tokens/messages updated — debounced persist (coalesces rapid turn-ends)
|
|
244
|
-
debouncedSaveSessionMeta(session);
|
|
245
|
-
|
|
246
|
-
// 执行路由
|
|
247
|
-
if (routes.length > 0) {
|
|
248
|
-
session.round++;
|
|
249
|
-
|
|
250
|
-
// ★ Collect turn images for auto-attach (last 3, then clear)
|
|
251
|
-
const turnImages = roleState.turnImages || [];
|
|
252
|
-
roleState.turnImages = [];
|
|
253
|
-
|
|
254
|
-
const currentTask = roleState.currentTask;
|
|
255
|
-
for (const route of routes) {
|
|
256
|
-
if (!route.taskId && currentTask) {
|
|
257
|
-
route.taskId = currentTask.taskId;
|
|
258
|
-
route.taskTitle = currentTask.taskTitle;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// 通知前端进入 routing 状态
|
|
263
|
-
sendCrewMessage({
|
|
264
|
-
type: 'crew_routing',
|
|
265
|
-
sessionId: session.id,
|
|
266
|
-
fromRole: roleName,
|
|
267
|
-
routes: routes.map(r => ({ to: r.to, taskId: r.taskId, taskTitle: r.taskTitle })),
|
|
268
|
-
status: 'routing'
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const results = await Promise.allSettled(routes.map(route =>
|
|
272
|
-
executeRoute(session, roleName, route, turnImages)
|
|
273
|
-
));
|
|
274
|
-
for (const r of results) {
|
|
275
|
-
if (r.status === 'rejected') {
|
|
276
|
-
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// routing 完成,通知前端恢复正常状态
|
|
281
|
-
sendCrewMessage({
|
|
282
|
-
type: 'crew_routing',
|
|
283
|
-
sessionId: session.id,
|
|
284
|
-
fromRole: roleName,
|
|
285
|
-
status: 'done'
|
|
286
|
-
});
|
|
287
|
-
sendStatusUpdate(session);
|
|
288
|
-
} else {
|
|
289
|
-
// ★ No ROUTE found — resolve via the single fallback policy
|
|
290
|
-
// (task-330b §B item 3). The resolver enforces:
|
|
291
|
-
// - PM never auto-forwards to itself (parks pending instead)
|
|
292
|
-
// - non-PM with active task / routing intent → PM (auto-forward)
|
|
293
|
-
// - everyone else → null (process human queue)
|
|
294
|
-
const hasActiveTask = !!roleState.currentTask;
|
|
295
|
-
const hasRouteIntent = _detectRouteIntent(roleState.lastTurnText);
|
|
296
|
-
const fallbackTo = resolveFallbackTarget(session, roleName, 'missing-route', {
|
|
297
|
-
hasActiveTask,
|
|
298
|
-
hasRouteIntent,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
if (fallbackTo) {
|
|
302
|
-
// §B item 1 — record the metric on every fallback dispatch.
|
|
303
|
-
const reason = hasActiveTask ? 'has active task' : 'has routing intent';
|
|
304
|
-
recordRoutingEvent(session, 'fallback-forward', {
|
|
305
|
-
fromRole: roleName,
|
|
306
|
-
toRole: fallbackTo,
|
|
307
|
-
taskId: roleState.currentTask?.taskId || null,
|
|
308
|
-
note: reason,
|
|
309
|
-
});
|
|
310
|
-
// Also record the upstream cause so dashboards can split
|
|
311
|
-
// "missing ROUTE" events from the auto-forward dispatches.
|
|
312
|
-
recordRoutingEvent(session, 'missing-route', {
|
|
313
|
-
fromRole: roleName,
|
|
314
|
-
taskId: roleState.currentTask?.taskId || null,
|
|
315
|
-
note: reason,
|
|
316
|
-
});
|
|
317
|
-
// task-328: forward parser-clean displayBody (ROUTE residue removed)
|
|
318
|
-
// so PM sees the actual prose, not stray END markers.
|
|
319
|
-
console.log(`[Crew] ${roleName} turn ended without ROUTE (${reason}) — auto-forwarding to ${fallbackTo}`);
|
|
320
|
-
const forwardSource = roleState.lastTurnDisplayBody || roleState.lastTurnText || '';
|
|
321
|
-
const autoSummary = `[auto-forward: ${roleName} turn 结束但未输出 ROUTE 块 (${reason})]\n${forwardSource.slice(-800).trim()}`;
|
|
322
|
-
await executeRoute(session, roleName, {
|
|
323
|
-
to: fallbackTo,
|
|
324
|
-
summary: autoSummary,
|
|
325
|
-
taskId: roleState.currentTask?.taskId || null,
|
|
326
|
-
taskTitle: roleState.currentTask?.taskTitle || null,
|
|
327
|
-
});
|
|
328
|
-
} else {
|
|
329
|
-
// §B item 2 — PM no-auto-forward: when PM ends a turn without
|
|
330
|
-
// ROUTE we DO NOT self-route. Instead we record the metric and
|
|
331
|
-
// park the session (effectively pending) so the user's next
|
|
332
|
-
// input drives the next step. Same applies to non-PM roles
|
|
333
|
-
// with no active task / no routing intent.
|
|
334
|
-
if (roleName === session.decisionMaker && (hasActiveTask || hasRouteIntent)) {
|
|
335
|
-
recordRoutingEvent(session, 'missing-route', {
|
|
336
|
-
fromRole: roleName,
|
|
337
|
-
taskId: roleState.currentTask?.taskId || null,
|
|
338
|
-
note: 'pm-pending (no auto-forward)',
|
|
339
|
-
});
|
|
340
|
-
console.log(`[Crew] ${roleName} (PM) turn ended without ROUTE — staying pending (no self-forward)`);
|
|
341
|
-
}
|
|
342
|
-
const { processHumanQueue } = await import('./human-interaction.js');
|
|
343
|
-
await processHumanQueue(session);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} catch (error) {
|
|
349
|
-
if (error.name === 'AbortError') {
|
|
350
|
-
console.log(`[Crew] ${roleName} aborted`);
|
|
351
|
-
// 结算 abort 前累积的 usage,避免丢失 token
|
|
352
|
-
settleLastSeenUsage();
|
|
353
|
-
if (session.status === 'paused' && roleState.accumulatedText) {
|
|
354
|
-
const routes = parseRoutes(roleState.accumulatedText);
|
|
355
|
-
if (routes.length > 0 && session.pendingRoutes.length === 0) {
|
|
356
|
-
// Fill missing taskId from roleState.currentTask (same as normal path L170-175)
|
|
357
|
-
const currentTask = roleState.currentTask;
|
|
358
|
-
for (const route of routes) {
|
|
359
|
-
if (!route.taskId && currentTask) {
|
|
360
|
-
route.taskId = currentTask.taskId;
|
|
361
|
-
route.taskTitle = currentTask.taskTitle;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
session.pendingRoutes = routes.map(route => ({ fromRole: roleName, route }));
|
|
365
|
-
console.log(`[Crew] Saved ${routes.length} pending route(s) from aborted ${roleName}`);
|
|
366
|
-
}
|
|
367
|
-
roleState.accumulatedText = '';
|
|
368
|
-
}
|
|
369
|
-
} else {
|
|
370
|
-
console.error(`[Crew] ${roleName} error:`, error.message);
|
|
371
|
-
|
|
372
|
-
// 结算 error 前累积的 usage,避免丢失 token
|
|
373
|
-
settleLastSeenUsage();
|
|
374
|
-
|
|
375
|
-
// Step 1: 清理 roleState
|
|
376
|
-
endRoleStreaming(session, roleName);
|
|
377
|
-
const errorTurnText = roleState.accumulatedText;
|
|
378
|
-
roleState.query = null;
|
|
379
|
-
roleState.inputStream = null;
|
|
380
|
-
roleState.turnActive = false;
|
|
381
|
-
roleState.accumulatedText = '';
|
|
382
|
-
|
|
383
|
-
// Step 2: 错误分类
|
|
384
|
-
const classification = classifyRoleError(error);
|
|
385
|
-
roleState.consecutiveErrors++;
|
|
386
|
-
|
|
387
|
-
// Step 3: 通知前端
|
|
388
|
-
sendCrewMessage({
|
|
389
|
-
type: 'crew_role_error',
|
|
390
|
-
sessionId: session.id,
|
|
391
|
-
role: roleName,
|
|
392
|
-
error: error.message.substring(0, 500),
|
|
393
|
-
reason: classification.reason,
|
|
394
|
-
recoverable: classification.recoverable,
|
|
395
|
-
retryCount: roleState.consecutiveErrors
|
|
396
|
-
});
|
|
397
|
-
sendStatusUpdate(session);
|
|
398
|
-
|
|
399
|
-
// Step 4: 判断是否重试
|
|
400
|
-
const MAX_RETRIES = 3;
|
|
401
|
-
if (!classification.recoverable || roleState.consecutiveErrors > MAX_RETRIES) {
|
|
402
|
-
const exhausted = roleState.consecutiveErrors > MAX_RETRIES;
|
|
403
|
-
const errDetail = exhausted
|
|
404
|
-
? `角色 ${roleName} 连续 ${MAX_RETRIES} 次错误后停止重试。最后错误: ${error.message}`
|
|
405
|
-
: `角色 ${roleName} 不可恢复错误: ${error.message}`;
|
|
406
|
-
if (roleName !== session.decisionMaker) {
|
|
407
|
-
await dispatchToRole(session, session.decisionMaker, errDetail, 'system');
|
|
408
|
-
} else {
|
|
409
|
-
session.status = 'waiting_human';
|
|
410
|
-
sendCrewMessage({
|
|
411
|
-
type: 'crew_human_needed',
|
|
412
|
-
sessionId: session.id,
|
|
413
|
-
fromRole: roleName,
|
|
414
|
-
reason: 'error',
|
|
415
|
-
message: errDetail
|
|
416
|
-
});
|
|
417
|
-
sendStatusUpdate(session);
|
|
418
|
-
// Status changed to waiting_human — persist
|
|
419
|
-
saveSessionMeta(session).catch(e => console.warn('[Crew] Failed to save after error→human:', e.message));
|
|
420
|
-
}
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Step 5: 可恢复 → 保存摘要后 clear + 重建重试
|
|
425
|
-
console.log(`[Crew] ${roleName} attempting recovery (${classification.reason}), retry ${roleState.consecutiveErrors}/${MAX_RETRIES}`);
|
|
426
|
-
|
|
427
|
-
sendCrewOutput(session, 'system', 'system', {
|
|
428
|
-
type: 'assistant',
|
|
429
|
-
message: { role: 'assistant', content: [{
|
|
430
|
-
type: 'text',
|
|
431
|
-
text: `${roleName} 遇到 ${classification.reason},正在自动恢复 (${roleState.consecutiveErrors}/${MAX_RETRIES})...`
|
|
432
|
-
}] }
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
if (roleState.lastDispatchContent) {
|
|
436
|
-
// 保存工作摘要
|
|
437
|
-
await saveRoleWorkSummary(session, roleName, errorTurnText).catch(e =>
|
|
438
|
-
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
439
|
-
|
|
440
|
-
// 所有可恢复错误统一 clear + rebuild
|
|
441
|
-
await clearRoleSessionId(session.sharedDir, roleName);
|
|
442
|
-
const consecutiveErrors = roleState.consecutiveErrors;
|
|
443
|
-
await dispatchToRole(
|
|
444
|
-
session, roleName,
|
|
445
|
-
roleState.lastDispatchContent,
|
|
446
|
-
roleState.lastDispatchFrom || 'system',
|
|
447
|
-
roleState.lastDispatchTaskId,
|
|
448
|
-
roleState.lastDispatchTaskTitle
|
|
449
|
-
);
|
|
450
|
-
// 保持错误计数
|
|
451
|
-
const newState = session.roleStates.get(roleName);
|
|
452
|
-
if (newState) newState.consecutiveErrors = consecutiveErrors;
|
|
453
|
-
} else {
|
|
454
|
-
const msg = `角色 ${roleName} 已恢复(${classification.reason}),但无待重试消息。`;
|
|
455
|
-
if (roleName !== session.decisionMaker) {
|
|
456
|
-
await dispatchToRole(session, session.decisionMaker, msg, 'system');
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|