๐Ÿ“„ apikeys.ts  โ€ข  8172 bytes
/**
 * CmdCode V0.5 - ๆ•ๆ„Ÿ้…็ฝฎ็ปŸไธ€ๅ…ฅๅฃ
 * 
 * ๐Ÿ“ฆ ๆžถๆž„็ฎ€ๅŒ–๏ผš
 * - crypto-util.ts๏ผšๅŠ ๅฏ†ๅญ˜ๅ‚จ + ๅฏ†้’ฅๆฑ ็ฎก็†
 * - apikeys.ts๏ผš็ปŸไธ€ๅฏผๅ‡บๅ…ฅๅฃ
 * 
 * ๅญ˜ๅ‚จไฝ็ฝฎ๏ผš
 * - ~/.cmdcode/secrets.enc๏ผˆๆ‰€ๆœ‰ๆ•ๆ„Ÿ้…็ฝฎ๏ผŒAES-256-CBC ๅŠ ๅฏ†๏ผ‰
 */
import {
  // ๅŠ ๅฏ†/่งฃๅฏ†
  encrypt, decrypt,
  encryptField, decryptField,
  
  // ้…็ฝฎ็ฎก็†
  loadSecrets, saveSecrets, updateSecrets, clearSecrets, hasSecrets, maskSecret,
  
  // ๅฏ†้’ฅๆฑ ็ฎก็†
  loadChatKeyPool, saveChatKeyPool,
  addChatKey, removeChatKey,
  loadEmbeddingKeyPool, saveEmbeddingKeyPool,
  addEmbeddingKey, removeEmbeddingKey,
  
  // ๅฏ†้’ฅ่Žทๅ–
  getChatApiKey, rotateChatApiKey,
  getEmbeddingApiKey, rotateEmbeddingApiKey,
  
  // ๅฏ†้’ฅๆฑ ็Šถๆ€
  getChatKeyPoolStatus, getEmbeddingKeyPoolStatus,
  resetChatKeyPool, resetEmbeddingKeyPool, resetAllKeyPools,
  markChatKeyExhausted, markEmbeddingKeyExhausted,
  
  // ๅฎšๆ—ถ้‡็ฝฎ
  startDailyKeyPoolReset, stopDailyKeyPoolReset,
  
  // ๅบ”็”จ้…็ฝฎ
  loadAppConfig, saveAppConfig,
  
  // ็”จๆˆท็ผ“ๅญ˜
  saveUserCache, loadUserCache, clearUserCache,
  
  // ๅ‘้‡่ฎฐๅฟ†้…็ฝฎ
  loadMemorySearchConfig, saveMemorySearchConfig, hasMemorySearchConfig,
  DEFAULT_MEMORY_SEARCH,
  
  // ็ฑปๅž‹
  type Secrets, type KeyPoolEntry, type AppSettings
} from './crypto-util.js'

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ้‡ๆ–ฐๅฏผๅ‡บๆ‰€ๆœ‰ๆŽฅๅฃ
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// ๅŠ ๅฏ†/่งฃๅฏ†
export { encrypt, decrypt, encryptField, decryptField }

// ้…็ฝฎ็ฎก็†
export { loadSecrets, saveSecrets, updateSecrets, clearSecrets, hasSecrets, maskSecret }

// ๅฏ†้’ฅๆฑ ็ฎก็†
export {
  loadChatKeyPool, saveChatKeyPool,
  addChatKey, removeChatKey,
  loadEmbeddingKeyPool, saveEmbeddingKeyPool,
  addEmbeddingKey, removeEmbeddingKey
}

// ๅฏ†้’ฅ่Žทๅ–
export {
  getChatApiKey, rotateChatApiKey,
  getEmbeddingApiKey, rotateEmbeddingApiKey
}

// ๅฏ†้’ฅๆฑ ็Šถๆ€
export {
  getChatKeyPoolStatus, getEmbeddingKeyPoolStatus,
  resetChatKeyPool, resetEmbeddingKeyPool, resetAllKeyPools,
  markChatKeyExhausted, markEmbeddingKeyExhausted
}

// ๅฎšๆ—ถ้‡็ฝฎ
export { startDailyKeyPoolReset, stopDailyKeyPoolReset }

// ๅบ”็”จ้…็ฝฎ
export { loadAppConfig, saveAppConfig }

// ็”จๆˆท็ผ“ๅญ˜
export { saveUserCache, loadUserCache, clearUserCache }

// ๅ‘้‡่ฎฐๅฟ†้…็ฝฎ
export { loadMemorySearchConfig, saveMemorySearchConfig, hasMemorySearchConfig, DEFAULT_MEMORY_SEARCH }

// ็ฑปๅž‹
export type { Secrets, KeyPoolEntry, AppSettings }

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ไผš่ฏๆ˜Žๆ–‡ๅญ˜ๅ‚จ๏ผˆๆ— ้œ€ๅŠ ๅฏ†๏ผŒๆ–นไพฟ่ฐƒๅ–ๆŸฅ็œ‹ไฟฎๆ”น๏ผ‰
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs'
import { join } from 'node:path'
import { homedir } from 'node:os'

const CMD_DIR = join(homedir(), '.cmdcode')
const SESSIONS_DIR = join(CMD_DIR, 'sessions')

export interface SessionIndex {
  id: string
  createdAt: string
  updatedAt: string
  messageCount: number
  preview: string
}

/** ไฟๅญ˜ไผš่ฏ๏ผˆๆ˜Žๆ–‡JSON๏ผ‰ - P2 #29: ๅ…ˆๅค‡ไปฝๆ—ง็‰ˆๆœฌ */
export function saveSessionPlaintext(sessionId: string, data: any): void {
  if (!existsSync(SESSIONS_DIR)) mkdirSync(SESSIONS_DIR, { recursive: true })
  const filePath = join(SESSIONS_DIR, `${sessionId}.json`)
  
  // P2 #29: ๅค‡ไปฝๆ—ง็‰ˆๆœฌ๏ผˆๆœ€ๅคšไฟ็•™3ไธชๅค‡ไปฝ๏ผ‰
  if (existsSync(filePath)) {
    const backupDir = join(SESSIONS_DIR, 'backup')
    if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true })
    
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
    const backupPath = join(backupDir, `${sessionId}-${timestamp}.json`)
    
    try {
      // ๅคๅˆถๅฝ“ๅ‰ๆ–‡ไปถๅˆฐๅค‡ไปฝ
      const currentContent = readFileSync(filePath, 'utf-8')
      writeFileSync(backupPath, currentContent, 'utf-8')
      
      // ๆธ…็†ๆ—งๅค‡ไปฝ๏ผˆไฟ็•™ๆœ€่ฟ‘3ไธช๏ผ‰
      const backups = readdirSync(backupDir)
        .filter(f => f.startsWith(sessionId) && f.endsWith('.json'))
        .sort()
      while (backups.length > 3) {
        const oldBackup = join(backupDir, backups.shift()!)
        try { require('node:fs').unlinkSync(oldBackup) } catch { /* ignore */ }
      }
    } catch { /* ๅค‡ไปฝๅคฑ่ดฅไธ้˜ปๆญขไฟๅญ˜ */ }
  }
  
  writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
}

/** ่ฏปๅ–ไผš่ฏ๏ผˆๆ˜Žๆ–‡JSON๏ผ‰ */
export function loadSessionPlaintext<T = any>(sessionId: string): T | null {
  const filePath = join(SESSIONS_DIR, `${sessionId}.json`)
  if (!existsSync(filePath)) return null
  try {
    return JSON.parse(readFileSync(filePath, 'utf-8')) as T
  } catch {
    return null
  }
}

/** ๅˆ—ๅ‡บๆ‰€ๆœ‰ไผš่ฏ */
export function listPlaintextSessions(): SessionIndex[] {
  if (!existsSync(SESSIONS_DIR)) return []
  const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'))
  
  const sessions = files.map(f => {
    const id = f.replace('.json', '')
    const filePath = join(SESSIONS_DIR, f)
    let messageCount = 0
    let preview = ''
    let updatedAt = ''
    let createdAt = ''
    
    try {
      const stat = require('node:fs').statSync(filePath)
      updatedAt = new Date(stat.mtime).toISOString()
      createdAt = new Date(stat.birthtime).toISOString()
    } catch { /* P5: ๆ–‡ไปถไธๅญ˜ๅœจๆˆ–ๆ— ๆƒ้™ */ }
    
    try {
      const messages = JSON.parse(readFileSync(filePath, 'utf-8'))
      if (Array.isArray(messages)) {
        messageCount = messages.length
        const firstUser = messages.find((m: any) => m.role === 'user')
        preview = firstUser?.content?.slice(0, 60) || ''
      }
    } catch { /* P5: JSON่งฃๆžๅคฑ่ดฅๆˆ–ๆ ผๅผ้”™่ฏฏ */ }
    
    return { id, createdAt, updatedAt, messageCount, preview }
  })
  
  // ๆŒ‰ updatedAt ้™ๅบๆŽ’ๅˆ—๏ผˆๆœ€่ฟ‘ๆ›ดๆ–ฐ็š„ๅœจๅ‰๏ผ‰
  sessions.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
  return sessions
}

/** ่Žทๅ–ๆœ€ๆ–ฐไผš่ฏID */
export function getLatestSessionId(): string | null {
  if (!existsSync(SESSIONS_DIR)) return null
  const files = readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json')).sort()
  if (files.length === 0) return null
  return files[files.length - 1].replace('.json', '')
}

/** ๅˆ ้™คไผš่ฏ */
export function deleteSession(sessionId: string): boolean {
  const filePath = join(SESSIONS_DIR, `${sessionId}.json`)
  if (!existsSync(filePath)) return false
  try {
    unlinkSync(filePath)
    return true
  } catch {
    return false
  }
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๅบŸๅผƒๆŽฅๅฃ๏ผˆๅ…ผๅฎนๆ—งไปฃ็ ๏ผ‰
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

/** @deprecated ไฝฟ็”จ getChatApiKey */
export const getCurrentMinimaxKey = getChatApiKey

/** @deprecated ไฝฟ็”จ rotateChatApiKey */
export const rotateMinimaxKey = rotateChatApiKey

/** @deprecated ไฝฟ็”จ getChatKeyPoolStatus */
export const getMinimaxKeyPoolStatus = getChatKeyPoolStatus

/** @deprecated ไฝฟ็”จ resetChatKeyPool */
export const resetMinimaxKeyPool = resetChatKeyPool

/** @deprecated ไฝฟ็”จ loadChatKeyPool */
export function getChatKeyPool(): KeyPoolEntry[] { return loadChatKeyPool() }

/** ไฟๅญ˜ API Key ๅ’Œ Base URL๏ผˆ็ฎ€ๅŒ–ๆŽฅๅฃ๏ผ‰ */
export function saveKeys(apiKey: string, baseUrl?: string): void {
  const secrets = loadSecrets()
  secrets.apiKey = apiKey
  if (baseUrl) {
    secrets.baseUrl = baseUrl
  }
  saveSecrets(secrets)
}