Skip to content

Commit 02d3e41

Browse files
pythonHuangclaude
andcommitted
feat: 大模型设置页增强-供应商预置下拉可直接输入且自动填默认接口地址(DeepSeek/通义/Kimi/OpenAI/智谱/Ollama),获取模型/配置测试按钮拉取可用模型,默认模型下拉选择,可用模型多选下拉
Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 1500ee5 commit 02d3e41

2 files changed

Lines changed: 125 additions & 6 deletions

File tree

‎Juggle.Api/Controllers/Api/AiController.cs‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,54 @@ public class AiController : ControllerBase
2121
private readonly AiService _aiService;
2222
private readonly JuggleDbContext _db;
2323
private readonly ITenantAccessor _tenant;
24+
private readonly IHttpClientFactory _httpClientFactory;
2425

25-
public AiController(AiService aiService, JuggleDbContext db, ITenantAccessor tenant)
26+
public AiController(AiService aiService, JuggleDbContext db, ITenantAccessor tenant, IHttpClientFactory httpClientFactory)
2627
{
2728
_aiService = aiService;
2829
_db = db;
2930
_tenant = tenant;
31+
_httpClientFactory = httpClientFactory;
3032
}
3133

34+
/// <summary>拉取供应商可用模型列表(兼作配置测试:密钥正确返回模型列表)</summary>
35+
[HttpPost("fetch-models")]
36+
public async Task<ApiResult> FetchModels([FromBody] AiFetchModelsRequest req)
37+
{
38+
try
39+
{
40+
var url = (req.BaseUrl ?? "").Trim().TrimEnd('/');
41+
if (!url.EndsWith("/models", StringComparison.OrdinalIgnoreCase))
42+
url += "/models";
43+
var client = _httpClientFactory.CreateClient();
44+
client.Timeout = TimeSpan.FromSeconds(30);
45+
using var msg = new HttpRequestMessage(HttpMethod.Get, url);
46+
if (!string.IsNullOrWhiteSpace(req.ApiKey))
47+
msg.Headers.TryAddWithoutValidation("Authorization", $"Bearer {req.ApiKey.Trim()}");
48+
using var resp = await client.SendAsync(msg);
49+
var body = await resp.Content.ReadAsStringAsync();
50+
if (!resp.IsSuccessStatusCode)
51+
throw new Exception($"接口返回 {(int)resp.StatusCode}: {Truncate(body, 200)}");
52+
using var doc = System.Text.Json.JsonDocument.Parse(body);
53+
var models = new List<string>();
54+
if (doc.RootElement.TryGetProperty("data", out var dataEl) && dataEl.ValueKind == System.Text.Json.JsonValueKind.Array)
55+
{
56+
foreach (var m in dataEl.EnumerateArray())
57+
{
58+
var id = m.TryGetProperty("id", out var idEl) ? idEl.GetString() : null;
59+
if (!string.IsNullOrEmpty(id) && !models.Contains(id)) models.Add(id);
60+
}
61+
}
62+
return ApiResult.Success(new { models });
63+
}
64+
catch (Exception ex)
65+
{
66+
return ApiResult.Fail($"配置测试失败: {ex.Message}");
67+
}
68+
}
69+
70+
private static string Truncate(string s, int len) => s.Length <= len ? s : s[..len] + "...";
71+
3272
// ==================== 供应商管理 ====================
3373

3474
/// <summary>供应商列表(含启停状态)</summary>
@@ -218,3 +258,9 @@ public class AiProviderToggleRequest
218258
public long Id { get; set; }
219259
public bool Enabled { get; set; }
220260
}
261+
262+
public class AiFetchModelsRequest
263+
{
264+
public string BaseUrl { get; set; } = "";
265+
public string ApiKey { get; set; } = "";
266+
}

‎JuggleNet6.Frontend/src/views/system/AiProviderList.vue‎

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,33 @@
3030
</div>
3131
</el-card>
3232

33-
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑供应商' : '添加供应商'" width="560px">
33+
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑供应商' : '添加供应商'" width="620px">
3434
<el-form :model="form" label-width="90px">
35-
<el-form-item label="供应商名称"><el-input v-model="form.providerName" placeholder="如 DeepSeek / 通义千问" /></el-form-item>
35+
<el-form-item label="供应商名称">
36+
<el-select v-model="form.providerName" filterable allow-create default-first-option placeholder="选择或输入供应商名称"
37+
style="width:100%" @change="onProviderNameChange">
38+
<el-option v-for="p in presetProviders" :key="p.name" :label="p.name" :value="p.name" />
39+
</el-select>
40+
</el-form-item>
3641
<el-form-item label="接口地址"><el-input v-model="form.baseUrl" placeholder="https://api.deepseek.com/v1" /></el-form-item>
37-
<el-form-item label="API Key"><el-input v-model="form.apiKey" type="password" show-password placeholder="sk-..." /></el-form-item>
38-
<el-form-item label="默认模型"><el-input v-model="form.model" placeholder="deepseek-chat" /></el-form-item>
39-
<el-form-item label="可用模型"><el-input v-model="form.models" placeholder="逗号分隔,如 deepseek-chat,deepseek-reasoner" /></el-form-item>
42+
<el-form-item label="API Key">
43+
<div style="display:flex;gap:6px;width:100%">
44+
<el-input v-model="form.apiKey" type="password" show-password placeholder="sk-..." style="flex:1" />
45+
<el-button :loading="testingConfig" @click="doFetchModels">获取模型/测试</el-button>
46+
</div>
47+
<div v-if="testMsg" :style="`font-size:12px;margin-top:4px;color:${testOk ? '#67c23a' : '#f56c6c'}`">{{ testMsg }}</div>
48+
</el-form-item>
49+
<el-form-item label="默认模型">
50+
<el-select v-model="form.model" filterable allow-create default-first-option placeholder="选择或输入模型名" style="width:100%">
51+
<el-option v-for="m in fetchedModels" :key="m" :label="m" :value="m" />
52+
</el-select>
53+
</el-form-item>
54+
<el-form-item label="可用模型">
55+
<el-select v-model="selectedModels" multiple filterable allow-create default-first-option
56+
collapse-tags collapse-tags-tooltip placeholder="多选可用模型(对话中可切换)" style="width:100%">
57+
<el-option v-for="m in fetchedModels" :key="m" :label="m" :value="m" />
58+
</el-select>
59+
</el-form-item>
4060
<el-form-item label="启用"><el-switch v-model="form.enabled" /></el-form-item>
4161
<el-form-item label="备注"><el-input v-model="form.remark" /></el-form-item>
4262
</el-form>
@@ -57,6 +77,22 @@ const loading = ref(false)
5777
const tableData = ref<any[]>([])
5878
const dialogVisible = ref(false)
5979
const form = ref<any>({ id: 0, providerName: '', baseUrl: '', apiKey: '', model: '', models: '', enabled: true, remark: '' })
80+
// 模型拉取/多选状态
81+
const fetchedModels = ref<string[]>([])
82+
const selectedModels = ref<string[]>([])
83+
const testingConfig = ref(false)
84+
const testMsg = ref('')
85+
const testOk = ref(false)
86+
87+
// 预置供应商(选择后自动填默认接口地址)
88+
const presetProviders = [
89+
{ name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1' },
90+
{ name: '通义千问', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
91+
{ name: 'Kimi (Moonshot)', baseUrl: 'https://api.moonshot.cn/v1' },
92+
{ name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' },
93+
{ name: '智谱GLM', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' },
94+
{ name: 'Ollama', baseUrl: 'http://localhost:11434/v1' }
95+
]
6096
6197
onMounted(loadData)
6298
@@ -70,13 +106,50 @@ async function loadData() {
70106
71107
function openAdd() {
72108
form.value = { id: 0, providerName: '', baseUrl: '', apiKey: '', model: '', models: '', enabled: true, remark: '' }
109+
fetchedModels.value = []
110+
selectedModels.value = []
111+
testMsg.value = ''
73112
dialogVisible.value = true
74113
}
75114
function openEdit(row: any) {
76115
form.value = { ...row, enabled: row.enabled === 1 }
116+
fetchedModels.value = String(row.models || '').split(',').map((s: string) => s.trim()).filter(Boolean)
117+
selectedModels.value = [...fetchedModels.value]
118+
testMsg.value = ''
77119
dialogVisible.value = true
78120
}
121+
122+
/** 选择预置供应商 → 自动填接口地址默认值 */
123+
function onProviderNameChange() {
124+
const preset = presetProviders.find((p: any) => p.name === form.value.providerName)
125+
if (preset && !form.value.baseUrl) form.value.baseUrl = preset.baseUrl
126+
}
127+
128+
/** 获取模型列表(兼作配置测试) */
129+
async function doFetchModels() {
130+
if (!form.value.baseUrl) { ElMessage.warning('请先填写接口地址'); return }
131+
testingConfig.value = true
132+
testMsg.value = ''
133+
try {
134+
const res: any = await request.post('/ai/fetch-models', { baseUrl: form.value.baseUrl, apiKey: form.value.apiKey })
135+
const models: string[] = res.data?.models || []
136+
fetchedModels.value = models
137+
// 默认模型:优先保留当前选择,否则取第一个
138+
if (models.length > 0) {
139+
if (!form.value.model || !models.includes(form.value.model)) form.value.model = models[0]
140+
selectedModels.value = models.filter((m: string) => selectedModels.value.includes(m))
141+
if (selectedModels.value.length === 0) selectedModels.value = [...models]
142+
}
143+
testOk.value = true
144+
testMsg.value = `✅ 配置测试通过,共 ${models.length} 个模型`
145+
} catch (e: any) {
146+
testOk.value = false
147+
testMsg.value = e?.message || '测试失败'
148+
} finally { testingConfig.value = false }
149+
}
150+
79151
async function doSave() {
152+
form.value.models = selectedModels.value.join(',')
80153
await request.post('/ai/provider/save', form.value)
81154
ElMessage.success('保存成功')
82155
dialogVisible.value = false

0 commit comments

Comments
 (0)