@yeaft/webchat-agent 0.0.234 → 0.0.236
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/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/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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 人工交互
|
|
3
|
+
* handleCrewHumanInput, processHumanQueue
|
|
4
|
+
*/
|
|
5
|
+
import { dispatchToRole } from './routing.js';
|
|
6
|
+
import { sendStatusUpdate } from './ui-messages.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 处理人的输入
|
|
10
|
+
*/
|
|
11
|
+
export async function handleCrewHumanInput(msg) {
|
|
12
|
+
// Lazy import to avoid circular dependency
|
|
13
|
+
const { crewSessions } = await import('./session.js');
|
|
14
|
+
|
|
15
|
+
const { sessionId, content, targetRole, files } = msg;
|
|
16
|
+
const session = crewSessions.get(sessionId);
|
|
17
|
+
if (!session) {
|
|
18
|
+
console.warn(`[Crew] Session not found: ${sessionId}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Build dispatch content (supports image attachments)
|
|
23
|
+
function buildHumanContent(prefix, text) {
|
|
24
|
+
if (files && files.length > 0) {
|
|
25
|
+
const blocks = [];
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (file.isImage || file.mimeType?.startsWith('image/')) {
|
|
28
|
+
blocks.push({
|
|
29
|
+
type: 'image',
|
|
30
|
+
source: { type: 'base64', media_type: file.mimeType, data: file.data }
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
blocks.push({ type: 'text', text: `${prefix}\n${text}` });
|
|
35
|
+
return blocks;
|
|
36
|
+
}
|
|
37
|
+
return `${prefix}\n${text}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 记录到 uiMessages 用于恢复时重放
|
|
41
|
+
session.uiMessages.push({
|
|
42
|
+
role: 'human', roleIcon: '', roleName: '你',
|
|
43
|
+
type: 'text', content,
|
|
44
|
+
timestamp: Date.now()
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 如果在等待人工介入
|
|
48
|
+
if (session.status === 'waiting_human') {
|
|
49
|
+
const waitingContext = session.waitingHumanContext;
|
|
50
|
+
session.status = 'running';
|
|
51
|
+
session.waitingHumanContext = null;
|
|
52
|
+
sendStatusUpdate(session);
|
|
53
|
+
|
|
54
|
+
const target = targetRole || waitingContext?.fromRole || session.decisionMaker;
|
|
55
|
+
await dispatchToRole(session, target, buildHumanContent('人工回复:', content), 'human');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 解析 @role 指令
|
|
60
|
+
const atMatch = content.match(/^@(\S+)\s*([\s\S]*)/);
|
|
61
|
+
if (atMatch) {
|
|
62
|
+
const atTarget = atMatch[1];
|
|
63
|
+
const message = atMatch[2].trim() || content;
|
|
64
|
+
|
|
65
|
+
let target = null;
|
|
66
|
+
for (const [name, role] of session.roles) {
|
|
67
|
+
if (name === atTarget.toLowerCase()) {
|
|
68
|
+
target = name;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (role.displayName === atTarget) {
|
|
72
|
+
target = name;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (target) {
|
|
78
|
+
await dispatchToRole(session, target, buildHumanContent('人工消息:', message), 'human');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 默认发给决策者
|
|
84
|
+
const target = targetRole || session.decisionMaker;
|
|
85
|
+
await dispatchToRole(session, target, buildHumanContent('人工消息:', content), 'human');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 处理排队的人的消息
|
|
90
|
+
*/
|
|
91
|
+
export async function processHumanQueue(session) {
|
|
92
|
+
if (session.humanMessageQueue.length === 0) return;
|
|
93
|
+
if (session._processingHumanQueue) return;
|
|
94
|
+
session._processingHumanQueue = true;
|
|
95
|
+
try {
|
|
96
|
+
const msgs = session.humanMessageQueue.splice(0);
|
|
97
|
+
if (msgs.length === 1) {
|
|
98
|
+
const humanPrompt = `人工消息:\n${msgs[0].content}`;
|
|
99
|
+
await dispatchToRole(session, msgs[0].target, humanPrompt, 'human');
|
|
100
|
+
} else {
|
|
101
|
+
const byTarget = new Map();
|
|
102
|
+
for (const m of msgs) {
|
|
103
|
+
if (!byTarget.has(m.target)) byTarget.set(m.target, []);
|
|
104
|
+
byTarget.get(m.target).push(m.content);
|
|
105
|
+
}
|
|
106
|
+
for (const [target, contents] of byTarget) {
|
|
107
|
+
const combined = contents.join('\n\n---\n\n');
|
|
108
|
+
const humanPrompt = `人工消息:\n你有 ${contents.length} 条待处理消息,请一并分析并用多个 ROUTE 块并行分配:\n\n${combined}`;
|
|
109
|
+
await dispatchToRole(session, target, humanPrompt, 'human');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
session._processingHumanQueue = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 持久化管理
|
|
3
|
+
* Session 索引 (~/.claude/crew-sessions.json)、session 元数据、消息分片
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
// =====================================================================
|
|
10
|
+
// Crew Session Index (~/.claude/crew-sessions.json)
|
|
11
|
+
// =====================================================================
|
|
12
|
+
|
|
13
|
+
const CREW_INDEX_PATH = join(homedir(), '.claude', 'crew-sessions.json');
|
|
14
|
+
|
|
15
|
+
// 写入锁:防止并发写入导致文件损坏
|
|
16
|
+
let _indexWriteLock = Promise.resolve();
|
|
17
|
+
|
|
18
|
+
export async function loadCrewIndex() {
|
|
19
|
+
try { return JSON.parse(await fs.readFile(CREW_INDEX_PATH, 'utf-8')); }
|
|
20
|
+
catch { return []; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function saveCrewIndex(index) {
|
|
24
|
+
const doWrite = async () => {
|
|
25
|
+
await fs.mkdir(join(homedir(), '.claude'), { recursive: true });
|
|
26
|
+
const data = JSON.stringify(index, null, 2);
|
|
27
|
+
// 先写临时文件再 rename,保证原子性
|
|
28
|
+
const tmpPath = CREW_INDEX_PATH + '.tmp';
|
|
29
|
+
await fs.writeFile(tmpPath, data);
|
|
30
|
+
await fs.rename(tmpPath, CREW_INDEX_PATH);
|
|
31
|
+
};
|
|
32
|
+
// 串行化写入
|
|
33
|
+
_indexWriteLock = _indexWriteLock.then(doWrite, doWrite);
|
|
34
|
+
return _indexWriteLock;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sessionToIndexEntry(session) {
|
|
38
|
+
return {
|
|
39
|
+
sessionId: session.id,
|
|
40
|
+
projectDir: session.projectDir,
|
|
41
|
+
sharedDir: session.sharedDir,
|
|
42
|
+
status: session.status,
|
|
43
|
+
name: session.name || '',
|
|
44
|
+
userId: session.userId,
|
|
45
|
+
username: session.username,
|
|
46
|
+
agentId: session.agentId || null,
|
|
47
|
+
createdAt: session.createdAt,
|
|
48
|
+
updatedAt: Date.now()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function upsertCrewIndex(session) {
|
|
53
|
+
const index = await loadCrewIndex();
|
|
54
|
+
const entry = sessionToIndexEntry(session);
|
|
55
|
+
const idx = index.findIndex(e => e.sessionId === session.id);
|
|
56
|
+
if (idx >= 0) index[idx] = entry; else index.push(entry);
|
|
57
|
+
await saveCrewIndex(index);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function removeFromCrewIndex(sessionId) {
|
|
61
|
+
// Lazy import to avoid circular dependency
|
|
62
|
+
const { crewSessions } = await import('./session.js');
|
|
63
|
+
|
|
64
|
+
const index = await loadCrewIndex();
|
|
65
|
+
const entry = index.find(e => e.sessionId === sessionId);
|
|
66
|
+
const filtered = index.filter(e => e.sessionId !== sessionId);
|
|
67
|
+
if (filtered.length !== index.length) {
|
|
68
|
+
await saveCrewIndex(filtered);
|
|
69
|
+
console.log(`[Crew] Removed session ${sessionId} from index`);
|
|
70
|
+
}
|
|
71
|
+
// 从内存中也移除(防止 sendConversationList 重新加入)
|
|
72
|
+
if (crewSessions.has(sessionId)) {
|
|
73
|
+
crewSessions.delete(sessionId);
|
|
74
|
+
console.log(`[Crew] Removed session ${sessionId} from active sessions`);
|
|
75
|
+
}
|
|
76
|
+
// 删除磁盘上的 session 数据文件
|
|
77
|
+
const sharedDir = entry?.sharedDir;
|
|
78
|
+
if (sharedDir) {
|
|
79
|
+
try {
|
|
80
|
+
for (const file of ['session.json', 'messages.json']) {
|
|
81
|
+
await fs.unlink(join(sharedDir, file)).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
// Clean up message shard files
|
|
84
|
+
await cleanupMessageShards(sharedDir);
|
|
85
|
+
console.log(`[Crew] Cleaned session files in ${sharedDir}`);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn(`[Crew] Failed to clean session files:`, e.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// =====================================================================
|
|
93
|
+
// Session Metadata (.crew/session.json)
|
|
94
|
+
// =====================================================================
|
|
95
|
+
|
|
96
|
+
const MESSAGE_SHARD_SIZE = 256 * 1024; // 256KB per shard
|
|
97
|
+
|
|
98
|
+
export async function saveSessionMeta(session) {
|
|
99
|
+
const meta = {
|
|
100
|
+
sessionId: session.id,
|
|
101
|
+
projectDir: session.projectDir,
|
|
102
|
+
sharedDir: session.sharedDir,
|
|
103
|
+
name: session.name || '',
|
|
104
|
+
status: session.status,
|
|
105
|
+
roles: Array.from(session.roles.values()).map(r => ({
|
|
106
|
+
name: r.name, displayName: r.displayName, icon: r.icon,
|
|
107
|
+
description: r.description, isDecisionMaker: r.isDecisionMaker || false,
|
|
108
|
+
groupIndex: r.groupIndex, roleType: r.roleType, model: r.model
|
|
109
|
+
})),
|
|
110
|
+
decisionMaker: session.decisionMaker,
|
|
111
|
+
round: session.round,
|
|
112
|
+
createdAt: session.createdAt,
|
|
113
|
+
updatedAt: Date.now(),
|
|
114
|
+
userId: session.userId,
|
|
115
|
+
username: session.username,
|
|
116
|
+
agentId: session.agentId || null,
|
|
117
|
+
teamType: session.teamType || 'dev',
|
|
118
|
+
language: session.language || 'zh-CN',
|
|
119
|
+
costUsd: session.costUsd,
|
|
120
|
+
totalInputTokens: session.totalInputTokens,
|
|
121
|
+
totalOutputTokens: session.totalOutputTokens,
|
|
122
|
+
features: Array.from(session.features.values()),
|
|
123
|
+
_completedTaskIds: Array.from(session._completedTaskIds || [])
|
|
124
|
+
};
|
|
125
|
+
await fs.writeFile(join(session.sharedDir, 'session.json'), JSON.stringify(meta, null, 2));
|
|
126
|
+
// 保存 UI 消息历史(用于恢复时重放)
|
|
127
|
+
if (session.uiMessages && session.uiMessages.length > 0) {
|
|
128
|
+
// 清理 _streaming 标记后保存
|
|
129
|
+
const cleaned = session.uiMessages.map(m => {
|
|
130
|
+
const { _streaming, ...rest } = m;
|
|
131
|
+
return rest;
|
|
132
|
+
});
|
|
133
|
+
const json = JSON.stringify(cleaned);
|
|
134
|
+
// 超过阈值时直接归档(rotateMessages 内部写两个文件,避免双写)
|
|
135
|
+
if (json.length > MESSAGE_SHARD_SIZE && !session._rotating) {
|
|
136
|
+
await rotateMessages(session, cleaned);
|
|
137
|
+
} else {
|
|
138
|
+
await fs.writeFile(join(session.sharedDir, 'messages.json'), json);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 归档旧消息到分片文件(logrotate 风格)
|
|
145
|
+
*/
|
|
146
|
+
async function rotateMessages(session, cleaned) {
|
|
147
|
+
session._rotating = true;
|
|
148
|
+
try {
|
|
149
|
+
const halfLen = Math.floor(cleaned.length / 2);
|
|
150
|
+
let splitIdx = halfLen;
|
|
151
|
+
for (let i = halfLen; i > Math.max(0, halfLen - 20); i--) {
|
|
152
|
+
if (cleaned[i].type === 'route' || cleaned[i].type === 'system') {
|
|
153
|
+
splitIdx = i + 1;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (splitIdx === halfLen) {
|
|
158
|
+
for (let i = halfLen + 1; i < Math.min(cleaned.length - 1, halfLen + 20); i++) {
|
|
159
|
+
if (cleaned[i].type === 'route' || cleaned[i].type === 'system') {
|
|
160
|
+
splitIdx = i + 1;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
splitIdx = Math.max(1, Math.min(splitIdx, cleaned.length - 1));
|
|
166
|
+
|
|
167
|
+
const archivePart = cleaned.slice(0, splitIdx);
|
|
168
|
+
const remainPart = cleaned.slice(splitIdx);
|
|
169
|
+
|
|
170
|
+
const maxShard = await getMaxShardIndex(session.sharedDir);
|
|
171
|
+
for (let i = maxShard; i >= 1; i--) {
|
|
172
|
+
const src = join(session.sharedDir, `messages.${i}.json`);
|
|
173
|
+
const dst = join(session.sharedDir, `messages.${i + 1}.json`);
|
|
174
|
+
await fs.rename(src, dst).catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await fs.writeFile(join(session.sharedDir, 'messages.1.json'), JSON.stringify(archivePart));
|
|
178
|
+
await fs.writeFile(join(session.sharedDir, 'messages.json'), JSON.stringify(remainPart));
|
|
179
|
+
session.uiMessages = remainPart.map(m => ({ ...m }));
|
|
180
|
+
|
|
181
|
+
console.log(`[Crew] Rotated messages: archived ${archivePart.length} msgs to shard 1, kept ${remainPart.length} in active`);
|
|
182
|
+
} finally {
|
|
183
|
+
session._rotating = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 获取当前最大分片编号
|
|
189
|
+
*/
|
|
190
|
+
export async function getMaxShardIndex(sharedDir) {
|
|
191
|
+
let max = 0;
|
|
192
|
+
try {
|
|
193
|
+
const files = await fs.readdir(sharedDir);
|
|
194
|
+
for (const f of files) {
|
|
195
|
+
const match = f.match(/^messages\.(\d+)\.json$/);
|
|
196
|
+
if (match) {
|
|
197
|
+
const idx = parseInt(match[1], 10);
|
|
198
|
+
if (idx > max) max = idx;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch { /* dir may not exist */ }
|
|
202
|
+
return max;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 删除所有消息分片文件
|
|
207
|
+
*/
|
|
208
|
+
export async function cleanupMessageShards(sharedDir) {
|
|
209
|
+
try {
|
|
210
|
+
const files = await fs.readdir(sharedDir);
|
|
211
|
+
for (const f of files) {
|
|
212
|
+
if (/^messages\.\d+\.json$/.test(f)) {
|
|
213
|
+
await fs.unlink(join(sharedDir, f)).catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch { /* dir may not exist */ }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function loadSessionMeta(sharedDir) {
|
|
220
|
+
try { return JSON.parse(await fs.readFile(join(sharedDir, 'session.json'), 'utf-8')); }
|
|
221
|
+
catch { return null; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function loadSessionMessages(sharedDir) {
|
|
225
|
+
let messages = [];
|
|
226
|
+
try { messages = JSON.parse(await fs.readFile(join(sharedDir, 'messages.json'), 'utf-8')); }
|
|
227
|
+
catch { /* file may not exist */ }
|
|
228
|
+
let hasOlderMessages = false;
|
|
229
|
+
try {
|
|
230
|
+
await fs.access(join(sharedDir, 'messages.1.json'));
|
|
231
|
+
hasOlderMessages = true;
|
|
232
|
+
} catch { /* no older shards */ }
|
|
233
|
+
return { messages, hasOlderMessages };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 加载历史消息分片(前端上滑到顶部时按需请求)
|
|
238
|
+
*/
|
|
239
|
+
export async function handleLoadCrewHistory(msg) {
|
|
240
|
+
const { sessionId, requestId } = msg;
|
|
241
|
+
const shardIndex = parseInt(msg.shardIndex, 10);
|
|
242
|
+
|
|
243
|
+
// Lazy import to avoid circular dependency
|
|
244
|
+
const { crewSessions } = await import('./session.js');
|
|
245
|
+
const { sendCrewMessage } = await import('./ui-messages.js');
|
|
246
|
+
|
|
247
|
+
if (!Number.isFinite(shardIndex) || shardIndex < 1) {
|
|
248
|
+
sendCrewMessage({
|
|
249
|
+
type: 'crew_history_loaded',
|
|
250
|
+
sessionId,
|
|
251
|
+
shardIndex: msg.shardIndex,
|
|
252
|
+
requestId,
|
|
253
|
+
messages: [],
|
|
254
|
+
hasMore: false
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const session = crewSessions.get(sessionId);
|
|
259
|
+
if (!session) {
|
|
260
|
+
sendCrewMessage({
|
|
261
|
+
type: 'crew_history_loaded',
|
|
262
|
+
sessionId,
|
|
263
|
+
shardIndex,
|
|
264
|
+
requestId,
|
|
265
|
+
messages: [],
|
|
266
|
+
hasMore: false
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const shardPath = join(session.sharedDir, `messages.${shardIndex}.json`);
|
|
272
|
+
let messages = [];
|
|
273
|
+
try {
|
|
274
|
+
messages = JSON.parse(await fs.readFile(shardPath, 'utf-8'));
|
|
275
|
+
} catch { /* shard file doesn't exist */ }
|
|
276
|
+
|
|
277
|
+
const hasMore = shardIndex < await getMaxShardIndex(session.sharedDir);
|
|
278
|
+
|
|
279
|
+
sendCrewMessage({
|
|
280
|
+
type: 'crew_history_loaded',
|
|
281
|
+
sessionId,
|
|
282
|
+
shardIndex,
|
|
283
|
+
requestId,
|
|
284
|
+
messages,
|
|
285
|
+
hasMore
|
|
286
|
+
});
|
|
287
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 动态角色管理
|
|
3
|
+
* addRoleToSession, removeRoleFromSession
|
|
4
|
+
*/
|
|
5
|
+
import { initRoleDir, updateSharedClaudeMd } from './shared-dir.js';
|
|
6
|
+
import { saveRoleSessionId } from './role-query.js';
|
|
7
|
+
import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
|
|
8
|
+
|
|
9
|
+
/** Format role label */
|
|
10
|
+
function roleLabel(r) {
|
|
11
|
+
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 向现有 session 动态添加角色
|
|
16
|
+
*/
|
|
17
|
+
export async function addRoleToSession(msg) {
|
|
18
|
+
// Lazy import to avoid circular dependency
|
|
19
|
+
const { crewSessions, expandRoles } = await import('./session.js');
|
|
20
|
+
|
|
21
|
+
const { sessionId, role } = msg;
|
|
22
|
+
const session = crewSessions.get(sessionId);
|
|
23
|
+
if (!session) {
|
|
24
|
+
console.warn(`[Crew] Session not found: ${sessionId}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rolesToAdd = expandRoles([role]);
|
|
29
|
+
|
|
30
|
+
for (const r of rolesToAdd) {
|
|
31
|
+
if (session.roles.has(r.name)) {
|
|
32
|
+
console.warn(`[Crew] Role already exists: ${r.name}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
session.roles.set(r.name, r);
|
|
37
|
+
|
|
38
|
+
if (r.isDecisionMaker) {
|
|
39
|
+
session.decisionMaker = r.name;
|
|
40
|
+
}
|
|
41
|
+
if (!session.decisionMaker) {
|
|
42
|
+
session.decisionMaker = r.name;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await initRoleDir(session.sharedDir, r, session.language || 'zh-CN');
|
|
46
|
+
|
|
47
|
+
console.log(`[Crew] Role added: ${r.name} (${r.displayName}) to session ${sessionId}`);
|
|
48
|
+
|
|
49
|
+
sendCrewMessage({
|
|
50
|
+
type: 'crew_role_added',
|
|
51
|
+
sessionId,
|
|
52
|
+
role: {
|
|
53
|
+
name: r.name,
|
|
54
|
+
displayName: r.displayName,
|
|
55
|
+
icon: r.icon,
|
|
56
|
+
description: r.description,
|
|
57
|
+
isDecisionMaker: r.isDecisionMaker || false,
|
|
58
|
+
model: r.model,
|
|
59
|
+
roleType: r.roleType,
|
|
60
|
+
groupIndex: r.groupIndex
|
|
61
|
+
},
|
|
62
|
+
decisionMaker: session.decisionMaker
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
sendCrewOutput(session, 'system', 'system', {
|
|
66
|
+
type: 'assistant',
|
|
67
|
+
message: { role: 'assistant', content: [{ type: 'text', text: `${roleLabel(r)} 加入了群聊` }] }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await updateSharedClaudeMd(session);
|
|
72
|
+
sendStatusUpdate(session);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 从 session 移除角色
|
|
77
|
+
*/
|
|
78
|
+
export async function removeRoleFromSession(msg) {
|
|
79
|
+
const { crewSessions } = await import('./session.js');
|
|
80
|
+
|
|
81
|
+
const { sessionId, roleName } = msg;
|
|
82
|
+
const session = crewSessions.get(sessionId);
|
|
83
|
+
if (!session) {
|
|
84
|
+
console.warn(`[Crew] Session not found: ${sessionId}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const role = session.roles.get(roleName);
|
|
89
|
+
if (!role) {
|
|
90
|
+
console.warn(`[Crew] Role not found: ${roleName}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 停止角色的 query
|
|
95
|
+
const roleState = session.roleStates.get(roleName);
|
|
96
|
+
if (roleState) {
|
|
97
|
+
if (roleState.claudeSessionId) {
|
|
98
|
+
await saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId);
|
|
99
|
+
}
|
|
100
|
+
if (roleState.abortController) {
|
|
101
|
+
roleState.abortController.abort();
|
|
102
|
+
}
|
|
103
|
+
session.roleStates.delete(roleName);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
session.roles.delete(roleName);
|
|
107
|
+
|
|
108
|
+
if (session.decisionMaker === roleName) {
|
|
109
|
+
const remaining = Array.from(session.roles.values());
|
|
110
|
+
const newDM = remaining.find(r => r.isDecisionMaker) || remaining[0];
|
|
111
|
+
session.decisionMaker = newDM?.name || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await updateSharedClaudeMd(session);
|
|
115
|
+
|
|
116
|
+
console.log(`[Crew] Role removed: ${roleName} from session ${sessionId}`);
|
|
117
|
+
|
|
118
|
+
sendCrewMessage({
|
|
119
|
+
type: 'crew_role_removed',
|
|
120
|
+
sessionId,
|
|
121
|
+
roleName,
|
|
122
|
+
decisionMaker: session.decisionMaker
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
sendCrewOutput(session, 'system', 'system', {
|
|
126
|
+
type: 'assistant',
|
|
127
|
+
message: { role: 'assistant', content: [{ type: 'text', text: `${roleLabel(role)} 离开了群聊` }] }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
sendStatusUpdate(session);
|
|
131
|
+
}
|