* Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
import { reactive, readonly } from 'vue'
import { DEFAULT_LLM_MODELS } from '../../constants'
import { formatComponents, getAgentSystemPrompt, getJsonFixPrompt } from '../../constants/prompts'
import {
isValidJsonPatchObjectString,
getRobotServiceOptions,
addSystemPrompt,
jsonPatchAutoFix,
isValidFastJsonPatch,
getJsonObjectString,
fixMethods,
schemaAutoFix
} from '../../utils'
import { ChatMode } from '../../types/mode.types'
import type { ModelConfig, ModelService, RobotSettings, SelectedModelInfo } from '../../types/setting.types'
import apiService from '../../services/api'
import { updatePageSchema } from '../core/pageUpdater'
const SETTING_STORAGE_KEY = 'tiny-engine-robot-settings'
const SETTING_VERSION = 2
const robotSettingState = reactive<RobotSettings>({
version: SETTING_VERSION,
defaultModel: {
serviceId: '',
modelName: ''
},
quickModel: {
serviceId: '',
modelName: ''
},
services: [],
chatMode: ChatMode.Agent,
enableThinking: false
})
const getAIModelOptions = () => {
const customAIModels = getRobotServiceOptions()?.customCompatibleAIModels || []
if (!customAIModels.length) {
return DEFAULT_LLM_MODELS
}
return mergeAIModelOptions(DEFAULT_LLM_MODELS, customAIModels)
}
const initBuiltInServices = (): ModelService[] => {
return getAIModelOptions().map((service: any) => ({
id: service.provider,
provider: service.provider,
label: service.label,
baseUrl: service.baseUrl,
apiKey: '',
allowEmptyApiKey: service.allowEmptyApiKey ?? false,
isBuiltIn: true,
models: service.models
}))
}
const initDefaultSettings = (): RobotSettings => {
const builtInServices = initBuiltInServices()
const firstService = builtInServices[0]
const firstModel = firstService?.models[0]
return {
version: SETTING_VERSION,
defaultModel: {
serviceId: firstService?.id || '',
modelName: firstModel?.name || ''
},
quickModel: {
serviceId: '',
modelName: ''
},
services: builtInServices,
chatMode: ChatMode.Agent,
enableThinking: false
}
}
const migrateOldSettings = (oldSettings: any): RobotSettings | null => {
if (!oldSettings || oldSettings.version === SETTING_VERSION) {
return null
}
const { activeName, existModel, customizeModel, chatMode, enableThinking } = oldSettings
const builtInServices = initBuiltInServices()
const services: ModelService[] = [...builtInServices]
if (activeName === 'existingModels' && existModel) {
const service = services.find((s) => s.baseUrl === existModel.baseUrl)
if (service) {
service.apiKey = existModel.apiKey || ''
}
}
if (activeName === 'customize' && customizeModel?.baseUrl) {
const customService: ModelService = {
id: `custom_${Date.now()}`,
provider: 'custom',
label: '自定义服务',
baseUrl: customizeModel.baseUrl,
apiKey: customizeModel.apiKey || '',
allowEmptyApiKey: false,
isBuiltIn: false,
models: [
{
name: customizeModel.model || 'default',
label: customizeModel.model || '默认模型',
capabilities: {}
}
]
}
if (customizeModel.completeModel) {
customService.models.push({
name: customizeModel.completeModel,
label: customizeModel.completeModel,
capabilities: { compact: true }
})
}
services.push(customService)
}
const selectedModel = activeName === 'existingModels' ? existModel : customizeModel
const defaultServiceId =
activeName === 'existingModels' ? services.find((s) => s.baseUrl === selectedModel?.baseUrl)?.id : ''
return {
version: SETTING_VERSION,
defaultModel: {
serviceId: defaultServiceId || services[0]?.id || '',
modelName: selectedModel?.model || services[0]?.models[0]?.name || ''
},
quickModel: {
serviceId: defaultServiceId || '',
modelName: selectedModel?.completeModel || ''
},
services,
chatMode: chatMode || ChatMode.Agent,
enableThinking: enableThinking || false
} as RobotSettings
}
const loadRobotSettingState = (): RobotSettings | null => {
const items = localStorage.getItem(SETTING_STORAGE_KEY)
if (!items) {
return null
}
try {
const parsed = JSON.parse(items)
if (!parsed.version || parsed.version < SETTING_VERSION) {
const migrated = migrateOldSettings(parsed)
if (migrated) {
saveRobotSettingState(migrated)
return migrated
}
}
return parsed
} catch (error) {
return null
}
}
const saveRobotSettingState = (state: Partial<RobotSettings>, updateState = true) => {
if (updateState) {
Object.assign(robotSettingState, state)
}
const currentState = loadRobotSettingState() || initDefaultSettings()
const newState = { ...currentState, ...state, version: SETTING_VERSION }
localStorage.setItem(SETTING_STORAGE_KEY, JSON.stringify(newState))
}
* 合并默认模型服务配置和用户自定义配置
* @param defaults 默认的模型服务配置列表
* @param customs 用户自定义的模型服务配置列表
* @returns 合并后的模型服务配置列表
*
* 支持的操作:
* 1. 删除整个服务:{ provider: 'deepseek', _remove: true }
* 2. 修改服务属性:{ provider: 'qwen', label: '新名称', baseUrl: '新地址' }
* 3. 删除服务中的模型:{ provider: 'qwen', models: [{ name: 'qwen3-8b', _remove: true }] }
* 4. 添加新模型:{ provider: 'qwen', models: [{ name: 'new-model', label: '新模型' }] }
* 5. 覆盖模型配置:{ provider: 'qwen', models: [{ name: 'qwen-plus', label: '新标签', capabilities: {...} }] }
* 6. 添加新服务:{ provider: 'openai', label: 'OpenAI', baseUrl: '...', models: [...] }
*/
const mergeAIModelOptions = (defaults: any[], customs: any[]): any[] => {
const result = JSON.parse(JSON.stringify(defaults))
customs.forEach((customProvider) => {
if (customProvider._remove) {
const index = result.findIndex((p: any) => p.provider === customProvider.provider)
if (index !== -1) {
result.splice(index, 1)
}
return
}
const existingProviderIndex = result.findIndex((p: any) => p.provider === customProvider.provider)
if (existingProviderIndex !== -1) {
const existingProvider = result[existingProviderIndex]
if (customProvider.label !== undefined) existingProvider.label = customProvider.label
if (customProvider.baseUrl !== undefined) existingProvider.baseUrl = customProvider.baseUrl
if (customProvider.allowEmptyApiKey !== undefined)
existingProvider.allowEmptyApiKey = customProvider.allowEmptyApiKey
customProvider.models?.forEach((customModel: any) => {
if (customModel._remove) {
const modelIndex = existingProvider.models.findIndex((m: any) => m.name === customModel.name)
if (modelIndex !== -1) {
existingProvider.models.splice(modelIndex, 1)
}
} else {
const existingModelIndex = existingProvider.models.findIndex((m: any) => m.name === customModel.name)
if (existingModelIndex !== -1) {
const { _remove, ...modelWithoutRemove } = customModel
existingProvider.models[existingModelIndex] = modelWithoutRemove
} else {
const { _remove, ...modelWithoutRemove } = customModel
existingProvider.models.push(modelWithoutRemove)
}
}
})
} else {
const { _remove, ...providerWithoutRemove } = customProvider
providerWithoutRemove.models = (providerWithoutRemove.models || [])
.filter((m: any) => !m._remove)
.map((m: any) => {
const { _remove, ...modelWithoutRemove } = m
return modelWithoutRemove
})
result.push(providerWithoutRemove)
}
})
return result
}
const mergeServices = (services: ModelService[] = [], builtInServices: ModelService[]): ModelService[] => {
const cachedServiceMap = new Map(services.map((s) => [s.id, s]))
const result: ModelService[] = []
builtInServices.forEach((builtIn) => {
const cached = cachedServiceMap.get(builtIn.id)
if (cached) {
result.push({
...builtIn,
apiKey: cached.apiKey || ''
})
cachedServiceMap.delete(builtIn.id)
} else {
result.push({ ...builtIn })
}
})
cachedServiceMap.forEach((service) => {
if (!service.isBuiltIn) {
result.push(service)
}
})
return result
}
export const init = () => {
let settingState = loadRobotSettingState()
if (!settingState) {
settingState = initDefaultSettings()
} else {
settingState.services = mergeServices(settingState.services, initBuiltInServices())
}
saveRobotSettingState(settingState)
}
const getModelCapabilities = (serviceId: string, modelName: string) => {
if (!serviceId || !modelName) {
return null
}
const service = robotSettingState.services.find((s) => s.id === serviceId)
return service?.models.find((m) => m.name === modelName)?.capabilities
}
const getAllAvailableModels = () => {
return robotSettingState.services.flatMap((service) =>
service.models.map((model) => ({
serviceId: service.id,
serviceName: service.label,
modelName: model.name,
modelLabel: model.label,
capabilities: model.capabilities || {},
displayLabel: `${service.label} - ${model.label}`,
value: `${service.id}::${model.name}`
}))
)
}
const getCompactModels = () => {
return getAllAvailableModels().filter((model) => model.capabilities?.compact)
}
const updateThinkingState = (value: boolean) => {
robotSettingState.enableThinking = value
saveRobotSettingState({ enableThinking: robotSettingState.enableThinking })
}
const updateChatModeState = (value: string) => {
robotSettingState.chatMode = value
saveRobotSettingState({ chatMode: robotSettingState.chatMode })
}
const encryptServiceApiKey = async (apiKey: string): Promise<string> => {
if (!apiKey || !getRobotServiceOptions()?.encryptServiceApiKey || apiKey.startsWith('EKEY_')) return apiKey
try {
const { token } = await apiService.encryptKey(apiKey)
return token
} catch (error) {
const logger = console
logger.error('加密API密钥失败', error)
return apiKey
}
}
const addCustomService = async (service: Omit<ModelService, 'id' | 'isBuiltIn'>) => {
const newService: ModelService = {
...service,
id: `custom_${Date.now()}`,
isBuiltIn: false,
apiKey: await encryptServiceApiKey(service.apiKey)
}
robotSettingState.services.push(newService)
saveRobotSettingState({ services: robotSettingState.services }, false)
return newService.id
}
const updateService = async (serviceId: string, updates: Partial<ModelService>) => {
const index = robotSettingState.services.findIndex((s) => s.id === serviceId)
if (index !== -1) {
Object.assign(robotSettingState.services[index], {
...updates,
...('apiKey' in updates ? { apiKey: await encryptServiceApiKey(updates.apiKey || '') } : {})
})
saveRobotSettingState({ services: robotSettingState.services }, false)
}
}
const deleteService = (serviceId: string) => {
const index = robotSettingState.services.findIndex((s) => s.id === serviceId)
if (index !== -1 && !robotSettingState.services[index].isBuiltIn) {
robotSettingState.services.splice(index, 1)
saveRobotSettingState({ services: robotSettingState.services }, false)
}
}
const getServiceById = (serviceId: string) => {
return robotSettingState.services.find((s) => s.id === serviceId)
}
const getSelectedModelInfo = (): SelectedModelInfo => {
const currentService: ModelService | undefined = getServiceById(robotSettingState.defaultModel.serviceId)
const currentModel: ModelConfig | undefined = currentService?.models.find(
(m) => m.name === robotSettingState.defaultModel.modelName
)
const { name = '', label = '', capabilities = {} } = currentModel || {}
const { models, ...service } = currentService ?? ({} as Partial<ModelService>)
return {
name,
label,
capabilities,
service: (currentService ? service : null) as ModelService | null,
config: {
chatMode: robotSettingState.chatMode,
enableThinking: robotSettingState.enableThinking
},
model: robotSettingState.defaultModel.modelName,
baseUrl: currentService?.baseUrl || '',
apiKey: currentService?.apiKey || ''
}
}
const getSelectedQuickModelInfo = (): SelectedModelInfo => {
const currentService: ModelService | undefined = getServiceById(robotSettingState.quickModel.serviceId)
const currentModel: ModelConfig | undefined = currentService?.models.find(
(m) => m.name === robotSettingState.quickModel.modelName
)
const { name = '', label = '', capabilities = {} } = currentModel || {}
const { models, ...service } = currentService ?? ({} as Partial<ModelService>)
return {
name,
label,
capabilities,
service: (currentService ? service : null) as ModelService | null,
model: robotSettingState.quickModel.modelName,
completeModel: robotSettingState.quickModel.modelName || '',
baseUrl: currentService?.baseUrl || '',
apiKey: currentService?.apiKey || ''
}
}
export default () => {
return {
robotSettingState: readonly(robotSettingState),
updateThinkingState,
updateChatModeState,
saveRobotSettingState,
loadRobotSettingState,
getAIModelOptions,
getModelCapabilities,
getAllAvailableModels,
getCompactModels,
getSelectedModelInfo,
getSelectedQuickModelInfo,
addCustomService,
updateService,
deleteService,
getServiceById,
formatComponents,
getAgentSystemPrompt,
getJsonFixPrompt,
isValidJsonPatchObjectString,
getRobotServiceOptions,
addSystemPrompt,
jsonPatchAutoFix,
isValidFastJsonPatch,
getJsonObjectString,
fixMethods,
schemaAutoFix,
updatePageSchema
}
}