📄 model.ts  •  16289 bytes
import { t } from '../i18n.js'
import { BUILTIN_PROVIDERS, testConnection } from '../models.js'
import { createUserModel, loadUserModel, listUserModels, setUserDefaultModel, deleteUserModel } from '../user-models.js'
import { ChatEngine } from '../chat.js'

/** 显示模型选择菜单并获取用户选择 */
export async function interactiveModelSetup(
  username: string,
  color: any,
  BRAND: string,
  MUTED: string,
  SUCCESS: string,
  ERROR: string,
  WARN: string,
  ACCENT: string,
  askQuestion: (q: string) => Promise<string>
): Promise<{
  apiKey: string
  baseUrl: string
  model: string
  name?: string
  note1?: string
  note2?: string
  saveToUser: boolean
} | null> {
  console.log('')
  console.log(`  ${color.bold}${t('model.select_title')}${color.reset}`)
  console.log(`  ${MUTED}──────────────────────────────────────────────────${color.reset}`)
  console.log('')

  // 显示内置模型列表
  BUILTIN_PROVIDERS.forEach((p, i) => {
    const freeTag = p.free ? `${SUCCESS}FREE${color.reset}` : `${WARN}PAID${color.reset}`
    const idx = String(i + 1).padStart(2, ' ')
    console.log(`    ${BRAND}${color.bold}${idx}${color.reset}  ${color.bold}${p.vendor}${color.reset} ${MUTED}·${color.reset} ${p.name}  ${freeTag}`)
    console.log(`        ${MUTED}${p.description}${color.reset}`)
  })

  const customIdx = BUILTIN_PROVIDERS.length + 1
  console.log(`    ${BRAND}${color.bold}${String(customIdx).padStart(2, ' ')}${color.reset}  ${MUTED}${t('model.custom')}${color.reset}`)
  console.log('')
  console.log(`  ${MUTED}──────────────────────────────────────────────────${color.reset}`)
  console.log(`  ${MUTED}${t("model.op_input_hint")}${color.reset}`)
  console.log('')

  // 用户选择
  const choice = await askQuestion(`  ${MUTED}›${color.reset} ${t("model.select_prompt", {max: customIdx})} `)
  if (choice.trim().toLowerCase() === 'q') return null
  const choiceNum = parseInt(choice, 10)

  if (choiceNum >= 1 && choiceNum <= BUILTIN_PROVIDERS.length) {
    const provider = BUILTIN_PROVIDERS[choiceNum - 1]
    console.log('')
    console.log(`  ${SUCCESS}${t("model.selected")} ${color.bold}${provider.vendor} · ${provider.name}${color.reset}`)
    console.log(`  ${MUTED}${t("model.api_key_hint")} ${provider.apiKeyHint}${color.reset}`)
    console.log('')

    const apiKey = await askQuestion(`  ${MUTED}›${color.reset} API Key: `)
    if (!apiKey) {
      console.log(`  ${ERROR}${t("model.api_key_empty")}${color.reset}`)
      return null
    }

    // 连接测试
    process.stdout.write(`  ${ACCENT}${t("model.testing_connection", {vendor: provider.vendor})}${color.reset}`)
    const result = await testConnection(provider.url, apiKey, provider.id)
    process.stdout.write('\r' + ' '.repeat(40) + '\r')
    
    if (result.success) {
      console.log(`  ${SUCCESS}连接成功${color.reset} ${MUTED}(${result.latencyMs}ms)${color.reset}`)
    } else if (result.error?.includes('API Key 无效')) {
      console.log(`  ${ERROR}${result.error}${color.reset}`)
      return null
    } else {
      console.log(`  ${WARN}连接测试: ${result.error || '异常'}${color.reset}`)
      console.log(`  ${MUTED}${t('model.network_unstable')}${color.reset}`)
      const proceed = await askQuestion(`  ${MUTED}›${color.reset} ${t("model.continue_anyway")}`)
      if (proceed.toLowerCase() === 'n') return null
    }

    // 询问是否保存到个人配置
    const saveChoice = await askQuestion(`  ${MUTED}›${color.reset} 保存为个人模型? [Y/n]: `)
    const saveToUser = saveChoice.toLowerCase() !== 'n'

    if (saveToUser) {
      const note1 = await askQuestion(`  ${MUTED}›${color.reset} 备注1 (可选): `)
      const note2 = await askQuestion(`  ${MUTED}›${color.reset} 备注2 (可选): `)
      return { apiKey, baseUrl: provider.url, model: provider.id, name: provider.name, note1, note2, saveToUser }
    }

    return { apiKey, baseUrl: provider.url, model: provider.id, saveToUser }

  } else if (choiceNum === customIdx) {
    console.log('')
    console.log(`  ${color.bold}${t('model.custom_title')}${color.reset}`)
    console.log(`  ${MUTED}${t('model.custom_prompt')}${color.reset}`)
    console.log('')

    const model = await askQuestion(`  ${MUTED}›${color.reset} 模型ID: `)
    const name = await askQuestion(`  ${MUTED}›${color.reset} 显示名称: `)
    const baseUrl = await askQuestion(`  ${MUTED}›${color.reset} API地址: `)
    const apiKey = await askQuestion(`  ${MUTED}›${color.reset} API Key: `)
    const note1 = await askQuestion(`  ${MUTED}›${color.reset} 备注1 (可选): `)
    const note2 = await askQuestion(`  ${MUTED}›${color.reset} 备注2 (可选): `)

    if (!model || !baseUrl || !apiKey) {
      console.log(`  ${ERROR}${t('model.params_empty')}${color.reset}`)
      return null
    }

    process.stdout.write(`  ${ACCENT}${t("model.connection_test")}...${color.reset}`)
    const result = await testConnection(baseUrl, apiKey, model)
    process.stdout.write('\r' + ' '.repeat(30) + '\r')
    
    if (result.success) {
      console.log(`  ${SUCCESS}${t("model.connect_success")}${color.reset} ${MUTED}(${result.latencyMs}ms)${color.reset}`)
    } else {
      console.log(`  ${WARN}${t("model.connection_test")} ${result.error || 'error'}${color.reset}`)
      const proceed = await askQuestion(`  ${MUTED}›${color.reset} ${t("model.continue_anyway")}`)
      if (proceed.toLowerCase() === 'n') return null
    }

    // 自定义模型默认保存到用户目录
    return { apiKey, baseUrl, model, name: name || model, note1, note2, saveToUser: true }

  } else {
    console.log(`  ${ERROR}${t('model.invalid_selection')}${color.reset}`)
    return null
  }
}

/** 处理 /model 命令,显示模型管理界面 */
export async function handleModelCommand(
  username: string,
  config: { apiKey: string; baseUrl: string; model: string; timeoutMs: number },
  engine: any,
  color: any,
  BRAND: string,
  MUTED: string,
  SUCCESS: string,
  ERROR: string,
  ACCENT: string,
  WARN: string,
  askQuestion: (q: string) => Promise<string>
): Promise<{ config: { apiKey: string; baseUrl: string; model: string; timeoutMs: number }; changed: boolean }> {
  const userModels = listUserModels(username)
  let changed = false

  console.log('')
  console.log(`  ${color.bold}${BRAND}${t('model.manage_title')}${color.reset}`)
  console.log(`  ${MUTED}──────────────────────────────────────────────────${color.reset}`)
  
  // 当前使用的模型
  console.log(`  ${ACCENT}${t('model.current')} ${config.model}${color.reset}`)
  if (config.baseUrl) {
    console.log(`  ${MUTED}API: ${config.baseUrl}${color.reset}`)
  }
  console.log('')
  
  // 用户已配置的模型(如果有)
  if (userModels.length > 0) {
    console.log(`  ${SUCCESS}${t('model.your_config')}${color.reset}`)
    userModels.forEach((m, i) => {
      const defaultTag = m.isDefault ? `${ACCENT}${t('model.is_default')}${color.reset} ` : ''
      const currentTag = m.model === config.model ? `${SUCCESS}${t('model.is_current')}${color.reset} ` : ''
      console.log(`    ${BRAND}${color.bold}${i + 1}${color.reset}  ${m.name} ${MUTED}(${m.model})${color.reset} ${defaultTag}${currentTag}`)
      if (m.note1) console.log(`        ${MUTED}${m.note1}${color.reset}`)
    })
    console.log('')
  }
  
  // 内置模型(快速添加)
  const builtinCount = BUILTIN_PROVIDERS.length
    console.log(`  ${ACCENT}${t('model.builtin_fast')}${color.reset}`)
  BUILTIN_PROVIDERS.forEach((p, i) => {
    const freeTag = p.free ? `${SUCCESS}FREE${color.reset}` : `${WARN}PAID${color.reset}`
    const num = userModels.length + i + 1
    console.log(`    ${BRAND}${color.bold}${num}${color.reset}  ${p.name} ${MUTED}(${p.vendor})${color.reset} ${freeTag}`)
  })
  console.log('')
  
  // 操作选项
    console.log(`  ${ACCENT}${t('model.operations')}${color.reset}`)
  if (userModels.length > 0) {
    console.log(`    ${BRAND}1-${userModels.length}${color.reset}  ${ACCENT}${t('model.op_switch')}${color.reset}`)
  }
    console.log(`    ${BRAND}${userModels.length + 1}-${userModels.length + builtinCount}${color.reset}  ${ACCENT}${t('model.op_add_builtin')}${color.reset}`)
    console.log(`    ${BRAND}C${color.reset}  ${ACCENT}${t('model.op_custom')}${color.reset} ${MUTED}${t("model.custom_hint")}${color.reset}`)
  if (userModels.length > 0) {
      console.log(`    ${BRAND}D${color.reset}  ${ACCENT}${t('model.op_delete')}${color.reset}`)
  }
    console.log(`    ${BRAND}Q${color.reset}  ${ACCENT}${t('model.op_cancel')}${color.reset}`)
  console.log(`  ${MUTED}${t("model.op_action_hint")}${color.reset}`)
  console.log('')
  
  const action = await askQuestion(`  ${ACCENT}› ${t('model.select_action')} ${color.reset}`)
  
  const actionLower = action.toLowerCase()
  
  if (actionLower === 'q') {
    // 取消
    return { config, changed: false }
  } else if (actionLower === 'c') {
    // 自定义接入
    const setup = await interactiveModelSetup(username, color, BRAND, MUTED, SUCCESS, ERROR, WARN, ACCENT, askQuestion)
    if (setup && setup.saveToUser) {
      createUserModel(username, {
        name: setup.name || setup.model,
        model: setup.model,
        baseUrl: setup.baseUrl,
        apiKey: setup.apiKey,
        note1: setup.note1,
        note2: setup.note2,
      })
      console.log(`  ${SUCCESS}${t('model.added_switched')}${color.reset}`)
      config = { apiKey: setup.apiKey, baseUrl: setup.baseUrl, model: setup.model, timeoutMs: config.timeoutMs }
      Object.assign(engine, new ChatEngine(config))
      changed = true
    }
  } else if (actionLower === 'd' && userModels.length > 0) {
    // 删除模型
    const idxStr = await askQuestion(`  ${ACCENT}› ${t("model.delete_index", {max: userModels.length})} ${color.reset}`)
    const idx = parseInt(idxStr, 10)
    if (idx >= 1 && idx <= userModels.length) {
      const selected = userModels[idx - 1]
      const confirm = await askQuestion(`  ${ACCENT}› ${t("model.confirm_delete", {name: selected.name})} ${color.reset}`)
      if (confirm.toLowerCase() === 'y') {
        deleteUserModel(username, selected.id)
        console.log(`  ${SUCCESS}${t('model.deleted')} ${selected.name}${color.reset}`)
      }
    } else {
      console.log(`  ${ERROR}${t('model.invalid_selection')}${color.reset}`)
    }
  } else {
    // 数字选择
    const idx = parseInt(action, 10)
    if (idx >= 1 && idx <= userModels.length) {
      // 切换到已配置模型
      const selected = userModels[idx - 1]
      const modelConfig = loadUserModel(username, selected.id)
      if (modelConfig) {
        config = { apiKey: modelConfig.apiKey, baseUrl: modelConfig.baseUrl, model: modelConfig.model, timeoutMs: config.timeoutMs }
        Object.assign(engine, new ChatEngine(config))
        setUserDefaultModel(username, selected.id)
        console.log(`  ${SUCCESS}${t('model.switched')} ${selected.name}${color.reset}`)
        changed = true
      }
    } else if (idx >= userModels.length + 1 && idx <= userModels.length + BUILTIN_PROVIDERS.length) {
      // 添加内置模型
      const provider = BUILTIN_PROVIDERS[idx - userModels.length - 1]
      console.log('')
      console.log(`  ${SUCCESS}${t("model.selected")} ${color.bold}${provider.vendor} · ${provider.name}${color.reset}`)
      console.log(`  ${MUTED}${t("model.api_key_hint")} ${provider.apiKeyHint}${color.reset}`)
      console.log('')
      
      const apiKey = await askQuestion(`  ${ACCENT}› API Key: ${color.reset}`)
      if (!apiKey) {
        console.log(`  ${ERROR}${t("model.api_key_empty")}${color.reset}`)
        return { config, changed: false }
      }
      
      // 连接测试
      process.stdout.write(`  ${ACCENT}${t("api.testing")}${color.reset}`)
      const result = await testConnection(provider.url, apiKey, provider.id)
      process.stdout.write('\r' + ' '.repeat(30) + '\r')
      
      if (result.success) {
        console.log(`  ${SUCCESS}${t("model.connect_success")} (${result.latencyMs}ms)${color.reset}`)
      } else if (result.error?.includes('Invalid API Key')) {
        console.log(`  ${ERROR}${result.error}${color.reset}`)
        return { config, changed: false }
      } else {
        console.log(`  ${WARN}${t("model.connection_test")} ${result.error || 'error'}${color.reset}`)
        const proceed = await askQuestion(`  ${ACCENT}› ${t("model.continue_anyway")} ${color.reset}`)
        if (proceed.toLowerCase() === 'n') return { config, changed: false }
      }
      
      // 保存并切换
      const note1 = await askQuestion(`  ${MUTED}› ${t("model.note1")} ${color.reset}`)
      const note2 = await askQuestion(`  ${MUTED}› ${t("model.note2")} ${color.reset}`)
      
      createUserModel(username, {
        name: provider.name,
        model: provider.id,
        baseUrl: provider.url,
        apiKey,
        note1,
        note2,
      })
      
      config = { apiKey, baseUrl: provider.url, model: provider.id, timeoutMs: config.timeoutMs }
      Object.assign(engine, new ChatEngine(config))
        console.log(`  ${SUCCESS}${t('model.added_switched')} ${provider.name}${color.reset}`)
      changed = true
    } else {
      console.log(`  ${ERROR}${t('model.invalid_selection')}${color.reset}`)
    }
  }

  return { config, changed }
}

/**
 * 非交互式模型快速切换(命令式 /model <model-id>)
 * 用途:绕过交互菜单,直接根据 model-id 切换配置
 * 支持内置模型 ID(如 minimax-m2.7-m1)和用户自定义模型 ID
 * 如果密钥池中有该模型的密钥,直接允许切换(不受 free 标志限制)
 */
export async function switchModelDirect(
  modelId: string,
  config: { apiKey: string; baseUrl: string; model: string; timeoutMs: number },
  color: any,
  SUCCESS: string,
  ERROR: string,
  WARN: string,
): Promise<{ config: typeof config; changed: boolean }> {
  // 0. 先加载密钥池
  const { loadChatKeyPool, getChatKeyPool } = await import('../apikeys.js')
  loadChatKeyPool()
  const pool = getChatKeyPool()
  const poolKey = pool.find(k => k.model === modelId || k.name === modelId)
  const hasPoolKey = !!poolKey

  // 1. 先查用户自定义模型
  const { listUserModels, loadUserModel, setUserDefaultModel } = await import('../user-models.js')
  const userModels = listUserModels('default')
  const userModel = userModels.find(m => m.model === modelId || m.id === modelId)
  if (userModel) {
    const modelConfig = loadUserModel('default', userModel.id)
    if (modelConfig) {
      const newConfig = { ...config, apiKey: modelConfig.apiKey, baseUrl: modelConfig.baseUrl, model: modelConfig.model }
      setUserDefaultModel('default', userModel.id)
      return { config: newConfig, changed: true }
    }
  }

  // 2. 查内置模型
  const { BUILTIN_PROVIDERS } = await import('../models.js')
  const provider = BUILTIN_PROVIDERS.find(p => p.id === modelId)
  if (provider) {
    // 如果密钥池中有该模型密钥(用户已配置),允许切换
    if (hasPoolKey) {
      const newConfig = { ...config, baseUrl: provider.url, model: provider.id }
      return { config: newConfig, changed: true }
    }
    // 无密钥且非免费模型,提示去菜单配置
    if (!provider.free) {
      console.log(`  ${WARN}⚠️ ${provider.name} 是付费模型,需要先配置 API Key${color.reset}`)
      console.log(`  ${WARN}请使用 /model 进入交互菜单添加密钥${color.reset}`)
      return { config, changed: false }
    }
    // 免费内置模型直接切换
    const newConfig = { ...config, baseUrl: provider.url, model: provider.id }
    return { config: newConfig, changed: true }
  }

  // 3. modelId 不存在
  console.log(`  ${ERROR}❌ 未找到模型: ${modelId}${color.reset}`)
  const available = [...userModels.map(m => m.model), ...BUILTIN_PROVIDERS.map(p => p.id)].join(', ')
  console.log(`  ${WARN}可用模型: ${available}${color.reset}`)
  return { config, changed: false }
}