@yeaft/webchat-agent 0.0.57 → 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 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);
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 || 'sonnet',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",