@yeaft/webchat-agent 0.0.75 → 0.0.77

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.
Files changed (2) hide show
  1. package/crew.js +89 -4
  2. package/package.json +1 -1
package/crew.js CHANGED
@@ -111,6 +111,15 @@ async function saveSessionMeta(session) {
111
111
  username: session.username
112
112
  };
113
113
  await fs.writeFile(join(session.sharedDir, 'session.json'), JSON.stringify(meta, null, 2));
114
+ // 保存 UI 消息历史(用于恢复时重放)
115
+ if (session.uiMessages && session.uiMessages.length > 0) {
116
+ // 清理 _streaming 标记后保存
117
+ const cleaned = session.uiMessages.map(m => {
118
+ const { _streaming, ...rest } = m;
119
+ return rest;
120
+ });
121
+ await fs.writeFile(join(session.sharedDir, 'messages.json'), JSON.stringify(cleaned));
122
+ }
114
123
  }
115
124
 
116
125
  async function loadSessionMeta(sharedDir) {
@@ -118,6 +127,11 @@ async function loadSessionMeta(sharedDir) {
118
127
  catch { return null; }
119
128
  }
120
129
 
130
+ async function loadSessionMessages(sharedDir) {
131
+ try { return JSON.parse(await fs.readFile(join(sharedDir, 'messages.json'), 'utf-8')); }
132
+ catch { return []; }
133
+ }
134
+
121
135
  // =====================================================================
122
136
  // List & Resume Crew Sessions
123
137
  // =====================================================================
@@ -155,6 +169,10 @@ export async function resumeCrewSession(msg) {
155
169
  if (crewSessions.has(sessionId)) {
156
170
  const session = crewSessions.get(sessionId);
157
171
  const roles = Array.from(session.roles.values());
172
+ // 如果内存中没有 uiMessages,尝试从磁盘加载
173
+ if ((!session.uiMessages || session.uiMessages.length === 0) && session.sharedDir) {
174
+ session.uiMessages = await loadSessionMessages(session.sharedDir);
175
+ }
158
176
  sendCrewMessage({
159
177
  type: 'crew_session_restored',
160
178
  sessionId,
@@ -168,7 +186,8 @@ export async function resumeCrewSession(msg) {
168
186
  decisionMaker: session.decisionMaker,
169
187
  maxRounds: session.maxRounds,
170
188
  userId: session.userId,
171
- username: session.username
189
+ username: session.username,
190
+ uiMessages: session.uiMessages || []
172
191
  });
173
192
  sendStatusUpdate(session);
174
193
  return;
@@ -205,6 +224,7 @@ export async function resumeCrewSession(msg) {
205
224
  maxRounds: meta.maxRounds || 20,
206
225
  costUsd: 0,
207
226
  messageHistory: [],
227
+ uiMessages: [], // will be loaded from messages.json
208
228
  humanMessageQueue: [],
209
229
  waitingHumanContext: null,
210
230
  pendingRoute: null,
@@ -214,6 +234,9 @@ export async function resumeCrewSession(msg) {
214
234
  };
215
235
  crewSessions.set(sessionId, session);
216
236
 
237
+ // 加载 UI 消息历史
238
+ session.uiMessages = await loadSessionMessages(session.sharedDir);
239
+
217
240
  // 通知 server
218
241
  sendCrewMessage({
219
242
  type: 'crew_session_restored',
@@ -228,7 +251,8 @@ export async function resumeCrewSession(msg) {
228
251
  decisionMaker,
229
252
  maxRounds: session.maxRounds,
230
253
  userId: session.userId,
231
- username: session.username
254
+ username: session.username,
255
+ uiMessages: session.uiMessages
232
256
  });
233
257
  sendStatusUpdate(session);
234
258
 
@@ -283,6 +307,7 @@ export async function createCrewSession(msg) {
283
307
  maxRounds,
284
308
  costUsd: 0,
285
309
  messageHistory: [], // 群聊消息历史
310
+ uiMessages: [], // 精简的 UI 消息历史(用于恢复时重放)
286
311
  humanMessageQueue: [], // 人的消息排队
287
312
  waitingHumanContext: null, // { fromRole, reason, message }
288
313
  pendingRoute: null, // { fromRole, route } — 暂停时未完成的路由
@@ -764,6 +789,10 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
764
789
  // ★ Turn 完成!
765
790
  console.log(`[Crew] ${roleName} turn completed`);
766
791
 
792
+ // 结束 uiMessages 中最后一条的 streaming 标记
793
+ const lastUi = session.uiMessages[session.uiMessages.length - 1];
794
+ if (lastUi && lastUi._streaming) delete lastUi._streaming;
795
+
767
796
  // 更新费用
768
797
  if (message.total_cost_usd) {
769
798
  session.costUsd += message.total_cost_usd;
@@ -1003,6 +1032,12 @@ export async function handleCrewHumanInput(msg) {
1003
1032
  }
1004
1033
 
1005
1034
  // 注意:不在这里发送人的消息到 Web(前端已本地添加,避免重复)
1035
+ // 但需要记录到 uiMessages 用于恢复时重放
1036
+ session.uiMessages.push({
1037
+ role: 'human', roleIcon: 'H', roleName: '你',
1038
+ type: 'text', content,
1039
+ timestamp: Date.now()
1040
+ });
1006
1041
 
1007
1042
  // 如果在等待人工介入
1008
1043
  if (session.status === 'waiting_human') {
@@ -1246,17 +1281,67 @@ function sendCrewMessage(msg) {
1246
1281
  */
1247
1282
  function sendCrewOutput(session, roleName, outputType, rawMessage, extra = {}) {
1248
1283
  const role = session.roles.get(roleName);
1284
+ const roleIcon = role?.icon || (roleName === 'human' ? 'H' : roleName === 'system' ? 'S' : 'A');
1285
+ const displayName = role?.displayName || roleName;
1249
1286
 
1250
1287
  sendCrewMessage({
1251
1288
  type: 'crew_output',
1252
1289
  sessionId: session.id,
1253
1290
  role: roleName,
1254
- roleIcon: role?.icon || (roleName === 'human' ? 'H' : roleName === 'system' ? 'S' : 'A'),
1255
- roleName: role?.displayName || roleName,
1291
+ roleIcon,
1292
+ roleName: displayName,
1256
1293
  outputType, // 'text' | 'tool_use' | 'tool_result' | 'route' | 'system'
1257
1294
  data: rawMessage,
1258
1295
  ...extra
1259
1296
  });
1297
+
1298
+ // ★ 记录精简 UI 消息用于恢复(跳过 tool_use/tool_result,只记录可见内容)
1299
+ if (outputType === 'text') {
1300
+ const content = rawMessage?.message?.content;
1301
+ let text = '';
1302
+ if (typeof content === 'string') {
1303
+ text = content;
1304
+ } else if (Array.isArray(content)) {
1305
+ text = content.filter(b => b.type === 'text').map(b => b.text).join('');
1306
+ }
1307
+ if (!text) return;
1308
+ // 合并同一角色的连续文本
1309
+ const last = session.uiMessages[session.uiMessages.length - 1];
1310
+ if (last && last.role === roleName && last.type === 'text' && last._streaming) {
1311
+ last.content += text;
1312
+ } else {
1313
+ session.uiMessages.push({
1314
+ role: roleName, roleIcon, roleName: displayName,
1315
+ type: 'text', content: text, _streaming: true,
1316
+ timestamp: Date.now()
1317
+ });
1318
+ }
1319
+ } else if (outputType === 'route') {
1320
+ // 结束前一条消息的 streaming
1321
+ const last = session.uiMessages[session.uiMessages.length - 1];
1322
+ if (last && last._streaming) delete last._streaming;
1323
+ session.uiMessages.push({
1324
+ role: roleName, roleIcon, roleName: displayName,
1325
+ type: 'route', routeTo: extra.routeTo,
1326
+ content: `→ @${extra.routeTo} ${extra.routeSummary || ''}`,
1327
+ timestamp: Date.now()
1328
+ });
1329
+ } else if (outputType === 'system') {
1330
+ const content = rawMessage?.message?.content;
1331
+ let text = '';
1332
+ if (typeof content === 'string') {
1333
+ text = content;
1334
+ } else if (Array.isArray(content)) {
1335
+ text = content.filter(b => b.type === 'text').map(b => b.text).join('');
1336
+ }
1337
+ if (!text) return;
1338
+ session.uiMessages.push({
1339
+ role: roleName, roleIcon, roleName: displayName,
1340
+ type: 'system', content: text,
1341
+ timestamp: Date.now()
1342
+ });
1343
+ }
1344
+ // tool_use 和 tool_result 不记录(太大,恢复时不需要)
1260
1345
  }
1261
1346
 
1262
1347
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",