Skip to content

Commit ab2544c

Browse files
committed
update: 适配某些模型可以自动选择,无需手动输入模型名称(部分服务商协议不标准不支持)
1 parent 2ec767a commit ab2544c

File tree

3 files changed

+190
-42
lines changed

3 files changed

+190
-42
lines changed

src/main/java/com/xiaozhi/controller/ConfigController.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.xiaozhi.controller;
22

3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.core.type.TypeReference;
36
import com.github.pagehelper.PageInfo;
47
import com.xiaozhi.common.web.AjaxResult;
58
import com.xiaozhi.common.web.PageFilter;
@@ -11,9 +14,17 @@
1114
import com.xiaozhi.utils.CmsUtils;
1215
import jakarta.annotation.Resource;
1316
import jakarta.servlet.http.HttpServletRequest;
17+
18+
import org.springframework.http.HttpEntity;
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.http.HttpMethod;
21+
import org.springframework.http.ResponseEntity;
1422
import org.springframework.web.bind.annotation.*;
23+
import org.springframework.web.client.HttpClientErrorException;
24+
import org.springframework.web.client.RestTemplate;
1525

1626
import java.util.List;
27+
import java.util.Map;
1728

1829
/**
1930
* 配置管理
@@ -108,4 +119,56 @@ public AjaxResult add(SysConfig config) {
108119
return AjaxResult.error();
109120
}
110121
}
122+
123+
@PostMapping("/getModels")
124+
@ResponseBody
125+
public AjaxResult getModels(SysConfig config) {
126+
try {
127+
RestTemplate restTemplate = new RestTemplate();
128+
// 设置请求头
129+
HttpHeaders headers = new HttpHeaders();
130+
headers.set("Authorization", "Bearer " + config.getApiKey());
131+
132+
// 构建请求实体
133+
HttpEntity<String> entity = new HttpEntity<>(headers);
134+
135+
// 调用 /v1/models 接口,解析为 JSON 字符串
136+
ResponseEntity<String> response = restTemplate.exchange(
137+
config.getApiUrl() + "/models",
138+
HttpMethod.GET,
139+
entity,
140+
String.class);
141+
142+
// 使用 ObjectMapper 解析 JSON 响应
143+
ObjectMapper objectMapper = new ObjectMapper();
144+
JsonNode rootNode = objectMapper.readTree(response.getBody());
145+
146+
// 提取 "data" 字段
147+
JsonNode dataNode = rootNode.get("data");
148+
if (dataNode == null || !dataNode.isArray()) {
149+
return AjaxResult.error("响应数据格式错误,缺少 data 字段或 data 不是数组");
150+
}
151+
152+
// 将 "data" 字段解析为 List<Map<String, Object>>
153+
List<Map<String, Object>> modelList = objectMapper.convertValue(
154+
dataNode,
155+
new TypeReference<List<Map<String, Object>>>() {
156+
});
157+
158+
// 返回成功结果
159+
AjaxResult result = AjaxResult.success();
160+
result.put("data", modelList);
161+
return result;
162+
163+
} catch (HttpClientErrorException e) {
164+
// 捕获 HTTP 客户端异常并返回详细错误信息
165+
String errorMessage = e.getResponseBodyAsString();
166+
// 返回详细错误信息到前端
167+
return AjaxResult.error("调用模型接口失败: " + errorMessage);
168+
169+
} catch (Exception e) {
170+
// 捕获其他异常并记录日志
171+
return AjaxResult.error();
172+
}
173+
}
111174
}

web/src/components/ConfigManager.vue

Lines changed: 125 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
<a-space>
4949
<a href="javascript:" @click="edit(record)">编辑</a>
5050
<!-- 添加设为默认按钮,但在TTS中不显示 -->
51-
<a v-if="configType !== 'tts' && record.isDefault != 1" href="javascript:" :disabled="record.isDefault == 1"
52-
@click="setAsDefault(record)">设为默认</a>
51+
<a v-if="configType !== 'tts' && record.isDefault != 1" href="javascript:"
52+
:disabled="record.isDefault == 1" @click="setAsDefault(record)">设为默认</a>
5353
<a-popconfirm :title="`确定要删除这个${configTypeInfo.label}配置吗?`"
5454
@confirm="deleteConfig(record.configId)">
5555
<a v-if="record.isDefault != 1" href="javascript:" style="color: #ff4d4f">删除</a>
@@ -76,18 +76,29 @@
7676
</a-col>
7777
<a-col :xl="16" :lg="12" :xs="24">
7878
<a-form-item :label="`${configTypeInfo.label}名称`">
79-
<!-- 修改这里,添加模型名称提示 -->
80-
<a-tooltip v-if="configType === 'llm' && currentType && getModelNameTip(currentType)"
81-
:title="getModelNameTip(currentType)" placement="top">
82-
<a-input v-decorator="[
79+
<!-- 如果是 llm 且有 currentType,变为可输入的下拉框 -->
80+
<a-select v-if="configType === 'llm' && currentType"
81+
v-decorator="[
8382
'configName',
8483
{ rules: [{ required: true, message: `请输入${configTypeInfo.label}名称` }] }
85-
]" autocomplete="off" :placeholder="`请输入${configTypeInfo.label}名称`" />
86-
</a-tooltip>
87-
<a-input v-else v-decorator="[
88-
'configName',
89-
{ rules: [{ required: true, message: `请输入${configTypeInfo.label}名称` }] }
90-
]" autocomplete="off" :placeholder="`请输入${configTypeInfo.label}名称`" />
84+
]"
85+
showSearch
86+
allowClear
87+
:placeholder="`请输入${configTypeInfo.label}名称`"
88+
:options="modelOptions"
89+
:filterOption="modelFilterOption"
90+
@search="handleModelInputChange"
91+
@change="handleModelChange"
92+
@blur="handleModelBlur">
93+
</a-select>
94+
<!-- 如果不是 llm 或没有 currentType,保留原来的输入框 -->
95+
<a-input v-else
96+
v-decorator="[
97+
'configName',
98+
{ rules: [{ required: true, message: `请输入${configTypeInfo.label}名称` }] }
99+
]"
100+
autocomplete="off"
101+
:placeholder="`请输入${configTypeInfo.label}名称`" />
91102
</a-form-item>
92103
</a-col>
93104
</a-row>
@@ -116,7 +127,8 @@
116127
<a-input v-decorator="[
117128
field.name,
118129
{ rules: [{ required: field.required, message: `请输入${field.label}` }] }
119-
]" :placeholder="`请输入${field.label}`" :type="field.inputType || 'text'">
130+
]" :placeholder="`请输入${field.label}`" :type="field.inputType || 'text'"
131+
@change="getModelList()">
120132
<template v-if="field.suffix" slot="suffix">
121133
<span style="color: #999">{{ field.suffix }}</span>
122134
</template>
@@ -185,13 +197,7 @@ export default {
185197
currentType: '',
186198
loading: false,
187199
188-
// 模型名称提示信息
189-
// TODO 需要扩展每项模型提示,或者为每项增加默认模型名称
190-
modelNameTips: {
191-
openai: "请输入要调用的模型名称,如:gpt-3.5-turbo, gpt-4, qwen-max, deepseek-chat等",
192-
ollama: "请输入要调用的Ollama模型名称,如:deepseek-r1, qwen2.5:7b, gemma3:12b等",
193-
spark: "请输入星火大模型官方模型名称,如:Lite, Pro, Max等"
194-
},
200+
modelOptions: [], // 存储模型下拉框选项
195201
196202
columns: [
197203
{
@@ -286,9 +292,77 @@ export default {
286292
this.getData()
287293
},
288294
methods: {
289-
// 获取模型名称提示
290-
getModelNameTip(providerType) {
291-
return this.configType === 'llm' ? this.modelNameTips[providerType] : null;
295+
getModelList() {
296+
297+
const formValues = this.configForm.getFieldsValue();
298+
const apiKey = formValues.apiKey;
299+
const apiUrl = formValues.apiUrl;
300+
301+
// 检查是否输入了必要的参数
302+
if (!apiKey || !apiUrl) {
303+
return;
304+
}
305+
axios
306+
.post({
307+
url: api.config.getModels,
308+
data: {
309+
...formValues
310+
}
311+
})
312+
.then(res => {
313+
if (res.code === 200) {
314+
this.modelOptions = res.data.map((item) => ({
315+
value: item.id,
316+
label: item.id,
317+
}));
318+
}
319+
})
320+
.catch(() => {
321+
this.showError();
322+
})
323+
},
324+
325+
filterOption(input, option) {
326+
return option.label.toLowerCase().includes(input.toLowerCase());
327+
},
328+
329+
// 处理输入变化
330+
handleModelInputChange(value) {
331+
this.$nextTick(() => {
332+
// 手动绑定输入的值到表单字段
333+
setTimeout(() => {
334+
this.configForm.setFieldsValue({
335+
configName: value
336+
});
337+
}, 0);
338+
});
339+
},
340+
341+
// 处理选项变化
342+
handleModelChange(value) {
343+
// 如果用户选择了一个选项,直接更新表单字段
344+
this.$nextTick(() => {
345+
// 手动绑定输入的值到表单字段
346+
setTimeout(() => {
347+
this.configForm.setFieldsValue({
348+
configName: value
349+
});
350+
}, 0);
351+
});
352+
},
353+
354+
// 处理失去焦点时的逻辑
355+
handleModelBlur() {
356+
const value = this.configForm.getFieldValue('configName');
357+
// 如果输入的值不在选项列表中,保留用户输入的值
358+
this.$nextTick(() => {
359+
// 手动绑定输入的值到表单字段
360+
setTimeout(() => {
361+
this.configForm.setFieldsValue({
362+
configName: value
363+
});
364+
}, 0);
365+
});
292366
},
293367
294368
// 处理标签页切换
@@ -313,7 +387,7 @@ export default {
313387
configName: formValues.configName,
314388
configDesc: formValues.configDesc
315389
};
316-
390+
317391
// 如果不是TTS,添加isDefault字段
318392
if (this.configType !== 'tts') {
319393
newValues.isDefault = formValues.isDefault;
@@ -376,21 +450,28 @@ export default {
376450
e.preventDefault()
377451
this.configForm.validateFields((err, values) => {
378452
if (!err) {
379-
this.loading = true
380453
454+
if (this.configType === 'llm') {
455+
// 校验 configName 是否为英文、数字或者它们的组合
456+
const configName = values.configName;
457+
const containsChineseRegex = /[\u4e00-\u9fa5]/; // 检测是否包含中文字符
458+
if (containsChineseRegex.test(configName)) {
459+
this.$message.error('模型名称不能随意输入,请输入正确的模型名称,例如:deepseek-chat、qwen-plus官方名称');
460+
return;
461+
}
462+
}
463+
381464
// 处理可能的URL后缀重复问题
382-
if (values.apiUrl) {
383-
const currentType = values.provider;
384-
const typeFields = this.configTypeInfo.typeFields || {};
385-
const apiUrlField = (typeFields[currentType] || []).find(field => field.name === 'apiUrl');
386-
387-
if (apiUrlField && apiUrlField.suffix) {
388-
const suffix = apiUrlField.suffix;
389-
// 检查URL是否已经以后缀结尾,如果是则不再添加
390-
if (values.apiUrl.endsWith(suffix)) {
391-
// 移除URL末尾的后缀部分
392-
values.apiUrl = values.apiUrl.substring(0, values.apiUrl.length - suffix.length);
393-
}
465+
const currentType = values.provider;
466+
const typeFields = this.configTypeInfo.typeFields || {};
467+
const apiUrlField = (typeFields[currentType] || []).find(field => field.name === 'apiUrl');
468+
469+
if (apiUrlField && apiUrlField.suffix) {
470+
const suffix = apiUrlField.suffix;
471+
// 检查URL是否已经以后缀结尾,如果是则不再添加
472+
if (values.apiUrl.endsWith(suffix)) {
473+
// 移除URL末尾的后缀部分
474+
values.apiUrl = values.apiUrl.substring(0, values.apiUrl.length - suffix.length);
394475
}
395476
}
396477
@@ -405,6 +486,7 @@ export default {
405486
if (this.configType !== 'tts') {
406487
formData.isDefault = values.isDefault ? 1 : 0;
407488
}
489+
this.loading = true
408490
409491
const url = this.editingConfigId
410492
? api.config.update
@@ -451,14 +533,15 @@ export default {
451533
452534
// 设置表单值,使用setTimeout确保表单已渲染
453535
setTimeout(() => {
454-
const formValues = {...record};
455-
536+
const formValues = { ...record };
537+
456538
// 只有非TTS类型才设置isDefault
457539
if (this.configType !== 'tts') {
458540
formValues.isDefault = record.isDefault == 1;
459541
}
460-
542+
461543
configForm.setFieldsValue(formValues);
544+
this.getModelList();
462545
}, 0);
463546
})
464547
},
@@ -467,7 +550,7 @@ export default {
467550
setAsDefault(record) {
468551
// TTS不应该有这个功能,但为了安全起见,再次检查
469552
if (this.configType === 'tts') return;
470-
553+
471554
this.$confirm({
472555
title: `确定要将此${this.configTypeInfo.label}设为默认吗?`,
473556
content: `设为默认后,系统将优先使用此${this.configTypeInfo.label}配置,原默认${this.configTypeInfo.label}将被取消默认状态。`,
@@ -534,6 +617,7 @@ export default {
534617
resetForm() {
535618
this.configForm.resetFields()
536619
this.currentType = ''
620+
this.modelOptions = []
537621
this.editingConfigId = null
538622
}
539623
}

web/src/services/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export default {
4242
config: {
4343
add: "/api/config/add",
4444
query: "/api/config/query",
45-
update: "/api/config/update"
45+
update: "/api/config/update",
46+
getModels: "/api/config/getModels"
4647
},
4748
upload: "/api/file/upload"
4849
};

0 commit comments

Comments
 (0)