📄 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 }
}