@yeaft/webchat-agent 0.0.197 → 0.0.199

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 (3) hide show
  1. package/connection.js +6 -1
  2. package/crew.js +171 -9
  3. package/package.json +1 -1
package/connection.js CHANGED
@@ -23,7 +23,8 @@ import {
23
23
  import {
24
24
  createCrewSession, handleCrewHumanInput, handleCrewControl,
25
25
  addRoleToSession, removeRoleFromSession,
26
- handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex
26
+ handleListCrewSessions, handleCheckCrewExists, handleDeleteCrewDir, resumeCrewSession, removeFromCrewIndex,
27
+ handleLoadCrewHistory
27
28
  } from './crew.js';
28
29
 
29
30
  // 需要在断连期间缓冲的消息类型(Claude 输出相关的关键消息)
@@ -321,6 +322,10 @@ async function handleMessage(msg) {
321
322
  await (await import('./crew.js')).handleUpdateCrewSession(msg);
322
323
  break;
323
324
 
325
+ case 'crew_load_history':
326
+ await handleLoadCrewHistory(msg);
327
+ break;
328
+
324
329
  // Port proxy
325
330
  case 'proxy_request':
326
331
  handleProxyHttpRequest(msg);
package/crew.js CHANGED
@@ -291,6 +291,8 @@ export async function removeFromCrewIndex(sessionId) {
291
291
  for (const file of ['session.json', 'messages.json']) {
292
292
  await fs.unlink(join(sharedDir, file)).catch(() => {});
293
293
  }
294
+ // Clean up message shard files
295
+ await cleanupMessageShards(sharedDir);
294
296
  console.log(`[Crew] Cleaned session files in ${sharedDir}`);
295
297
  } catch (e) {
296
298
  console.warn(`[Crew] Failed to clean session files:`, e.message);
@@ -302,6 +304,8 @@ export async function removeFromCrewIndex(sessionId) {
302
304
  // Session Metadata (.crew/session.json)
303
305
  // =====================================================================
304
306
 
307
+ const MESSAGE_SHARD_SIZE = 256 * 1024; // 256KB per shard
308
+
305
309
  async function saveSessionMeta(session) {
306
310
  const meta = {
307
311
  sessionId: session.id,
@@ -339,18 +343,169 @@ async function saveSessionMeta(session) {
339
343
  const { _streaming, ...rest } = m;
340
344
  return rest;
341
345
  });
342
- await fs.writeFile(join(session.sharedDir, 'messages.json'), JSON.stringify(cleaned));
346
+ const json = JSON.stringify(cleaned);
347
+ // 超过阈值时直接归档(rotateMessages 内部写两个文件,避免双写)
348
+ if (json.length > MESSAGE_SHARD_SIZE && !session._rotating) {
349
+ await rotateMessages(session, cleaned);
350
+ } else {
351
+ await fs.writeFile(join(session.sharedDir, 'messages.json'), json);
352
+ }
343
353
  }
344
354
  }
345
355
 
356
+ /**
357
+ * 归档旧消息到分片文件(logrotate 风格)
358
+ * messages.json = 当前活跃分片(最新消息)
359
+ * messages.1.json = 最近归档,messages.2.json = 更早归档 ...
360
+ */
361
+ async function rotateMessages(session, cleaned) {
362
+ session._rotating = true;
363
+ try {
364
+ // 找到分割点:优先在 turn 边界(route/system 消息)分割,约归档前半部分
365
+ const halfLen = Math.floor(cleaned.length / 2);
366
+ let splitIdx = halfLen;
367
+ // 从 halfLen 附近向前搜索 turn 边界
368
+ for (let i = halfLen; i > Math.max(0, halfLen - 20); i--) {
369
+ if (cleaned[i].type === 'route' || cleaned[i].type === 'system') {
370
+ splitIdx = i + 1; // 在边界消息之后分割
371
+ break;
372
+ }
373
+ }
374
+ // 如果向前没找到,向后搜索
375
+ if (splitIdx === halfLen) {
376
+ for (let i = halfLen + 1; i < Math.min(cleaned.length - 1, halfLen + 20); i++) {
377
+ if (cleaned[i].type === 'route' || cleaned[i].type === 'system') {
378
+ splitIdx = i + 1;
379
+ break;
380
+ }
381
+ }
382
+ }
383
+ // 确保至少归档 1 条且保留 1 条
384
+ splitIdx = Math.max(1, Math.min(splitIdx, cleaned.length - 1));
385
+
386
+ const archivePart = cleaned.slice(0, splitIdx);
387
+ const remainPart = cleaned.slice(splitIdx);
388
+
389
+ // 将现有归档文件编号 +1(从最大编号开始,避免覆盖)
390
+ const maxShard = await getMaxShardIndex(session.sharedDir);
391
+ for (let i = maxShard; i >= 1; i--) {
392
+ const src = join(session.sharedDir, `messages.${i}.json`);
393
+ const dst = join(session.sharedDir, `messages.${i + 1}.json`);
394
+ await fs.rename(src, dst).catch(() => {});
395
+ }
396
+
397
+ // 写入归档分片
398
+ await fs.writeFile(join(session.sharedDir, 'messages.1.json'), JSON.stringify(archivePart));
399
+ // 重写当前活跃文件
400
+ await fs.writeFile(join(session.sharedDir, 'messages.json'), JSON.stringify(remainPart));
401
+ // 同步内存中的 uiMessages
402
+ session.uiMessages = remainPart.map(m => ({ ...m }));
403
+
404
+ console.log(`[Crew] Rotated messages: archived ${archivePart.length} msgs to shard 1, kept ${remainPart.length} in active`);
405
+ } finally {
406
+ session._rotating = false;
407
+ }
408
+ }
409
+
410
+ /**
411
+ * 获取当前最大分片编号
412
+ */
413
+ async function getMaxShardIndex(sharedDir) {
414
+ let max = 0;
415
+ try {
416
+ const files = await fs.readdir(sharedDir);
417
+ for (const f of files) {
418
+ const match = f.match(/^messages\.(\d+)\.json$/);
419
+ if (match) {
420
+ const idx = parseInt(match[1], 10);
421
+ if (idx > max) max = idx;
422
+ }
423
+ }
424
+ } catch { /* dir may not exist */ }
425
+ return max;
426
+ }
427
+
428
+ /**
429
+ * 删除所有消息分片文件(messages.1.json, messages.2.json, ...)
430
+ */
431
+ async function cleanupMessageShards(sharedDir) {
432
+ try {
433
+ const files = await fs.readdir(sharedDir);
434
+ for (const f of files) {
435
+ if (/^messages\.\d+\.json$/.test(f)) {
436
+ await fs.unlink(join(sharedDir, f)).catch(() => {});
437
+ }
438
+ }
439
+ } catch { /* dir may not exist */ }
440
+ }
441
+
346
442
  async function loadSessionMeta(sharedDir) {
347
443
  try { return JSON.parse(await fs.readFile(join(sharedDir, 'session.json'), 'utf-8')); }
348
444
  catch { return null; }
349
445
  }
350
446
 
351
447
  async function loadSessionMessages(sharedDir) {
352
- try { return JSON.parse(await fs.readFile(join(sharedDir, 'messages.json'), 'utf-8')); }
353
- catch { return []; }
448
+ let messages = [];
449
+ try { messages = JSON.parse(await fs.readFile(join(sharedDir, 'messages.json'), 'utf-8')); }
450
+ catch { /* file may not exist */ }
451
+ // Check if older shards exist
452
+ let hasOlderMessages = false;
453
+ try {
454
+ await fs.access(join(sharedDir, 'messages.1.json'));
455
+ hasOlderMessages = true;
456
+ } catch { /* no older shards */ }
457
+ return { messages, hasOlderMessages };
458
+ }
459
+
460
+ /**
461
+ * 加载历史消息分片
462
+ * 前端上滑到顶部时按需请求
463
+ */
464
+ export async function handleLoadCrewHistory(msg) {
465
+ const { sessionId, requestId } = msg;
466
+ // Validate shardIndex: must be a positive integer to prevent path traversal
467
+ const shardIndex = parseInt(msg.shardIndex, 10);
468
+ if (!Number.isFinite(shardIndex) || shardIndex < 1) {
469
+ sendCrewMessage({
470
+ type: 'crew_history_loaded',
471
+ sessionId,
472
+ shardIndex: msg.shardIndex,
473
+ requestId,
474
+ messages: [],
475
+ hasMore: false
476
+ });
477
+ return;
478
+ }
479
+ const session = crewSessions.get(sessionId);
480
+ if (!session) {
481
+ sendCrewMessage({
482
+ type: 'crew_history_loaded',
483
+ sessionId,
484
+ shardIndex,
485
+ requestId,
486
+ messages: [],
487
+ hasMore: false
488
+ });
489
+ return;
490
+ }
491
+
492
+ const shardPath = join(session.sharedDir, `messages.${shardIndex}.json`);
493
+ let messages = [];
494
+ try {
495
+ messages = JSON.parse(await fs.readFile(shardPath, 'utf-8'));
496
+ } catch { /* shard file doesn't exist */ }
497
+
498
+ // Check if there's an even older shard
499
+ const hasMore = shardIndex < await getMaxShardIndex(session.sharedDir);
500
+
501
+ sendCrewMessage({
502
+ type: 'crew_history_loaded',
503
+ sessionId,
504
+ shardIndex,
505
+ requestId,
506
+ messages,
507
+ hasMore
508
+ });
354
509
  }
355
510
 
356
511
  // =====================================================================
@@ -479,13 +634,16 @@ export async function resumeCrewSession(msg) {
479
634
  const roles = Array.from(session.roles.values());
480
635
  // 如果内存中没有 uiMessages,尝试从磁盘加载
481
636
  if ((!session.uiMessages || session.uiMessages.length === 0) && session.sharedDir) {
482
- session.uiMessages = await loadSessionMessages(session.sharedDir);
637
+ const loaded = await loadSessionMessages(session.sharedDir);
638
+ session.uiMessages = loaded.messages;
483
639
  }
484
640
  // 发送前清理 _streaming 标记(跟磁盘保存逻辑保持一致)
485
641
  const cleanedMessages = (session.uiMessages || []).map(m => {
486
642
  const { _streaming, ...rest } = m;
487
643
  return rest;
488
644
  });
645
+ // 检查是否有历史分片
646
+ const hasOlderMessages = await getMaxShardIndex(session.sharedDir) > 0;
489
647
 
490
648
  sendCrewMessage({
491
649
  type: 'crew_session_restored',
@@ -504,7 +662,8 @@ export async function resumeCrewSession(msg) {
504
662
  maxRounds: session.maxRounds,
505
663
  userId: session.userId,
506
664
  username: session.username,
507
- uiMessages: cleanedMessages
665
+ uiMessages: cleanedMessages,
666
+ hasOlderMessages
508
667
  });
509
668
  sendStatusUpdate(session);
510
669
  return;
@@ -559,8 +718,9 @@ export async function resumeCrewSession(msg) {
559
718
  };
560
719
  crewSessions.set(sessionId, session);
561
720
 
562
- // 加载 UI 消息历史
563
- session.uiMessages = await loadSessionMessages(session.sharedDir);
721
+ // 加载 UI 消息历史(仅最新分片)
722
+ const loaded = await loadSessionMessages(session.sharedDir);
723
+ session.uiMessages = loaded.messages;
564
724
 
565
725
  // 通知 server
566
726
  sendCrewMessage({
@@ -580,7 +740,8 @@ export async function resumeCrewSession(msg) {
580
740
  maxRounds: session.maxRounds,
581
741
  userId: session.userId,
582
742
  username: session.username,
583
- uiMessages: session.uiMessages
743
+ uiMessages: session.uiMessages,
744
+ hasOlderMessages: loaded.hasOlderMessages
584
745
  });
585
746
  sendStatusUpdate(session);
586
747
 
@@ -2730,9 +2891,10 @@ async function clearSession(session) {
2730
2891
  // 4. 重置计数
2731
2892
  session.round = 0;
2732
2893
 
2733
- // 5. 清空磁盘上的 messages.json
2894
+ // 5. 清空磁盘上的 messages.json 和所有分片
2734
2895
  const messagesPath = join(session.sharedDir, 'messages.json');
2735
2896
  await fs.writeFile(messagesPath, '[]').catch(() => {});
2897
+ await cleanupMessageShards(session.sharedDir);
2736
2898
 
2737
2899
  // 6. 恢复运行状态
2738
2900
  session.status = 'running';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.197",
3
+ "version": "0.0.199",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",