```
feat(api): 添加万川平台模型配置获取和同步功能 - 新增 getWanchuanModelConfig 函数,按模型编码获取平台模型配置 - 新增 syncWanchuanModelToSettings 函数,从万川平台拉取模型配置并写入后端 AI 设置 - 支持按用途分多个模型编码(generic/vision/voice)分别同步配置 - 配置失败时跳过对应字段,不影响其他模型同步 feat(settings): 重构AI模型配置界面支持多模块分组 - 将AI配置按话题分析、报告生成、视觉、语音四个模块分组展示 - 每个模块独立配置接口地址、密钥和模型名称 - 添加从万川平台获取配置的按钮和同步功能 - 优化配置状态指示和错误提示信息 refactor(config): 扩展AI配置支持独立的语音视觉报告网关 - 新增 voice_base_url/voice_api_key 配置项 - 新增 vision_base_url/vision_api_key 配置项 - 新增 summary_base_url/summary_api_key 配置项 - 留空时回退到 ai_base_url/ai_api_key 兼容单网关场景 refactor(http): 统一使用共享HTTP客户端减少连接开销 - 替换各处 httpx.AsyncClient 为 shared_client - 在 lifespan 中正确关闭共享客户端资源 - 优化 get_current_wxid 和 health 检查中的HTTP请求 refactor(ai): 按用途缓存AI客户端支持不同网关配置 - 重构 get_openai_client 支持按(base_url, api_key)缓存 - 新增 get_client_for 函数按用途获取对应客户端 - 支持语音、视觉、报告等不同用途使用独立网关和密钥 ```
This commit is contained in:
@@ -99,6 +99,112 @@ export async function uploadToKnowledgeBase(baseUrl, datasetId, file, secretLeve
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模型编码(code)获取平台模型配置
|
||||
* 接口:GET ${baseUrl}/api/system/model/getByCode/{code}
|
||||
* 返回结构:data.providerModels.{ modelName, encryptedConfig:"{apiKey,endpointUrl}" }
|
||||
* @param {string} baseUrl - 平台地址
|
||||
* @param {string} code - 模型编码,默认 generic(通用模型)
|
||||
* @returns {Promise<{modelName: string, apiKey: string, endpointUrl: string}>}
|
||||
*/
|
||||
export async function getWanchuanModelConfig(baseUrl, code = 'generic') {
|
||||
const resp = await fetch(`${baseUrl}/api/system/model/getByCode/${encodeURIComponent(code)}`, {
|
||||
method: 'GET',
|
||||
headers: authHeaders(baseUrl),
|
||||
})
|
||||
if (!resp.ok) throw new Error(`获取模型[${code}]失败 HTTP ${resp.status}`)
|
||||
const result = await resp.json()
|
||||
const pm = result?.data?.providerModels
|
||||
if (!pm) throw new Error(`平台未返回模型[${code}]配置${result?.msg ? `:${result.msg}` : ''}`)
|
||||
|
||||
// encryptedConfig 是 JSON 字符串:{ apiKey, endpointUrl, ... }
|
||||
let cfg = {}
|
||||
if (pm.encryptedConfig) {
|
||||
try { cfg = JSON.parse(pm.encryptedConfig) } catch { cfg = {} }
|
||||
}
|
||||
return {
|
||||
modelName: pm.modelName || '',
|
||||
apiKey: cfg.apiKey || '',
|
||||
endpointUrl: cfg.endpointUrl || '',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从万川平台拉取模型配置并写入后端 AI 设置(/api/settings)。
|
||||
*
|
||||
* 平台按用途分多个模型编码(code):
|
||||
* - generic:通用聊天模型 → 话题分析(ai_model) + 报告生成(summary_model),
|
||||
* 并提供共享的接口地址(ai_base_url) 与密钥(ai_api_key)
|
||||
* - vision :视觉模型 → vision_model
|
||||
* - voice :语音模型 → voice_model
|
||||
*
|
||||
* 仅写入拉取到的非空字段;vision/voice 拉取失败时跳过对应字段(不影响聊天模型同步),
|
||||
* 避免某个 code 在平台未配置时把整次同步打断或清空已有配置。
|
||||
*
|
||||
* 后端 voice/vision 各有独立的 base_url / api_key / model;为空时后端回退到
|
||||
* generic 的 ai_base_url / ai_api_key。这里按各自 code 分别拉取并写入对应字段。
|
||||
*
|
||||
* @param {string} [baseUrl] - 平台地址,缺省时从后端万川配置读取
|
||||
* @param {{chat?: string, vision?: string, voice?: string}} [codes] - 各用途模型编码
|
||||
* @returns {Promise<object>} 实际写入 /api/settings 的字段
|
||||
*/
|
||||
export async function syncWanchuanModelToSettings(baseUrl, codes = {}) {
|
||||
const { chat = 'generic', vision = 'vision', voice = 'voice' } = codes
|
||||
|
||||
let url = baseUrl
|
||||
if (!url) {
|
||||
const cfg = await getWanchuanConfig()
|
||||
url = cfg.platformUrl
|
||||
}
|
||||
if (!url) throw new Error('未配置平台地址')
|
||||
|
||||
// 通用聊天模型:必拉,提供共享地址/密钥与两个聊天模型。
|
||||
// 话题分析用 ai_*;报告生成单独存 summary_*(同 generic 网关,便于各组独立回显与覆盖)。
|
||||
const generic = await getWanchuanModelConfig(url, chat)
|
||||
/** @type {Record<string, string>} */
|
||||
const payload = {}
|
||||
if (generic.endpointUrl) {
|
||||
payload.ai_base_url = generic.endpointUrl
|
||||
payload.summary_base_url = generic.endpointUrl
|
||||
}
|
||||
if (generic.apiKey) {
|
||||
payload.ai_api_key = generic.apiKey
|
||||
payload.summary_api_key = generic.apiKey
|
||||
}
|
||||
if (generic.modelName) {
|
||||
payload.ai_model = generic.modelName
|
||||
payload.summary_model = generic.modelName
|
||||
}
|
||||
|
||||
// 视觉 / 语音模型:可选,各自按 code 拉取独立地址/密钥/模型名;失败则跳过
|
||||
await Promise.all([
|
||||
getWanchuanModelConfig(url, vision)
|
||||
.then(v => {
|
||||
if (v.endpointUrl) payload.vision_base_url = v.endpointUrl
|
||||
if (v.apiKey) payload.vision_api_key = v.apiKey
|
||||
if (v.modelName) payload.vision_model = v.modelName
|
||||
})
|
||||
.catch(() => {}),
|
||||
getWanchuanModelConfig(url, voice)
|
||||
.then(v => {
|
||||
if (v.endpointUrl) payload.voice_base_url = v.endpointUrl
|
||||
if (v.apiKey) payload.voice_api_key = v.apiKey
|
||||
if (v.modelName) payload.voice_model = v.modelName
|
||||
})
|
||||
.catch(() => {}),
|
||||
])
|
||||
|
||||
if (Object.keys(payload).length === 0) throw new Error('平台模型配置为空')
|
||||
|
||||
const resp = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
if (!resp.ok) throw new Error('保存 AI 设置失败')
|
||||
return payload
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存的 token
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Copy, Check, Save, Link2, Loader, AlertCircle, ChevronDown, ChevronRight, RefreshCw, BookOpen, ExternalLink, FileText } from 'lucide-react'
|
||||
import { wanchuanLogin, getWanchuanKnowledgeBases, uploadToKnowledgeBase, clearWanchuanToken, getWanchuanConfig, saveWanchuanConfig } from '../api/wanchuan'
|
||||
import { wanchuanLogin, getWanchuanKnowledgeBases, uploadToKnowledgeBase, clearWanchuanToken, getWanchuanConfig, saveWanchuanConfig, syncWanchuanModelToSettings } from '../api/wanchuan'
|
||||
|
||||
const CONFIG_ITEMS = [
|
||||
{
|
||||
@@ -27,13 +27,41 @@ const CONFIG_ITEMS = [
|
||||
},
|
||||
]
|
||||
|
||||
const AI_FIELDS = [
|
||||
{ key: 'ai_base_url', label: 'AI 接口地址', placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', desc: '兼容 OpenAI 格式的 API 地址' },
|
||||
{ key: 'ai_api_key', label: 'AI API Key', placeholder: 'sk-...', desc: '留空则 AI 功能不可用', type: 'password' },
|
||||
{ key: 'ai_model', label: '话题分析模型', placeholder: 'qwen-plus', desc: '用于消息分类的模型' },
|
||||
{ key: 'summary_model', label: '报告生成模型', placeholder: 'qwen-max', desc: '用于生成售后报告的模型' },
|
||||
{ key: 'vision_model', label: '视觉模型', placeholder: 'qwen-vl-plus', desc: '用于图片/视频描述' },
|
||||
{ key: 'voice_model', label: '语音模型', placeholder: 'paraformer-v2', desc: '用于语音转文字' },
|
||||
// 按模块分组:每个模块独立展示自己的接口地址 + 密钥 + 模型。
|
||||
// 话题分析用 ai_*(也是视觉/语音/报告的回退网关);报告生成用 summary_*。
|
||||
const AI_GROUPS = [
|
||||
{
|
||||
group: '话题分析模型',
|
||||
fields: [
|
||||
{ key: 'ai_base_url', label: '接口地址', placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', desc: '兼容 OpenAI 格式的 API 地址' },
|
||||
{ key: 'ai_api_key', label: '密钥', placeholder: 'sk-...', desc: '留空则 AI 功能不可用', type: 'password' },
|
||||
{ key: 'ai_model', label: '模型', placeholder: 'qwen-plus', desc: '用于消息分类的模型' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: '报告生成模型',
|
||||
fields: [
|
||||
{ key: 'summary_base_url', label: '接口地址', placeholder: '留空则用话题分析地址', desc: '独立网关;留空回退话题分析接口地址' },
|
||||
{ key: 'summary_api_key', label: '密钥', placeholder: '留空则用话题分析密钥', desc: '留空回退话题分析密钥', type: 'password' },
|
||||
{ key: 'summary_model', label: '模型', placeholder: 'qwen-max', desc: '用于生成售后报告的模型' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: '视觉模型(图片 / 视频描述)',
|
||||
fields: [
|
||||
{ key: 'vision_base_url', label: '接口地址', placeholder: '留空则用话题分析地址', desc: '独立网关;留空回退话题分析接口地址' },
|
||||
{ key: 'vision_api_key', label: '密钥', placeholder: '留空则用话题分析密钥', desc: '留空回退话题分析密钥', type: 'password' },
|
||||
{ key: 'vision_model', label: '模型', placeholder: 'qwen-vl-plus', desc: '用于图片/视频描述' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: '语音模型(语音转文字)',
|
||||
fields: [
|
||||
{ key: 'voice_base_url', label: '接口地址', placeholder: '留空则用话题分析地址', desc: '异步 ASR 网关,填到 .../api/v1;留空回退话题分析地址' },
|
||||
{ key: 'voice_api_key', label: '密钥', placeholder: '留空则用话题分析密钥', desc: '留空回退话题分析密钥', type: 'password' },
|
||||
{ key: 'voice_model', label: '模型', placeholder: 'paraformer-v2', desc: '用于语音转文字' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const TOPIC_PROMPT_PLACEHOLDER = '例如:本群主要是某类设备售后群,请优先按设备部件、故障现象、处理进度来拆分话题;不要按客户名或日期拆分。'
|
||||
@@ -57,17 +85,37 @@ function CopyButton({ text }) {
|
||||
)
|
||||
}
|
||||
|
||||
function AISettingsForm() {
|
||||
function AISettingsForm({ refreshKey = 0 }) {
|
||||
const [form, setForm] = useState({})
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [msg, setMsg] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = () =>
|
||||
fetch('/api/settings')
|
||||
.then(r => r.json())
|
||||
.then(data => setForm(data))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// refreshKey 由父级在万川平台登录/同步后递增,触发重新读取已写入的 AI 配置
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [refreshKey])
|
||||
|
||||
// 手动从万川平台拉取模型配置(base url / api key / 各模型),写入后端并回显
|
||||
const handleSyncFromPlatform = async () => {
|
||||
setSyncing(true)
|
||||
setMsg('')
|
||||
try {
|
||||
await syncWanchuanModelToSettings()
|
||||
await loadSettings()
|
||||
setMsg('已从平台获取')
|
||||
setTimeout(() => setMsg(''), 2000)
|
||||
} catch (e) {
|
||||
setMsg(e.message || '获取失败')
|
||||
}
|
||||
setSyncing(false)
|
||||
}
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setForm(prev => ({ ...prev, [key]: value }))
|
||||
@@ -102,7 +150,7 @@ function AISettingsForm() {
|
||||
AI 模型配置
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 12 }}>
|
||||
首次使用请填入你的 API Key 和接口地址,保存后立即生效,无需重启服务。
|
||||
已对接万川平台时,登录后会自动从平台获取模型配置;也可点「从平台获取」手动同步。字段支持手动修改并保存。
|
||||
</div>
|
||||
|
||||
{/* 未配置 API Key 时显示橙色警告横条 */}
|
||||
@@ -117,50 +165,55 @@ function AISettingsForm() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
|
||||
{AI_FIELDS.map((field, i) => (
|
||||
<div
|
||||
key={field.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: i < AI_FIELDS.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: '0 0 130px' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>{field.label}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{field.desc}</div>
|
||||
</div>
|
||||
<input
|
||||
type={field.type || 'text'}
|
||||
value={form[field.key] || ''}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(e) => handleChange(field.key, e.target.value)}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
padding: '7px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
background: 'var(--surface-2)',
|
||||
color: 'var(--text)',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
{/* 配置状态指示点:绿色=已配置,红色=未配置 */}
|
||||
<div
|
||||
title={form[field.key] ? '已配置' : '未配置'}
|
||||
style={{
|
||||
width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
|
||||
background: form[field.key] ? 'var(--success, #10b981)' : '#ef4444',
|
||||
boxShadow: form[field.key] ? '0 0 4px rgba(16,185,129,0.5)' : '0 0 4px rgba(239,68,68,0.5)',
|
||||
}}
|
||||
/>
|
||||
{AI_GROUPS.map((g) => (
|
||||
<div key={g.group} style={{ marginBottom: 14 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text)', marginBottom: 6 }}>{g.group}</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
|
||||
{g.fields.map((field, i) => (
|
||||
<div
|
||||
key={field.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 16px',
|
||||
borderBottom: i < g.fields.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: '0 0 130px' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>{field.label}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{field.desc}</div>
|
||||
</div>
|
||||
<input
|
||||
type={field.type || 'text'}
|
||||
value={form[field.key] || ''}
|
||||
placeholder={field.placeholder}
|
||||
onChange={(e) => handleChange(field.key, e.target.value)}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
padding: '7px 12px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
background: 'var(--surface-2)',
|
||||
color: 'var(--text)',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
{/* 配置状态指示点:绿色=已配置,红色=未配置 */}
|
||||
<div
|
||||
title={form[field.key] ? '已配置' : '未配置'}
|
||||
style={{
|
||||
width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
|
||||
background: form[field.key] ? 'var(--success, #10b981)' : '#ef4444',
|
||||
boxShadow: form[field.key] ? '0 0 4px rgba(16,185,129,0.5)' : '0 0 4px rgba(239,68,68,0.5)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>AI 话题分析提示词</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 8 }}>
|
||||
@@ -207,7 +260,27 @@ function AISettingsForm() {
|
||||
<Save size={14} />
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</button>
|
||||
{msg && <span style={{ fontSize: 12, color: msg === '已保存' ? 'var(--success, #10b981)' : '#ef4444' }}>{msg}</span>}
|
||||
<button
|
||||
onClick={handleSyncFromPlatform}
|
||||
disabled={syncing}
|
||||
title="从已对接的万川平台拉取模型配置并填入"
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 16px',
|
||||
background: 'transparent',
|
||||
color: 'var(--accent, #6366f1)',
|
||||
border: '1px solid var(--accent, #6366f1)',
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
cursor: syncing ? 'not-allowed' : 'pointer',
|
||||
opacity: syncing ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{syncing ? <Loader size={14} style={{ animation: 'spin 1s linear infinite' }} /> : <RefreshCw size={14} />}
|
||||
{syncing ? '获取中...' : '从平台获取'}
|
||||
</button>
|
||||
{msg && <span style={{ fontSize: 12, color: (msg === '已保存' || msg === '已从平台获取') ? 'var(--success, #10b981)' : '#ef4444' }}>{msg}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -217,7 +290,7 @@ function AISettingsForm() {
|
||||
* 万川 AI 平台对接配置
|
||||
* 支持输入平台地址 + 账号密码,测试连接后可展示平台模块内容(预留)
|
||||
*/
|
||||
function WanchuanPlatformForm() {
|
||||
function WanchuanPlatformForm({ onModelSynced }) {
|
||||
const [form, setForm] = useState({ platformUrl: '', username: '', password: '' })
|
||||
const [connecting, setConnecting] = useState(false)
|
||||
const [connected, setConnected] = useState(false)
|
||||
@@ -232,6 +305,9 @@ function WanchuanPlatformForm() {
|
||||
const [kbLoading, setKbLoading] = useState(false)
|
||||
const [kbError, setKbError] = useState('')
|
||||
const [kbExpanded, setKbExpanded] = useState(false)
|
||||
// 模型同步状态:成功提示已回填,失败展示原因(便于排查路径/token/平台未配模型)
|
||||
const [modelSyncMsg, setModelSyncMsg] = useState('')
|
||||
const [modelSyncError, setModelSyncError] = useState(false)
|
||||
|
||||
// 后端为唯一数据源:打开设置页时拉取已保存配置并填入表单,
|
||||
// 然后按已保存凭证自动恢复会话(拉取知识库列表)。
|
||||
@@ -256,9 +332,14 @@ function WanchuanPlatformForm() {
|
||||
fetchKnowledgeBases(saved.platformUrl, { allowRelogin: true, savedKbId: saved.selectedKbId, credsForRelogin: saved })
|
||||
} else if (saved.username && saved.password) {
|
||||
autoLogin(saved).then(ok => {
|
||||
if (ok) fetchKnowledgeBases(saved.platformUrl, { savedKbId: saved.selectedKbId })
|
||||
if (ok) {
|
||||
fetchKnowledgeBases(saved.platformUrl, { savedKbId: saved.selectedKbId })
|
||||
}
|
||||
})
|
||||
}
|
||||
// 注意:挂载时不自动 syncModel。自动同步会用平台配置覆盖用户在「AI 模型配置」里
|
||||
// 手动改过并保存的值(曾导致"改了保存后重进页面又被重置")。模型同步只在用户
|
||||
// 显式操作时进行:点「测试连接」或「从平台获取」。
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
@@ -364,6 +445,23 @@ function WanchuanPlatformForm() {
|
||||
return uploadToKnowledgeBase(form.platformUrl, targetDatasetId, file)
|
||||
}
|
||||
|
||||
// 登录成功后从平台拉取模型配置并写入后端 AI 设置;成功后通知父级刷新 AI 表单。
|
||||
// 同步结果用 modelSyncMsg/Error 展示,便于排查(路径错=404、token 失效=401、平台未配模型等);
|
||||
// 不阻断主流程(知识库等仍可用)。
|
||||
const syncModel = async (baseUrl) => {
|
||||
setModelSyncMsg('')
|
||||
setModelSyncError(false)
|
||||
try {
|
||||
const written = await syncWanchuanModelToSettings(baseUrl)
|
||||
onModelSynced?.()
|
||||
const n = Object.keys(written || {}).length
|
||||
setModelSyncMsg(`已从平台同步模型配置(${n} 项),已回填到 AI 模型配置`)
|
||||
} catch (e) {
|
||||
setModelSyncError(true)
|
||||
setModelSyncMsg(`模型配置同步失败:${e.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!form.platformUrl) {
|
||||
setConnectError('请输入平台地址')
|
||||
@@ -389,8 +487,9 @@ function WanchuanPlatformForm() {
|
||||
setConnected(true)
|
||||
handleSave()
|
||||
|
||||
// 登录成功后自动拉取岗位知识库列表
|
||||
// 登录成功后自动拉取岗位知识库列表 + 同步平台模型配置
|
||||
fetchKnowledgeBases(form.platformUrl)
|
||||
syncModel(form.platformUrl)
|
||||
} catch (e) {
|
||||
setConnectError(e.message || '登录失败')
|
||||
} finally {
|
||||
@@ -564,6 +663,11 @@ function WanchuanPlatformForm() {
|
||||
<AlertCircle size={12} /> {connectError}
|
||||
</span>
|
||||
)}
|
||||
{modelSyncMsg && (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: modelSyncError ? 'var(--danger, #dc2626)' : 'var(--success, #10b981)' }}>
|
||||
{modelSyncError ? <AlertCircle size={12} /> : <Check size={12} />} {modelSyncMsg}
|
||||
</span>
|
||||
)}
|
||||
{kbExpanded && !kbLoading && knowledgeBases.length === 0 && (
|
||||
<div style={{ padding: '20px 16px', textAlign: 'center', fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
暂无岗位知识库,请先点击「测试连接」登录平台
|
||||
@@ -714,6 +818,8 @@ function WanchuanPlatformForm() {
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
// 万川平台同步模型配置后递增此值,触发 AISettingsForm 重新读取后端 AI 配置
|
||||
const [aiRefreshKey, setAiRefreshKey] = useState(0)
|
||||
return (
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '28px 36px' }}>
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
@@ -722,11 +828,11 @@ export default function SettingsPage() {
|
||||
系统各服务地址及 AI 配置管理。
|
||||
</div>
|
||||
|
||||
{/* AI 配置表单 */}
|
||||
<AISettingsForm />
|
||||
{/* 万川 AI 平台对接:放最上面,登录后自动获取下方的 AI 模型配置 */}
|
||||
<WanchuanPlatformForm onModelSynced={() => setAiRefreshKey(k => k + 1)} />
|
||||
|
||||
{/* 万川 AI 平台对接 */}
|
||||
<WanchuanPlatformForm />
|
||||
{/* AI 配置表单:由上方万川平台登录后自动填充,也可手动修改 */}
|
||||
<AISettingsForm refreshKey={aiRefreshKey} />
|
||||
|
||||
{CONFIG_ITEMS.map((group) => (
|
||||
<div key={group.group} style={{ marginBottom: 28 }}>
|
||||
|
||||
Reference in New Issue
Block a user