@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.
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Crew — 路由解析与执行
3
+ * parseRoutes, executeRoute, buildRoutePrompt, dispatchToRole
4
+ */
5
+ import { join } from 'path';
6
+ import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
7
+ import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban } from './task-files.js';
8
+ import { createRoleQuery } from './role-query.js';
9
+
10
+ /** Format role label */
11
+ function roleLabel(r) {
12
+ return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
13
+ }
14
+
15
+ /**
16
+ * 从累积文本中解析所有 ROUTE 块(支持多 ROUTE + task 字段)
17
+ * @returns {Array<{ to, summary, taskId, taskTitle }>}
18
+ */
19
+ export function parseRoutes(text) {
20
+ const routes = [];
21
+ const regex = /---ROUTE---\s*\n([\s\S]*?)---END_ROUTE---/g;
22
+ let match;
23
+
24
+ while ((match = regex.exec(text)) !== null) {
25
+ const block = match[1];
26
+ const toMatch = block.match(/to:\s*(.+)/i);
27
+ if (!toMatch) continue;
28
+
29
+ const summaryMatch = block.match(/summary:\s*([\s\S]+)/i);
30
+ const taskMatch = block.match(/^task:\s*(.+)/im);
31
+ const taskTitleMatch = block.match(/^taskTitle:\s*(.+)/im);
32
+
33
+ routes.push({
34
+ to: toMatch[1].trim().toLowerCase(),
35
+ summary: summaryMatch ? summaryMatch[1].trim() : '',
36
+ taskId: taskMatch ? taskMatch[1].trim() : null,
37
+ taskTitle: taskTitleMatch ? taskTitleMatch[1].trim() : null
38
+ });
39
+ }
40
+
41
+ return routes;
42
+ }
43
+
44
+ /**
45
+ * 执行路由
46
+ */
47
+ export async function executeRoute(session, fromRole, route) {
48
+ const { to, summary, taskId, taskTitle } = route;
49
+
50
+ // 如果 session 已暂停或停止,保存为 pendingRoutes
51
+ if (session.status === 'paused' || session.status === 'stopped') {
52
+ session.pendingRoutes.push({ fromRole, route });
53
+ console.log(`[Crew] Session ${session.status}, route saved as pending: ${fromRole} -> ${to}`);
54
+ return;
55
+ }
56
+
57
+ // Task 文件自动管理(fire-and-forget)
58
+ if (taskId && summary) {
59
+ const fromRoleConfig = session.roles.get(fromRole);
60
+ if (fromRoleConfig?.isDecisionMaker && taskTitle && to !== 'human') {
61
+ ensureTaskFile(session, taskId, taskTitle, to, summary)
62
+ .catch(e => console.warn(`[Crew] Failed to create task file ${taskId}:`, e.message));
63
+ }
64
+ appendTaskRecord(session, taskId, fromRole, summary)
65
+ .catch(e => console.warn(`[Crew] Failed to append task record ${taskId}:`, e.message));
66
+
67
+ // 更新工作看板:推断状态
68
+ const { getMessages } = await import('../crew-i18n.js');
69
+ const m = getMessages(session.language || 'zh-CN');
70
+ const toRoleConfig = session.roles.get(to);
71
+ let status = m.kanbanStatusDev;
72
+ if (toRoleConfig) {
73
+ switch (toRoleConfig.roleType) {
74
+ case 'reviewer': status = m.kanbanStatusReview; break;
75
+ case 'tester': status = m.kanbanStatusTest; break;
76
+ default:
77
+ if (toRoleConfig.isDecisionMaker) status = m.kanbanStatusDecision;
78
+ }
79
+ }
80
+ updateKanban(session, {
81
+ taskId, taskTitle, assignee: to,
82
+ status, summary: summary.substring(0, 100)
83
+ }).catch(e => console.warn(`[Crew] Failed to update kanban:`, e.message));
84
+ }
85
+
86
+ // 发送路由消息(UI 显示)
87
+ sendCrewOutput(session, fromRole, 'route', null, { routeTo: to, routeSummary: summary });
88
+
89
+ // 路由到 human
90
+ if (to === 'human') {
91
+ session.status = 'waiting_human';
92
+ session.waitingHumanContext = {
93
+ fromRole,
94
+ reason: 'requested',
95
+ message: summary
96
+ };
97
+ sendCrewMessage({
98
+ type: 'crew_human_needed',
99
+ sessionId: session.id,
100
+ fromRole,
101
+ reason: 'requested',
102
+ message: summary
103
+ });
104
+ sendStatusUpdate(session);
105
+ return;
106
+ }
107
+
108
+ // 路由到指定角色
109
+ if (session.roles.has(to)) {
110
+ if (session.humanMessageQueue.length > 0) {
111
+ const { processHumanQueue } = await import('./human-interaction.js');
112
+ await processHumanQueue(session);
113
+ } else {
114
+ const taskPrompt = buildRoutePrompt(fromRole, summary, session);
115
+ await dispatchToRole(session, to, taskPrompt, fromRole, taskId, taskTitle);
116
+ }
117
+ } else {
118
+ console.warn(`[Crew] Unknown route target: ${to}`);
119
+ const errorMsg = `路由目标 "${to}" 不存在。来自 ${fromRole} 的消息: ${summary}`;
120
+ await dispatchToRole(session, session.decisionMaker, errorMsg, 'system');
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 构建路由转发的 prompt
126
+ */
127
+ export function buildRoutePrompt(fromRole, summary, session) {
128
+ const fromRoleConfig = session.roles.get(fromRole);
129
+ const fromName = fromRoleConfig ? roleLabel(fromRoleConfig) : fromRole;
130
+ return `来自 ${fromName} 的消息:\n${summary}\n\n请开始你的工作。完成后通过 ROUTE 块传递给下一个角色。`;
131
+ }
132
+
133
+ /**
134
+ * 向角色发送消息
135
+ */
136
+ export async function dispatchToRole(session, roleName, content, fromSource, taskId, taskTitle) {
137
+ if (session.status === 'paused' || session.status === 'stopped' || session.status === 'initializing') {
138
+ console.log(`[Crew] Session ${session.status}, skipping dispatch to ${roleName}`);
139
+ return;
140
+ }
141
+
142
+ let roleState = session.roleStates.get(roleName);
143
+
144
+ // 如果角色没有 query 实例,创建一个(支持 resume)
145
+ if (!roleState || !roleState.query || !roleState.inputStream) {
146
+ roleState = await createRoleQuery(session, roleName);
147
+ }
148
+
149
+ // 设置 task
150
+ if (taskId) {
151
+ roleState.currentTask = { taskId, taskTitle };
152
+ }
153
+
154
+ // Task 上下文注入
155
+ const effectiveTaskId = taskId || roleState.currentTask?.taskId;
156
+ if (effectiveTaskId && typeof content === 'string') {
157
+ const taskContent = await readTaskFile(session, effectiveTaskId);
158
+ if (taskContent) {
159
+ content = `${content}\n\n---\n<task-context file="context/features/${effectiveTaskId}.md">\n${taskContent}\n</task-context>`;
160
+ }
161
+ }
162
+
163
+ // 看板上下文注入(角色重启后知道全局状态)
164
+ if (typeof content === 'string') {
165
+ const kanbanContent = await readKanban(session);
166
+ if (kanbanContent) {
167
+ content = `${content}\n\n---\n<kanban file="context/kanban.md">\n${kanbanContent}\n</kanban>`;
168
+ }
169
+ }
170
+
171
+ // 记录消息历史
172
+ session.messageHistory.push({
173
+ from: fromSource,
174
+ to: roleName,
175
+ content: typeof content === 'string' ? content.substring(0, 200) : '...',
176
+ taskId: taskId || roleState.currentTask?.taskId || null,
177
+ timestamp: Date.now()
178
+ });
179
+
180
+ // 发送
181
+ roleState.lastDispatchContent = content;
182
+ roleState.lastDispatchFrom = fromSource;
183
+ roleState.lastDispatchTaskId = taskId || null;
184
+ roleState.lastDispatchTaskTitle = taskTitle || null;
185
+ roleState.turnActive = true;
186
+ roleState.accumulatedText = '';
187
+ roleState.inputStream.enqueue({
188
+ type: 'user',
189
+ message: { role: 'user', content }
190
+ });
191
+
192
+ sendStatusUpdate(session);
193
+ console.log(`[Crew] Dispatched to ${roleName} from ${fromSource}${taskId ? ` (task: ${taskId})` : ''}`);
194
+ }
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Crew Session — 核心数据结构、角色展开和 Session 生命周期管理
3
+ */
4
+ import { promises as fs } from 'fs';
5
+ import { join, isAbsolute } from 'path';
6
+ import ctx from '../context.js';
7
+ import { getMessages } from '../crew-i18n.js';
8
+ import { initWorktrees } from './worktree.js';
9
+ import { initSharedDir, writeRoleClaudeMd, updateSharedClaudeMd } from './shared-dir.js';
10
+ import {
11
+ loadCrewIndex, upsertCrewIndex, removeFromCrewIndex,
12
+ loadSessionMeta, saveSessionMeta, loadSessionMessages, getMaxShardIndex
13
+ } from './persistence.js';
14
+ import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
15
+
16
+ // =====================================================================
17
+ // Data Structures
18
+ // =====================================================================
19
+
20
+ /** @type {Map<string, CrewSession>} */
21
+ export const crewSessions = new Map();
22
+
23
+ // =====================================================================
24
+ // Role Multi-Instance Expansion
25
+ // =====================================================================
26
+
27
+ const SHORT_PREFIX = {
28
+ developer: 'dev',
29
+ tester: 'test',
30
+ reviewer: 'rev'
31
+ };
32
+
33
+ const EXPANDABLE_ROLES = new Set(['developer', 'tester', 'reviewer']);
34
+
35
+ /**
36
+ * 展开角色列表:count > 1 的执行者角色展开为多个实例
37
+ */
38
+ export function expandRoles(roles) {
39
+ const devRole = roles.find(r => r.name === 'developer');
40
+ const devCount = devRole?.count > 1 ? devRole.count : 1;
41
+
42
+ const expanded = [];
43
+ for (const role of roles) {
44
+ const isExpandable = EXPANDABLE_ROLES.has(role.name);
45
+ const count = isExpandable ? devCount : 1;
46
+
47
+ if (count <= 1) {
48
+ expanded.push({
49
+ ...role,
50
+ roleType: role.name,
51
+ groupIndex: isExpandable ? 1 : 0
52
+ });
53
+ } else {
54
+ const prefix = SHORT_PREFIX[role.name] || role.name;
55
+ for (let i = 1; i <= count; i++) {
56
+ expanded.push({
57
+ ...role,
58
+ name: `${prefix}-${i}`,
59
+ displayName: `${role.displayName}-${i}`,
60
+ roleType: role.name,
61
+ groupIndex: i,
62
+ count: undefined
63
+ });
64
+ }
65
+ }
66
+ }
67
+ return expanded;
68
+ }
69
+
70
+ /** Format role label */
71
+ export function roleLabel(r) {
72
+ return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
73
+ }
74
+
75
+ // =====================================================================
76
+ // Path Validation
77
+ // =====================================================================
78
+
79
+ function isValidProjectDir(dir) {
80
+ if (!dir || typeof dir !== 'string') return false;
81
+ if (!isAbsolute(dir)) return false;
82
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(dir)) return false;
83
+ return true;
84
+ }
85
+
86
+ // =====================================================================
87
+ // Session Lifecycle
88
+ // =====================================================================
89
+
90
+ /**
91
+ * 查找指定 projectDir 的已有 crew session
92
+ */
93
+ async function findExistingSessionByProjectDir(projectDir) {
94
+ const normalizedDir = projectDir.replace(/\/+$/, '');
95
+
96
+ for (const [, session] of crewSessions) {
97
+ if (session.projectDir.replace(/\/+$/, '') === normalizedDir
98
+ && session.status !== 'completed') {
99
+ return { sessionId: session.id, source: 'active' };
100
+ }
101
+ }
102
+
103
+ const index = await loadCrewIndex();
104
+ const agentId = ctx.CONFIG?.agentName || null;
105
+ const match = index.find(e =>
106
+ e.projectDir.replace(/\/+$/, '') === normalizedDir
107
+ && (!agentId || !e.agentId || e.agentId === agentId)
108
+ && e.status !== 'completed'
109
+ );
110
+
111
+ if (match) {
112
+ const meta = await loadSessionMeta(match.sharedDir);
113
+ if (meta) return { sessionId: match.sessionId, source: 'index' };
114
+ await removeFromCrewIndex(match.sessionId);
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * 创建 Crew Session
122
+ */
123
+ export async function createCrewSession(msg) {
124
+ const {
125
+ sessionId,
126
+ projectDir,
127
+ sharedDir: sharedDirRel,
128
+ name,
129
+ roles: rawRoles = [],
130
+ teamType = 'dev',
131
+ language = 'zh-CN',
132
+ userId,
133
+ username
134
+ } = msg;
135
+
136
+ // 同目录检查
137
+ const existingSession = await findExistingSessionByProjectDir(projectDir);
138
+ if (existingSession) {
139
+ console.log(`[Crew] Found existing session for ${projectDir}: ${existingSession.sessionId}, auto-resuming`);
140
+ await resumeCrewSession({ sessionId: existingSession.sessionId, userId, username });
141
+ return;
142
+ }
143
+
144
+ const roles = expandRoles(rawRoles);
145
+ const sharedDir = sharedDirRel?.startsWith('/')
146
+ ? sharedDirRel
147
+ : join(projectDir, sharedDirRel || '.crew');
148
+ const decisionMaker = roles.find(r => r.isDecisionMaker)?.name || roles[0]?.name || null;
149
+
150
+ const session = {
151
+ id: sessionId,
152
+ projectDir,
153
+ sharedDir,
154
+ name: name || '',
155
+ roles: new Map(roles.map(r => [r.name, r])),
156
+ roleStates: new Map(),
157
+ decisionMaker,
158
+ status: 'initializing',
159
+ round: 0,
160
+ costUsd: 0,
161
+ totalInputTokens: 0,
162
+ totalOutputTokens: 0,
163
+ messageHistory: [],
164
+ uiMessages: [],
165
+ humanMessageQueue: [],
166
+ waitingHumanContext: null,
167
+ pendingRoutes: [],
168
+ features: new Map(),
169
+ _completedTaskIds: new Set(),
170
+ initProgress: null,
171
+ userId,
172
+ username,
173
+ agentId: ctx.CONFIG?.agentName || null,
174
+ teamType,
175
+ language,
176
+ createdAt: Date.now()
177
+ };
178
+
179
+ crewSessions.set(sessionId, session);
180
+
181
+ sendCrewMessage({
182
+ type: 'crew_session_created',
183
+ sessionId,
184
+ projectDir,
185
+ sharedDir,
186
+ name: name || '',
187
+ roles: roles.map(r => ({
188
+ name: r.name,
189
+ displayName: r.displayName,
190
+ icon: r.icon,
191
+ description: r.description,
192
+ isDecisionMaker: r.isDecisionMaker || false,
193
+ model: r.model,
194
+ roleType: r.roleType,
195
+ groupIndex: r.groupIndex
196
+ })),
197
+ decisionMaker,
198
+ userId,
199
+ username
200
+ });
201
+
202
+ sendStatusUpdate(session);
203
+
204
+ try {
205
+ session.initProgress = 'roles';
206
+ sendStatusUpdate(session);
207
+ await initSharedDir(sharedDir, roles, projectDir, language);
208
+
209
+ const groupIndices = [...new Set(roles.filter(r => r.groupIndex > 0).map(r => r.groupIndex))];
210
+ if (groupIndices.length > 0) {
211
+ session.initProgress = 'worktrees';
212
+ sendStatusUpdate(session);
213
+ }
214
+ const worktreeMap = await initWorktrees(projectDir, roles);
215
+
216
+ for (const role of roles) {
217
+ if (role.groupIndex > 0 && worktreeMap.has(role.groupIndex)) {
218
+ role.workDir = worktreeMap.get(role.groupIndex);
219
+ await writeRoleClaudeMd(sharedDir, role, language);
220
+ }
221
+ }
222
+
223
+ await upsertCrewIndex(session);
224
+ await saveSessionMeta(session);
225
+
226
+ if (session.status === 'initializing') {
227
+ session.status = 'running';
228
+ }
229
+ session.initProgress = null;
230
+ sendStatusUpdate(session);
231
+ } catch (e) {
232
+ console.error('[Crew] Session initialization failed:', e);
233
+ if (session.status === 'initializing') {
234
+ session.status = 'running';
235
+ }
236
+ session.initProgress = null;
237
+ sendStatusUpdate(session);
238
+ sendCrewMessage({
239
+ type: 'crew_output',
240
+ sessionId,
241
+ roleName: 'system',
242
+ roleIcon: 'S',
243
+ roleDisplayName: '系统',
244
+ content: `工作环境初始化失败: ${e.message}`,
245
+ isTurnEnd: true
246
+ });
247
+ }
248
+
249
+ return session;
250
+ }
251
+
252
+ // =====================================================================
253
+ // List & Resume Sessions
254
+ // =====================================================================
255
+
256
+ /**
257
+ * 列出所有 crew sessions
258
+ */
259
+ export async function handleListCrewSessions(msg) {
260
+ const { requestId, _requestClientId } = msg;
261
+ const index = await loadCrewIndex();
262
+
263
+ const agentId = ctx.CONFIG?.agentName || null;
264
+ const filtered = agentId
265
+ ? index.filter(e => !e.agentId || e.agentId === agentId)
266
+ : index;
267
+
268
+ for (const entry of filtered) {
269
+ const active = crewSessions.get(entry.sessionId);
270
+ if (active) {
271
+ entry.status = active.status;
272
+ }
273
+ }
274
+
275
+ ctx.sendToServer({
276
+ type: 'crew_sessions_list',
277
+ requestId,
278
+ _requestClientId,
279
+ sessions: filtered
280
+ });
281
+ }
282
+
283
+ /**
284
+ * 检查工作目录下是否存在 .crew 目录
285
+ */
286
+ export async function handleCheckCrewExists(msg) {
287
+ const { projectDir, requestId, _requestClientId } = msg;
288
+ if (!projectDir || !isValidProjectDir(projectDir)) {
289
+ ctx.sendToServer({
290
+ type: 'crew_exists_result',
291
+ requestId,
292
+ _requestClientId,
293
+ exists: false,
294
+ error: 'projectDir is required'
295
+ });
296
+ return;
297
+ }
298
+
299
+ const crewDir = join(projectDir, '.crew');
300
+ try {
301
+ const stat = await fs.stat(crewDir);
302
+ if (stat.isDirectory()) {
303
+ let sessionInfo = null;
304
+ try {
305
+ const sessionPath = join(crewDir, 'session.json');
306
+ const data = await fs.readFile(sessionPath, 'utf-8');
307
+ sessionInfo = JSON.parse(data);
308
+ } catch {}
309
+ ctx.sendToServer({
310
+ type: 'crew_exists_result',
311
+ requestId,
312
+ _requestClientId,
313
+ exists: true,
314
+ projectDir,
315
+ sessionInfo
316
+ });
317
+ } else {
318
+ ctx.sendToServer({
319
+ type: 'crew_exists_result',
320
+ requestId,
321
+ _requestClientId,
322
+ exists: false,
323
+ projectDir
324
+ });
325
+ }
326
+ } catch {
327
+ ctx.sendToServer({
328
+ type: 'crew_exists_result',
329
+ requestId,
330
+ _requestClientId,
331
+ exists: false,
332
+ projectDir
333
+ });
334
+ }
335
+ }
336
+
337
+ /**
338
+ * 删除工作目录下的 .crew 目录
339
+ */
340
+ export async function handleDeleteCrewDir(msg) {
341
+ const { projectDir } = msg;
342
+ if (!isValidProjectDir(projectDir)) return;
343
+ const crewDir = join(projectDir, '.crew');
344
+ try {
345
+ await fs.rm(crewDir, { recursive: true, force: true });
346
+ } catch {}
347
+ }
348
+
349
+ /**
350
+ * 恢复已停止的 crew session
351
+ */
352
+ export async function resumeCrewSession(msg) {
353
+ const { sessionId, userId, username } = msg;
354
+
355
+ if (crewSessions.has(sessionId)) {
356
+ const session = crewSessions.get(sessionId);
357
+ const roles = Array.from(session.roles.values());
358
+ if ((!session.uiMessages || session.uiMessages.length === 0) && session.sharedDir) {
359
+ const loaded = await loadSessionMessages(session.sharedDir);
360
+ session.uiMessages = loaded.messages;
361
+ }
362
+ const cleanedMessages = (session.uiMessages || []).map(m => {
363
+ const { _streaming, ...rest } = m;
364
+ return rest;
365
+ });
366
+ const hasOlderMessages = await getMaxShardIndex(session.sharedDir) > 0;
367
+
368
+ sendCrewMessage({
369
+ type: 'crew_session_restored',
370
+ sessionId,
371
+ projectDir: session.projectDir,
372
+ sharedDir: session.sharedDir,
373
+ name: session.name || '',
374
+ roles: roles.map(r => ({
375
+ name: r.name, displayName: r.displayName, icon: r.icon,
376
+ description: r.description, isDecisionMaker: r.isDecisionMaker || false,
377
+ groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
378
+ })),
379
+ decisionMaker: session.decisionMaker,
380
+ userId: session.userId,
381
+ username: session.username,
382
+ uiMessages: cleanedMessages,
383
+ hasOlderMessages
384
+ });
385
+ sendStatusUpdate(session);
386
+ return;
387
+ }
388
+
389
+ const index = await loadCrewIndex();
390
+ const indexEntry = index.find(e => e.sessionId === sessionId);
391
+ if (!indexEntry) {
392
+ sendCrewMessage({ type: 'error', sessionId, message: 'Crew session not found in index' });
393
+ return;
394
+ }
395
+
396
+ const meta = await loadSessionMeta(indexEntry.sharedDir);
397
+ if (!meta) {
398
+ sendCrewMessage({ type: 'error', sessionId, message: 'Crew session metadata not found' });
399
+ return;
400
+ }
401
+
402
+ const roles = meta.roles || [];
403
+ const decisionMaker = meta.decisionMaker || roles[0]?.name || null;
404
+ const session = {
405
+ id: sessionId,
406
+ projectDir: meta.projectDir,
407
+ sharedDir: meta.sharedDir || indexEntry.sharedDir,
408
+ name: meta.name || '',
409
+ roles: new Map(roles.map(r => [r.name, r])),
410
+ roleStates: new Map(),
411
+ decisionMaker,
412
+ status: 'waiting_human',
413
+ round: meta.round || 0,
414
+ costUsd: meta.costUsd || 0,
415
+ totalInputTokens: meta.totalInputTokens || 0,
416
+ totalOutputTokens: meta.totalOutputTokens || 0,
417
+ messageHistory: [],
418
+ uiMessages: [],
419
+ humanMessageQueue: [],
420
+ waitingHumanContext: null,
421
+ pendingRoutes: [],
422
+ features: new Map((meta.features || []).map(f => [f.taskId, f])),
423
+ _completedTaskIds: new Set(meta._completedTaskIds || []),
424
+ userId: userId || meta.userId,
425
+ username: username || meta.username,
426
+ agentId: meta.agentId || ctx.CONFIG?.agentName || null,
427
+ teamType: meta.teamType || 'dev',
428
+ language: meta.language || 'zh-CN',
429
+ createdAt: meta.createdAt || Date.now()
430
+ };
431
+ crewSessions.set(sessionId, session);
432
+
433
+ const loaded = await loadSessionMessages(session.sharedDir);
434
+ session.uiMessages = loaded.messages;
435
+
436
+ sendCrewMessage({
437
+ type: 'crew_session_restored',
438
+ sessionId,
439
+ projectDir: session.projectDir,
440
+ sharedDir: session.sharedDir,
441
+ name: session.name || '',
442
+ roles: roles.map(r => ({
443
+ name: r.name, displayName: r.displayName, icon: r.icon,
444
+ description: r.description, isDecisionMaker: r.isDecisionMaker || false,
445
+ groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
446
+ })),
447
+ decisionMaker,
448
+ userId: session.userId,
449
+ username: session.username,
450
+ uiMessages: session.uiMessages,
451
+ hasOlderMessages: loaded.hasOlderMessages
452
+ });
453
+ sendStatusUpdate(session);
454
+
455
+ await upsertCrewIndex(session);
456
+ await saveSessionMeta(session);
457
+
458
+ console.log(`[Crew] Session ${sessionId} resumed, waiting for human input`);
459
+ }
460
+
461
+ /**
462
+ * 更新 crew session 的 name
463
+ */
464
+ export async function handleUpdateCrewSession(msg) {
465
+ const { sessionId, name } = msg;
466
+ const session = crewSessions.get(sessionId);
467
+ if (!session) {
468
+ console.warn(`[Crew] Session not found for update: ${sessionId}`);
469
+ return;
470
+ }
471
+ if (name !== undefined) session.name = name;
472
+ await saveSessionMeta(session);
473
+ await upsertCrewIndex(session);
474
+ }