📄 memoryManager.ts  •  3488 bytes
/**
 * CmdCode 向量记忆系统 - 统一对外接口
 */
import { t } from '../i18n'
import { 
  getDb, 
  initializeDatabase, 
  closeDb,
  DB_PATH 
} from './database'
import {
  createSession,
  getSession,
  listSessions,
  deleteSession,
  addMessage,
  getSessionMessages,
  searchFTS,
  type SessionInfo,
  type MessageInfo
} from './sessionStore'
import {
  searchVectors,
  storeMessageEmbedding,
  hasVector,
  getVectorCount,
  deleteMessageVector
} from './vectorSearch'
import { rrfFusion, simpleFusion, type SearchResult } from './rrf'
import { startBackfill, stopBackfill, triggerBackfill, getBackfillStatus } from './backfill'
import { loadConfig as loadEmbeddingConfig } from './embedding'

// 导出类型
export type { SessionInfo, MessageInfo, SearchResult }

// 导出函数
export {
  initializeDatabase,
  closeDb,
  DB_PATH,
  createSession,
  getSession,
  listSessions,
  deleteSession,
  addMessage,
  getSessionMessages,
  searchFTS,
  searchVectors,
  storeMessageEmbedding,
  hasVector,
  getVectorCount,
  deleteMessageVector,
  rrfFusion,
  simpleFusion,
  startBackfill,
  stopBackfill,
  triggerBackfill,
  getBackfillStatus
}

// 内存占用
let memoryUsage = 0

/** 统计摘要 */
export function getSummary(): {
  sessions: number
  messages: number
  vectors: number
  cacheSize: number
  backfillStatus: { running: boolean; interval: number | null }
} {
  const db = getDb()
  const sessions = (db.prepare('SELECT COUNT(*) as cnt FROM sessions').get() as { cnt: number }).cnt
  const messages = (db.prepare('SELECT COUNT(*) as cnt FROM messages').get() as { cnt: number }).cnt
  
  return {
    sessions,
    messages,
    vectors: getVectorCount(),
    cacheSize: memoryUsage,
    backfillStatus: getBackfillStatus()
  }
}

/** 搜索记忆(双路融合) */
export async function searchMemory(query: string, sessionId?: string, limit = 20): Promise<SearchResult[]> {
  // 1. FTS5 关键词搜索
  const ftsResults = searchFTS(query, sessionId, limit)
  
  // 2. 向量语义搜索
  const vecResults = await searchVectors(query, sessionId, limit)

  // 3. RRF 融合
  const map = new Map<string, SearchResult[]>()
  map.set('fts', ftsResults.map((r, i) => ({
    ...r,
    score: 1 / (60 + i + 1),
    source: 'fts' as const
  })))
  map.set('vec', vecResults.map((r, i) => ({
    ...r,
    score: 1 / (60 + i + 1),
    source: 'vec' as const
  })))

  return rrfFusion(map, 60).slice(0, limit)
}

/** 保存新消息并自动向量化 */
export async function saveMessage(sessionId: string, role: string, content: string): Promise<MessageInfo> {
  const message = addMessage(sessionId, role, content)
  
  // 异步存储向量(不阻塞)
  triggerBackfill().catch(console.error)
  
  return message
}

/** 初始化记忆系统 */
export function initMemorySystem(): void {
  console.log('  🧠 ' + t('memory.init'))
  // 注:getDb() 首次调用时已自动执行 initializeDatabase(),无需重复调用

  // 检查 Embedding API 密钥是否已配置
  const embedConfig = loadEmbeddingConfig()
  if (!embedConfig.key) {
    console.log('  ⚠️  未配置 Embedding API 密钥,向量搜索功能暂时不可用。')
    console.log('     请使用 /keypool add embedding 或 /set mem 命令添加密钥。')
  }

  startBackfill(5 * 60 * 1000)
  
  console.log('  ✅ ' + t('memory.ready'))
  console.log(`     ${t('memory.db_path')}: ${DB_PATH}`)
  console.log(`     ${t('memory.vector_count')}: ${getVectorCount()}`)
}