@yeaft/webchat-agent 0.0.196 → 0.0.198
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 +6 -1
- package/crew.js +171 -9
- 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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|