๐Ÿ“„ user-models.ts  โ€ข  7718 bytes
/**
 * CmdCode V0.5 - ็”จๆˆท่‡ชๅฎšไน‰ๆจกๅž‹้…็ฝฎ็ฎก็†
 * 
 * ๅŠŸ่ƒฝ๏ผš
 *   1. ็”จๆˆทๅฏๅˆ›ๅปบๅคšไธช่‡ชๅฎšไน‰ๆจกๅž‹้…็ฝฎ
 *   2. ้…็ฝฎๅŠ ๅฏ†ๅญ˜ๅ‚จๅœจ็”จๆˆทไธ“ๅฑž็›ฎๅฝ•
 *   3. ๆ”ฏๆŒๅˆ‡ๆข้ป˜่ฎคๆจกๅž‹
 *   4. ๅฏๅŠจๆ—ถ่‡ชๅŠจๅŠ ่ฝฝ็”จๆˆท้ป˜่ฎคๆจกๅž‹
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs'
import { join } from 'node:path'
import { homedir } from 'node:os'
import { encrypt, decrypt } from './crypto-util.js'
import { testConnection } from './models.js'

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ็ฑปๅž‹ๅฎšไน‰
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

/** ็”จๆˆท่‡ชๅฎšไน‰ๆจกๅž‹้…็ฝฎ */
export interface UserModelConfig {
  id: string           // ้…็ฝฎID๏ผˆๆ–‡ไปถๅ๏ผŒไธๅซๆ‰ฉๅฑ•ๅ๏ผ‰
  name: string         // ๆจกๅž‹ๆ˜พ็คบๅ็งฐ
  model: string        // ๆจกๅž‹ID๏ผˆAPI่ฐƒ็”จ็”จ๏ผ‰
  baseUrl: string      // APIๅœฐๅ€
  apiKey: string       // APIๅฏ†้’ฅ๏ผˆๅŠ ๅฏ†ๅญ˜ๅ‚จ๏ผ‰
  note1?: string       // ๅค‡ๆณจไฟกๆฏไธ€
  note2?: string       // ๅค‡ๆณจไฟกๆฏไบŒ
  createdAt: string    // ๅˆ›ๅปบๆ—ถ้—ด
  updatedAt: string    // ๆ›ดๆ–ฐๆ—ถ้—ด
  isDefault?: boolean  // ๆ˜ฏๅฆไธบ็”จๆˆท้ป˜่ฎคๆจกๅž‹
}

/** ๆจกๅž‹ๅˆ—่กจ้กน๏ผˆไธๅซๆ•ๆ„Ÿไฟกๆฏ๏ผ‰ */
export interface ModelListItem {
  id: string
  name: string
  model: string
  baseUrl: string
  note1?: string
  note2?: string
  createdAt: string
  isDefault: boolean
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ้…็ฝฎ
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

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

/** ่Žทๅ–็”จๆˆทๆจกๅž‹้…็ฝฎ็›ฎๅฝ• */
function getUserModelsDir(username: string): string {
  return join(CMD_DIR, 'workspaces', username, 'models')
}

/** ็กฎไฟ็›ฎๅฝ•ๅญ˜ๅœจ */
function ensureDir(dir: string): void {
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
}

/** ่ฎพ็ฝฎๆ–‡ไปถๆƒ้™ */
function setPermission(filePath: string): void {
  try { require('node:fs').chmodSync(filePath, 0o600) } catch { /* ignore */ }
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// CRUD ๆ“ไฝœ
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

/**
 * ๅˆ›ๅปบ็”จๆˆทๆจกๅž‹้…็ฝฎ
 */
export function createUserModel(
  username: string,
  config: Omit<UserModelConfig, 'id' | 'createdAt' | 'updatedAt' | 'isDefault'>
): UserModelConfig {
  const modelsDir = getUserModelsDir(username)
  ensureDir(modelsDir)
  
  // ็”Ÿๆˆๅ”ฏไธ€ID
  const timestamp = Date.now().toString(36)
  const random = Math.random().toString(36).slice(2, 6)
  const id = `model_${timestamp}_${random}`
  
  const now = new Date().toISOString()
  const fullConfig: UserModelConfig = {
    ...config,
    id,
    createdAt: now,
    updatedAt: now,
    isDefault: false,
  }
  
  // ๆฃ€ๆŸฅๆ˜ฏๅฆๆ˜ฏ็ฌฌไธ€ไธช้…็ฝฎ๏ผŒ่‡ชๅŠจ่ฎพไธบ้ป˜่ฎค
  const existing = listUserModels(username)
  if (existing.length === 0) {
    fullConfig.isDefault = true
  }
  
  // ๅŠ ๅฏ†ๅญ˜ๅ‚จ
  const filePath = join(modelsDir, `${id}.enc`)
  writeFileSync(filePath, encrypt(fullConfig), 'utf-8')
  setPermission(filePath)
  
  return fullConfig
}

/**
 * ่ฏปๅ–็”จๆˆทๆจกๅž‹้…็ฝฎ
 */
export function loadUserModel(username: string, modelId: string): UserModelConfig | null {
  const modelsDir = getUserModelsDir(username)
  const filePath = join(modelsDir, `${modelId}.enc`)
  
  if (!existsSync(filePath)) return null
  
  try {
    return decrypt<UserModelConfig>(readFileSync(filePath, 'utf-8'))
  } catch {
    return null
  }
}

/**
 * ๆ›ดๆ–ฐ็”จๆˆทๆจกๅž‹้…็ฝฎ
 */
export function updateUserModel(
  username: string,
  modelId: string,
  updates: Partial<Omit<UserModelConfig, 'id' | 'createdAt'>>
): UserModelConfig | null {
  const existing = loadUserModel(username, modelId)
  if (!existing) return null
  
  const updated: UserModelConfig = {
    ...existing,
    ...updates,
    id: modelId,
    createdAt: existing.createdAt,
    updatedAt: new Date().toISOString(),
  }
  
  const modelsDir = getUserModelsDir(username)
  const filePath = join(modelsDir, `${modelId}.enc`)
  writeFileSync(filePath, encrypt(updated), 'utf-8')
  setPermission(filePath)
  
  return updated
}

/**
 * ๅˆ ้™ค็”จๆˆทๆจกๅž‹้…็ฝฎ
 */
export function deleteUserModel(username: string, modelId: string): boolean {
  const modelsDir = getUserModelsDir(username)
  const filePath = join(modelsDir, `${modelId}.enc`)
  
  if (!existsSync(filePath)) return false
  
  try {
    unlinkSync(filePath)
    return true
  } catch {
    return false
  }
}

/**
 * ๅˆ—ๅ‡บ็”จๆˆทๆ‰€ๆœ‰ๆจกๅž‹้…็ฝฎ๏ผˆไธๅซAPI Key๏ผ‰
 */
export function listUserModels(username: string): ModelListItem[] {
  const modelsDir = getUserModelsDir(username)
  
  if (!existsSync(modelsDir)) return []
  
  const files = readdirSync(modelsDir)
    .filter(f => f.endsWith('.enc'))
    .sort()
  
  const models: ModelListItem[] = []
  
  for (const f of files) {
    try {
      const config = decrypt<UserModelConfig>(
        readFileSync(join(modelsDir, f), 'utf-8')
      )
      models.push({
        id: config.id,
        name: config.name,
        model: config.model,
        baseUrl: config.baseUrl,
        note1: config.note1,
        note2: config.note2,
        createdAt: config.createdAt,
        isDefault: config.isDefault || false,
      })
    } catch { /* skip broken files */ }
  }
  
  return models
}

/**
 * ่Žทๅ–็”จๆˆท้ป˜่ฎคๆจกๅž‹้…็ฝฎ๏ผˆๅซAPI Key๏ผ‰
 */
export function getUserDefaultModel(username: string): UserModelConfig | null {
  const modelsDir = getUserModelsDir(username)
  
  if (!existsSync(modelsDir)) return null
  
  const files = readdirSync(modelsDir).filter(f => f.endsWith('.enc'))
  
  for (const f of files) {
    try {
      const config = decrypt<UserModelConfig>(
        readFileSync(join(modelsDir, f), 'utf-8')
      )
      if (config.isDefault) {
        return config
      }
    } catch { /* skip */ }
  }
  
  // ๆฒกๆœ‰้ป˜่ฎค็š„๏ผŒ่ฟ”ๅ›ž็ฌฌไธ€ไธช
  if (files.length > 0) {
    try {
      return decrypt<UserModelConfig>(
        readFileSync(join(modelsDir, files[0]), 'utf-8')
      )
    } catch { /* skip */ }
  }
  
  return null
}

/**
 * ่ฎพ็ฝฎ็”จๆˆท้ป˜่ฎคๆจกๅž‹
 */
export function setUserDefaultModel(username: string, modelId: string): boolean {
  const modelsDir = getUserModelsDir(username)
  
  if (!existsSync(modelsDir)) return false
  
  const files = readdirSync(modelsDir).filter(f => f.endsWith('.enc'))
  let found = false
  
  for (const f of files) {
    try {
      const config = decrypt<UserModelConfig>(
        readFileSync(join(modelsDir, f), 'utf-8')
      )
      
      if (config.id === modelId) {
        config.isDefault = true
        found = true
      } else {
        config.isDefault = false
      }
      
      writeFileSync(join(modelsDir, f), encrypt(config), 'utf-8')
    } catch { /* skip */ }
  }
  
  return found
}

/**
 * ๆต‹่ฏ•ๆจกๅž‹่ฟžๆŽฅ
 */
export async function testUserModelConnection(config: UserModelConfig): Promise<{
  success: boolean
  latencyMs: number
  error?: string
}> {
  return testConnection(config.baseUrl, config.apiKey, config.model)
}

/**
 * ็ปŸ่ฎก็”จๆˆทๆจกๅž‹ๆ•ฐ้‡
 */
export function countUserModels(username: string): number {
  const modelsDir = getUserModelsDir(username)
  if (!existsSync(modelsDir)) return 0
  return readdirSync(modelsDir).filter(f => f.endsWith('.enc')).length
}