2026-05-22 23:47:28 +08:00
|
|
|
import { ref } from 'vue'
|
|
|
|
|
import { testModelConnectivity } from '../../services/settings.js'
|
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
2026-05-27 09:17:57 +08:00
|
|
|
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
2026-05-22 23:47:28 +08:00
|
|
|
|
|
|
|
|
const MODEL_SECRET_MASK = '********'
|
|
|
|
|
|
|
|
|
|
const MODEL_TEST_CONFIGS = {
|
|
|
|
|
main: {
|
|
|
|
|
label: '主模型',
|
|
|
|
|
providerKey: 'mainProvider',
|
|
|
|
|
modelKey: 'mainModel',
|
|
|
|
|
endpointKey: 'mainEndpoint',
|
|
|
|
|
apiKeyKey: 'mainApiKey',
|
|
|
|
|
capability: 'chat'
|
|
|
|
|
},
|
|
|
|
|
backup: {
|
|
|
|
|
label: '备份模型',
|
|
|
|
|
providerKey: 'backupProvider',
|
|
|
|
|
modelKey: 'backupModel',
|
|
|
|
|
endpointKey: 'backupEndpoint',
|
|
|
|
|
apiKeyKey: 'backupApiKey',
|
|
|
|
|
capability: 'chat'
|
|
|
|
|
},
|
|
|
|
|
embedding: {
|
|
|
|
|
label: 'Embedding 模型',
|
|
|
|
|
providerKey: 'embeddingProvider',
|
|
|
|
|
modelKey: 'embeddingModel',
|
|
|
|
|
endpointKey: 'embeddingEndpoint',
|
|
|
|
|
apiKeyKey: 'embeddingApiKey',
|
|
|
|
|
capability: 'embedding'
|
|
|
|
|
},
|
|
|
|
|
reranker: {
|
|
|
|
|
label: 'Reranker 模型',
|
|
|
|
|
providerKey: 'rerankerProvider',
|
|
|
|
|
modelKey: 'rerankerModel',
|
|
|
|
|
endpointKey: 'rerankerEndpoint',
|
|
|
|
|
apiKeyKey: 'rerankerApiKey',
|
|
|
|
|
capability: 'reranker'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CUSTOM_OPENAI_PROVIDER = 'Custom OpenAI Compatible'
|
|
|
|
|
|
|
|
|
|
const PROVIDER_ENDPOINTS = {
|
|
|
|
|
MiniMax: 'https://api.minimaxi.com/v1',
|
|
|
|
|
GLM: 'https://open.bigmodel.cn/api/paas/v4/',
|
|
|
|
|
Kimi: 'https://api.moonshot.ai/v1',
|
|
|
|
|
Ali: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
|
|
|
Codex: 'https://api.openai.com/v1',
|
|
|
|
|
Claude: 'https://api.anthropic.com/v1/',
|
|
|
|
|
Gemini: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
|
|
|
[CUSTOM_OPENAI_PROVIDER]: ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const RERANKER_PROVIDER_ENDPOINTS = {
|
|
|
|
|
Ali: 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank',
|
|
|
|
|
[CUSTOM_OPENAI_PROVIDER]: ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LEGACY_PROVIDER_MAP = {
|
|
|
|
|
'OpenAI Compatible': 'Codex',
|
|
|
|
|
'Azure OpenAI': CUSTOM_OPENAI_PROVIDER,
|
|
|
|
|
Ollama: CUSTOM_OPENAI_PROVIDER,
|
|
|
|
|
'自定义网关': CUSTOM_OPENAI_PROVIDER
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeValue(value) {
|
|
|
|
|
return String(value ?? '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeProviderValue(value, fallback = 'Codex') {
|
|
|
|
|
const normalized = normalizeValue(value)
|
|
|
|
|
|
|
|
|
|
const providerOptions = Object.keys(PROVIDER_ENDPOINTS)
|
|
|
|
|
if (providerOptions.includes(normalized)) {
|
|
|
|
|
return normalized
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (LEGACY_PROVIDER_MAP[normalized]) {
|
|
|
|
|
return LEGACY_PROVIDER_MAP[normalized]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getProviderEndpoint(provider) {
|
|
|
|
|
return PROVIDER_ENDPOINTS[provider] ?? ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRerankerEndpoint(provider) {
|
|
|
|
|
return RERANKER_PROVIDER_ENDPOINTS[provider] ?? getProviderEndpoint(provider)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isModelConfigReady(provider, model, endpoint) {
|
|
|
|
|
return Boolean(normalizeValue(provider) && normalizeValue(model) && normalizeValue(endpoint))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isModelSecretMask(value) {
|
|
|
|
|
return value === MODEL_SECRET_MASK
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'LlmSettingsPanel',
|
2026-05-27 09:17:57 +08:00
|
|
|
components: {
|
|
|
|
|
EnterpriseSelect
|
|
|
|
|
},
|
2026-05-22 23:47:28 +08:00
|
|
|
props: {
|
|
|
|
|
llmForm: {
|
|
|
|
|
type: Object,
|
|
|
|
|
required: true
|
|
|
|
|
},
|
|
|
|
|
providerOptions: {
|
|
|
|
|
type: Array,
|
|
|
|
|
required: true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
setup(props) {
|
|
|
|
|
const { toast } = useToast()
|
|
|
|
|
const modelTestState = ref({
|
|
|
|
|
main: { status: 'idle', message: '' },
|
|
|
|
|
backup: { status: 'idle', message: '' },
|
|
|
|
|
embedding: { status: 'idle', message: '' },
|
|
|
|
|
reranker: { status: 'idle', message: '' }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function applyProviderPreset(testKey) {
|
|
|
|
|
const config = MODEL_TEST_CONFIGS[testKey]
|
|
|
|
|
const provider = normalizeProviderValue(props.llmForm[config.providerKey], CUSTOM_OPENAI_PROVIDER)
|
|
|
|
|
|
|
|
|
|
props.llmForm[config.providerKey] = provider
|
|
|
|
|
props.llmForm[config.endpointKey] =
|
|
|
|
|
testKey === 'reranker' ? getRerankerEndpoint(provider) : getProviderEndpoint(provider)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getModelTestState(testKey) {
|
|
|
|
|
return modelTestState.value[testKey] || { status: 'idle', message: '' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isModelTesting(testKey) {
|
|
|
|
|
return getModelTestState(testKey).status === 'testing'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearModelSecretMask(testKey) {
|
|
|
|
|
const config = MODEL_TEST_CONFIGS[testKey]
|
|
|
|
|
if (isModelSecretMask(props.llmForm[config.apiKeyKey])) {
|
|
|
|
|
props.llmForm[config.apiKeyKey] = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function testModelConnection(testKey) {
|
|
|
|
|
const config = MODEL_TEST_CONFIGS[testKey]
|
|
|
|
|
const provider = props.llmForm[config.providerKey]
|
|
|
|
|
const model = props.llmForm[config.modelKey]
|
|
|
|
|
const endpoint = props.llmForm[config.endpointKey]
|
|
|
|
|
const apiKey = props.llmForm[config.apiKeyKey]
|
|
|
|
|
|
|
|
|
|
if (!isModelConfigReady(provider, model, endpoint)) {
|
|
|
|
|
const message = `请先完整填写${config.label}的供应商、模型名称和接口地址。`
|
|
|
|
|
modelTestState.value[testKey] = { status: 'error', message }
|
|
|
|
|
toast(message)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
modelTestState.value[testKey] = { status: 'testing', message: '正在测试模型连通性...' }
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
provider,
|
|
|
|
|
model,
|
|
|
|
|
endpoint,
|
|
|
|
|
api_key: isModelSecretMask(apiKey) ? '' : apiKey,
|
|
|
|
|
capability: config.capability,
|
|
|
|
|
slot: testKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await testModelConnectivity(payload)
|
|
|
|
|
modelTestState.value[testKey] = {
|
|
|
|
|
status: result.ok ? 'success' : 'error',
|
|
|
|
|
message: result.detail || (result.ok ? '模型连接成功。' : '模型连接失败。')
|
|
|
|
|
}
|
|
|
|
|
toast(modelTestState.value[testKey].message)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error.message || '模型测试请求失败,请确认 FastAPI 已启动。'
|
|
|
|
|
modelTestState.value[testKey] = { status: 'error', message }
|
|
|
|
|
toast(message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
applyProviderPreset,
|
|
|
|
|
getModelTestState,
|
|
|
|
|
isModelTesting,
|
|
|
|
|
clearModelSecretMask,
|
|
|
|
|
testModelConnection
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|