66
77import { Buffer } from 'node:buffer'
88import fs from 'node:fs'
9+ import { readdir , stat } from 'node:fs/promises'
10+ import { join } from 'node:path'
911import { inspect } from 'node:util'
1012
1113import { loadJsonFile , saveJsonFile } from '../../../../../../scripts/json_loader.mjs'
@@ -21,10 +23,10 @@ import { unlockAchievement } from '../../../achievements/src/api.mjs'
2123import { addfile , getfile } from '../files.mjs'
2224import { generateDiff , createBufferedSyncPreviewUpdater } from '../stream.mjs'
2325
24- import { deleteLogContextSidecar , hydrateLogContextFromSidecar , persistLogContextSidecar } from './context_sidecar.mjs'
26+ import { deleteLogContextSidecar , hydrateLogContextFromSidecar , persistLogContextSidecar , pruneSidecarsNotInSet } from './context_sidecar.mjs'
2527import { appendEvent , deleteChatData , ensureChat , getDefaultChannelId , getState , isValidChannelId , listChannelMessages } from './dag.mjs'
2628import { syncChatLogEntryToDag , mirrorDeleteToDag , mirrorEditToDag } from './dagSync.mjs'
27- import { chatJsonPath , chatsRoot } from './paths.mjs'
29+ import { chatDir , chatJsonPath , chatsRoot } from './paths.mjs'
2830import { createCharRpcDispatcher } from './rpcDispatcher.mjs'
2931import { broadcastEvent as broadcastGroupEvent , bufferStreamChunk , finishStreamBuffer } from './websocket.mjs'
3032
@@ -617,6 +619,44 @@ async function updateChatSummary(groupId, chatMetadata) {
617619 saveShellData ( username , 'chat' , 'chat_summaries_cache' )
618620}
619621
622+ /**
623+ * 按当前 chatLog 对齐各频道 context sidecar:删除已不存在条目对应的缓存文件。
624+ * @param {string } username 所有者
625+ * @param {string } groupId 群 ID
626+ * @param {chatLogEntry_t[] } chatLog 当前内存中的日志条目
627+ * @returns {Promise<void> }
628+ */
629+ async function reconcileContextSidecarsWithChatLog ( username , groupId , chatLog ) {
630+ if ( ! username || ! groupId || ! Array . isArray ( chatLog ) ) return
631+ const byCh = new Map ( )
632+ for ( const entry of chatLog ) {
633+ if ( ! entry ?. id ) continue
634+ const ch = isValidChannelId ( entry . extension ?. groupChannelId )
635+ ? entry . extension . groupChannelId
636+ : 'default'
637+ if ( ! byCh . has ( ch ) ) byCh . set ( ch , new Set ( ) )
638+ byCh . get ( ch ) . add ( entry . id )
639+ }
640+ const ctxRoot = join ( chatDir ( username , groupId ) , 'context_cache' )
641+ let names = [ ]
642+ try {
643+ names = await readdir ( ctxRoot )
644+ }
645+ catch {
646+ return
647+ }
648+ const channels = new Set ( byCh . keys ( ) )
649+ for ( const name of names )
650+ try {
651+ if ( ( await stat ( join ( ctxRoot , name ) ) ) . isDirectory ( ) )
652+ channels . add ( name )
653+ }
654+ catch { /* ignore */ }
655+
656+ for ( const ch of channels )
657+ await pruneSidecarsNotInSet ( username , groupId , ch , byCh . get ( ch ) ?? new Set ( ) )
658+ }
659+
620660/**
621661 * 从 DAG default 频道行构造 chatLog 条目。
622662 * @param {object } line DAG 消息事件行
@@ -699,6 +739,8 @@ async function hydrateChatLogFromDag(username, groupId, chatMetadata) {
699739 chatMetadata . timeLineIndex = 0
700740 if ( chatMetadata . chatLog . length )
701741 chatMetadata . LastTimeSlice = chatMetadata . chatLog [ chatMetadata . chatLog . length - 1 ] . timeSlice
742+
743+ await reconcileContextSidecarsWithChatLog ( username , groupId , chatMetadata . chatLog )
702744}
703745
704746/**
@@ -1451,7 +1493,7 @@ async function executeGeneration(groupId, request, stream, placeholderEntry, cha
14511493 }
14521494 catch { /* ignore */ }
14531495 const idx = chatMetadata . chatLog . findIndex ( e => e . id === entryId )
1454- if ( idx !== - 1 ) await deleteMessage ( groupId , idx )
1496+ if ( idx !== - 1 ) await deleteMessage ( groupId , null , idx )
14551497 return
14561498 }
14571499
@@ -1900,6 +1942,18 @@ export async function deleteChat(chatids, username) {
19001942 try {
19011943 const jsonPath = chatJsonPath ( username , groupId )
19021944 if ( fs . existsSync ( jsonPath ) ) await fs . promises . unlink ( jsonPath )
1945+ try {
1946+ const ctxRoot = join ( chatDir ( username , groupId ) , 'context_cache' )
1947+ const names = await readdir ( ctxRoot )
1948+ for ( const name of names )
1949+ try {
1950+ if ( ( await stat ( join ( ctxRoot , name ) ) ) . isDirectory ( ) )
1951+ await pruneSidecarsNotInSet ( username , groupId , name , new Set ( ) )
1952+ }
1953+ catch { /* ignore */ }
1954+
1955+ }
1956+ catch { /* ignore */ }
19031957 await deleteChatData ( username , groupId )
19041958 chatMetadatas . delete ( groupId )
19051959 delete summariesCache [ groupId ]
@@ -2017,6 +2071,7 @@ export async function deleteMessage(groupId, channelId, index) {
20172071
20182072 const owner = chatMetadatas . get ( groupId ) ?. username
20192073 await mirrorDeleteToDag ( groupId , entry , owner )
2074+ await reconcileContextSidecarsWithChatLog ( chatMetadata . username , groupId , chatMetadata . chatLog )
20202075}
20212076
20222077/**
0 commit comments