@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
package/crew/routing.js
ADDED
|
@@ -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
|
+
}
|
package/crew/session.js
ADDED
|
@@ -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
|
+
}
|