@yeaft/webchat-agent 0.0.56 → 0.0.58
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.js +17 -1
- package/conversation.js +38 -3
- package/crew.js +181 -2
- package/package.json +1 -1
- package/service.js +1 -0
package/connection.js
CHANGED
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
} from './conversation.js';
|
|
23
23
|
import {
|
|
24
24
|
createCrewSession, handleCrewHumanInput, handleCrewControl,
|
|
25
|
-
addRoleToSession, removeRoleFromSession
|
|
25
|
+
addRoleToSession, removeRoleFromSession,
|
|
26
|
+
handleListCrewSessions, resumeCrewSession
|
|
26
27
|
} from './crew.js';
|
|
27
28
|
|
|
28
29
|
// 需要在断连期间缓冲的消息类型(Claude 输出相关的关键消息)
|
|
@@ -286,6 +287,14 @@ async function handleMessage(msg) {
|
|
|
286
287
|
await removeRoleFromSession(msg);
|
|
287
288
|
break;
|
|
288
289
|
|
|
290
|
+
case 'list_crew_sessions':
|
|
291
|
+
await handleListCrewSessions(msg);
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case 'resume_crew_session':
|
|
295
|
+
await resumeCrewSession(msg);
|
|
296
|
+
break;
|
|
297
|
+
|
|
289
298
|
// Port proxy
|
|
290
299
|
case 'proxy_request':
|
|
291
300
|
handleProxyHttpRequest(msg);
|
|
@@ -475,6 +484,13 @@ async function handleMessage(msg) {
|
|
|
475
484
|
'#!/bin/bash',
|
|
476
485
|
`PID=${pid}`,
|
|
477
486
|
`PKG="${pkgName}@latest"`,
|
|
487
|
+
`LOGFILE="${join(configDir, 'logs', 'upgrade.log')}"`,
|
|
488
|
+
`export PATH="${process.env.PATH}"`,
|
|
489
|
+
'',
|
|
490
|
+
'# Redirect all output to log file',
|
|
491
|
+
'exec > "$LOGFILE" 2>&1',
|
|
492
|
+
'echo "[Upgrade] Started at $(date)"',
|
|
493
|
+
'',
|
|
478
494
|
...(cwd ? [`INSTALL_DIR="${cwd}"`] : []),
|
|
479
495
|
'',
|
|
480
496
|
'# Wait for current process to exit',
|
package/conversation.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ctx from './context.js';
|
|
2
2
|
import { loadSessionHistory } from './history.js';
|
|
3
3
|
import { startClaudeQuery } from './claude.js';
|
|
4
|
+
import { crewSessions, loadCrewIndex } from './crew.js';
|
|
4
5
|
|
|
5
6
|
// 不支持的斜杠命令(真正需要交互式 CLI 的命令)
|
|
6
7
|
const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
|
|
@@ -33,8 +34,8 @@ export function parseSlashCommand(message) {
|
|
|
33
34
|
return { type: null, message };
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
// 发送 conversation
|
|
37
|
-
export function sendConversationList() {
|
|
37
|
+
// 发送 conversation 列表(含活跃 crew sessions + 索引中已停止的 crew sessions)
|
|
38
|
+
export async function sendConversationList() {
|
|
38
39
|
const list = [];
|
|
39
40
|
for (const [id, state] of ctx.conversations) {
|
|
40
41
|
list.push({
|
|
@@ -47,7 +48,41 @@ export function sendConversationList() {
|
|
|
47
48
|
username: state.username
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
// 追加活跃 crew sessions
|
|
52
|
+
const activeCrewIds = new Set();
|
|
53
|
+
for (const [id, session] of crewSessions) {
|
|
54
|
+
activeCrewIds.add(id);
|
|
55
|
+
list.push({
|
|
56
|
+
id,
|
|
57
|
+
workDir: session.projectDir,
|
|
58
|
+
createdAt: session.createdAt,
|
|
59
|
+
processing: session.status === 'running',
|
|
60
|
+
userId: session.userId,
|
|
61
|
+
username: session.username,
|
|
62
|
+
type: 'crew',
|
|
63
|
+
goal: session.goal
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// 追加索引中已停止的 crew sessions(不重复)
|
|
67
|
+
try {
|
|
68
|
+
const index = await loadCrewIndex();
|
|
69
|
+
for (const entry of index) {
|
|
70
|
+
if (!activeCrewIds.has(entry.sessionId)) {
|
|
71
|
+
list.push({
|
|
72
|
+
id: entry.sessionId,
|
|
73
|
+
workDir: entry.projectDir,
|
|
74
|
+
createdAt: entry.createdAt,
|
|
75
|
+
processing: false,
|
|
76
|
+
userId: entry.userId,
|
|
77
|
+
username: entry.username,
|
|
78
|
+
type: 'crew',
|
|
79
|
+
status: entry.status
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.warn('[sendConversationList] Failed to load crew index:', e.message);
|
|
85
|
+
}
|
|
51
86
|
ctx.sendToServer({
|
|
52
87
|
type: 'conversation_list',
|
|
53
88
|
conversations: list
|
package/crew.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { query, Stream } from './sdk/index.js';
|
|
16
16
|
import { promises as fs } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
18
19
|
import ctx from './context.js';
|
|
19
20
|
|
|
20
21
|
// =====================================================================
|
|
@@ -24,9 +25,179 @@ import ctx from './context.js';
|
|
|
24
25
|
/** @type {Map<string, CrewSession>} */
|
|
25
26
|
const crewSessions = new Map();
|
|
26
27
|
|
|
27
|
-
// 导出供 connection.js 使用
|
|
28
|
+
// 导出供 connection.js / conversation.js 使用
|
|
28
29
|
export { crewSessions };
|
|
29
30
|
|
|
31
|
+
// =====================================================================
|
|
32
|
+
// Crew Session Index (~/.claude/crew-sessions.json)
|
|
33
|
+
// =====================================================================
|
|
34
|
+
|
|
35
|
+
const CREW_INDEX_PATH = join(homedir(), '.claude', 'crew-sessions.json');
|
|
36
|
+
|
|
37
|
+
export async function loadCrewIndex() {
|
|
38
|
+
try { return JSON.parse(await fs.readFile(CREW_INDEX_PATH, 'utf-8')); }
|
|
39
|
+
catch { return []; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function saveCrewIndex(index) {
|
|
43
|
+
await fs.mkdir(join(homedir(), '.claude'), { recursive: true });
|
|
44
|
+
await fs.writeFile(CREW_INDEX_PATH, JSON.stringify(index, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sessionToIndexEntry(session) {
|
|
48
|
+
return {
|
|
49
|
+
sessionId: session.id,
|
|
50
|
+
projectDir: session.projectDir,
|
|
51
|
+
sharedDir: session.sharedDir,
|
|
52
|
+
status: session.status,
|
|
53
|
+
createdAt: session.createdAt,
|
|
54
|
+
updatedAt: Date.now()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function upsertCrewIndex(session) {
|
|
59
|
+
const index = await loadCrewIndex();
|
|
60
|
+
const entry = sessionToIndexEntry(session);
|
|
61
|
+
const idx = index.findIndex(e => e.sessionId === session.id);
|
|
62
|
+
if (idx >= 0) index[idx] = entry; else index.push(entry);
|
|
63
|
+
await saveCrewIndex(index);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =====================================================================
|
|
67
|
+
// Session Metadata (.crew/session.json)
|
|
68
|
+
// =====================================================================
|
|
69
|
+
|
|
70
|
+
async function saveSessionMeta(session) {
|
|
71
|
+
const meta = {
|
|
72
|
+
sessionId: session.id,
|
|
73
|
+
projectDir: session.projectDir,
|
|
74
|
+
sharedDir: session.sharedDir,
|
|
75
|
+
goal: session.goal,
|
|
76
|
+
status: session.status,
|
|
77
|
+
roles: Array.from(session.roles.values()).map(r => ({
|
|
78
|
+
name: r.name, displayName: r.displayName, icon: r.icon,
|
|
79
|
+
description: r.description, isDecisionMaker: r.isDecisionMaker || false
|
|
80
|
+
})),
|
|
81
|
+
decisionMaker: session.decisionMaker,
|
|
82
|
+
maxRounds: session.maxRounds,
|
|
83
|
+
round: session.round,
|
|
84
|
+
createdAt: session.createdAt,
|
|
85
|
+
updatedAt: Date.now(),
|
|
86
|
+
userId: session.userId,
|
|
87
|
+
username: session.username
|
|
88
|
+
};
|
|
89
|
+
await fs.writeFile(join(session.sharedDir, 'session.json'), JSON.stringify(meta, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadSessionMeta(sharedDir) {
|
|
93
|
+
try { return JSON.parse(await fs.readFile(join(sharedDir, 'session.json'), 'utf-8')); }
|
|
94
|
+
catch { return null; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =====================================================================
|
|
98
|
+
// List & Resume Crew Sessions
|
|
99
|
+
// =====================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 列出所有 crew sessions(从索引文件 + 活跃 sessions 合并)
|
|
103
|
+
*/
|
|
104
|
+
export async function handleListCrewSessions(msg) {
|
|
105
|
+
const { requestId, _requestClientId } = msg;
|
|
106
|
+
const index = await loadCrewIndex();
|
|
107
|
+
|
|
108
|
+
// 用活跃 session 更新实时状态
|
|
109
|
+
for (const entry of index) {
|
|
110
|
+
const active = crewSessions.get(entry.sessionId);
|
|
111
|
+
if (active) {
|
|
112
|
+
entry.status = active.status;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ctx.sendToServer({
|
|
117
|
+
type: 'crew_sessions_list',
|
|
118
|
+
requestId,
|
|
119
|
+
_requestClientId,
|
|
120
|
+
sessions: index
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 恢复已停止的 crew session
|
|
126
|
+
*/
|
|
127
|
+
export async function resumeCrewSession(msg) {
|
|
128
|
+
const { sessionId, userId, username } = msg;
|
|
129
|
+
|
|
130
|
+
// 如果已经在活跃 sessions 中,直接返回状态
|
|
131
|
+
if (crewSessions.has(sessionId)) {
|
|
132
|
+
const session = crewSessions.get(sessionId);
|
|
133
|
+
sendStatusUpdate(session);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 从索引获取 sharedDir
|
|
138
|
+
const index = await loadCrewIndex();
|
|
139
|
+
const indexEntry = index.find(e => e.sessionId === sessionId);
|
|
140
|
+
if (!indexEntry) {
|
|
141
|
+
sendCrewMessage({ type: 'error', sessionId, message: 'Crew session not found in index' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 从 session.json 加载详细元数据
|
|
146
|
+
const meta = await loadSessionMeta(indexEntry.sharedDir);
|
|
147
|
+
if (!meta) {
|
|
148
|
+
sendCrewMessage({ type: 'error', sessionId, message: 'Crew session metadata not found' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 重建 session(跳过 initSharedDir,目录已存在)
|
|
153
|
+
const roles = meta.roles || [];
|
|
154
|
+
const decisionMaker = meta.decisionMaker || roles[0]?.name || null;
|
|
155
|
+
const session = {
|
|
156
|
+
id: sessionId,
|
|
157
|
+
projectDir: meta.projectDir,
|
|
158
|
+
sharedDir: meta.sharedDir || indexEntry.sharedDir,
|
|
159
|
+
goal: meta.goal,
|
|
160
|
+
roles: new Map(roles.map(r => [r.name, r])),
|
|
161
|
+
roleStates: new Map(),
|
|
162
|
+
decisionMaker,
|
|
163
|
+
status: 'waiting_human',
|
|
164
|
+
round: meta.round || 0,
|
|
165
|
+
maxRounds: meta.maxRounds || 20,
|
|
166
|
+
costUsd: 0,
|
|
167
|
+
messageHistory: [],
|
|
168
|
+
humanMessageQueue: [],
|
|
169
|
+
waitingHumanContext: null,
|
|
170
|
+
userId: userId || meta.userId,
|
|
171
|
+
username: username || meta.username,
|
|
172
|
+
createdAt: meta.createdAt || Date.now()
|
|
173
|
+
};
|
|
174
|
+
crewSessions.set(sessionId, session);
|
|
175
|
+
|
|
176
|
+
// 通知 server(复用 crew_session_created)
|
|
177
|
+
sendCrewMessage({
|
|
178
|
+
type: 'crew_session_created',
|
|
179
|
+
sessionId,
|
|
180
|
+
projectDir: session.projectDir,
|
|
181
|
+
sharedDir: session.sharedDir,
|
|
182
|
+
goal: session.goal,
|
|
183
|
+
roles: roles.map(r => ({
|
|
184
|
+
name: r.name, displayName: r.displayName, icon: r.icon,
|
|
185
|
+
description: r.description, isDecisionMaker: r.isDecisionMaker || false
|
|
186
|
+
})),
|
|
187
|
+
decisionMaker,
|
|
188
|
+
maxRounds: session.maxRounds,
|
|
189
|
+
userId: session.userId,
|
|
190
|
+
username: session.username
|
|
191
|
+
});
|
|
192
|
+
sendStatusUpdate(session);
|
|
193
|
+
|
|
194
|
+
// 更新索引和 session.json
|
|
195
|
+
await upsertCrewIndex(session);
|
|
196
|
+
await saveSessionMeta(session);
|
|
197
|
+
|
|
198
|
+
console.log(`[Crew] Session ${sessionId} resumed, waiting for human input`);
|
|
199
|
+
}
|
|
200
|
+
|
|
30
201
|
// =====================================================================
|
|
31
202
|
// Session Lifecycle
|
|
32
203
|
// =====================================================================
|
|
@@ -104,6 +275,10 @@ export async function createCrewSession(msg) {
|
|
|
104
275
|
// 发送状态
|
|
105
276
|
sendStatusUpdate(session);
|
|
106
277
|
|
|
278
|
+
// 持久化到索引和 session.json
|
|
279
|
+
await upsertCrewIndex(session);
|
|
280
|
+
await saveSessionMeta(session);
|
|
281
|
+
|
|
107
282
|
// 如果有预设角色,启动第一个
|
|
108
283
|
if (roles.length > 0) {
|
|
109
284
|
const firstRole = roles.find(r => r.name === 'pm') || roles[0];
|
|
@@ -402,7 +577,7 @@ async function createRoleQuery(session, roleName) {
|
|
|
402
577
|
cwd: roleCwd,
|
|
403
578
|
permissionMode: 'bypassPermissions',
|
|
404
579
|
abort: abortController.signal,
|
|
405
|
-
model: role.model ||
|
|
580
|
+
model: role.model || undefined,
|
|
406
581
|
appendSystemPrompt: systemPrompt
|
|
407
582
|
};
|
|
408
583
|
|
|
@@ -1022,4 +1197,8 @@ function sendStatusUpdate(session) {
|
|
|
1022
1197
|
.filter(([, s]) => s.turnActive)
|
|
1023
1198
|
.map(([name]) => name)
|
|
1024
1199
|
});
|
|
1200
|
+
|
|
1201
|
+
// 异步更新持久化
|
|
1202
|
+
upsertCrewIndex(session).catch(e => console.warn('[Crew] Failed to update index:', e.message));
|
|
1203
|
+
saveSessionMeta(session).catch(e => console.warn('[Crew] Failed to save session meta:', e.message));
|
|
1025
1204
|
}
|
package/package.json
CHANGED
package/service.js
CHANGED
|
@@ -164,6 +164,7 @@ ExecStart=${nodePath} ${cliPath}
|
|
|
164
164
|
WorkingDirectory=${config.workDir || homedir()}
|
|
165
165
|
Restart=on-failure
|
|
166
166
|
RestartSec=10
|
|
167
|
+
KillMode=process
|
|
167
168
|
${envLines.join('\n')}
|
|
168
169
|
Environment=PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
|
|
169
170
|
|