Files
get_wechat/chatlab-web/frontend/src/pages/SettingsPage.jsx
yuanzhipeng b137fd7915 feat: 添加万川AI平台对接功能
- 新增万川AI平台API接口层,支持登录、获取知识库列表、上传文件等操作
- 实现万川平台配置界面,支持平台地址、账号密码配置和知识库绑定
- 在知识库页面添加上传功能,支持单个或批量上传售后报告到万川知识库
- 提供检查配置的工具脚本便于调试
2026-06-11 16:14:38 +08:00

722 lines
31 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { Copy, Check, Save, Link2, Loader, AlertCircle, ChevronDown, ChevronRight, RefreshCw, BookOpen, ExternalLink, FileText } from 'lucide-react'
import { wanchuanLogin, getWanchuanKnowledgeBases, uploadToKnowledgeBase, clearWanchuanToken } from '../api/wanchuan'
const CONFIG_ITEMS = [
{
group: 'chatlog 底层服务',
items: [
{ label: 'chatlog 地址', value: 'http://127.0.0.1:5030', desc: 'Go 后端,负责读取微信数据库' },
{ label: 'API 前缀', value: '/api/v1', desc: '所有 chatlog 接口均在此前缀下' },
],
},
{
group: 'chatlog_fastAPI 业务层',
items: [
{ label: 'FastAPI 地址', value: 'http://127.0.0.1:8000', desc: 'Python 后端,负责 AI 分析和知识库' },
{ label: '搜索接口', value: '/api/search', desc: '聊天记录搜索' },
{ label: '话题接口', value: '/api/topics', desc: 'AI 话题分析管理' },
{ label: '报告库接口', value: '/api/knowledge', desc: '售后报告管理' },
],
},
{
group: '桌面应用服务',
items: [
{ label: '本地应用入口', value: '/', desc: '桌面应用内置界面,由本地业务服务托管' },
],
},
]
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: '用于语音转文字' },
]
const TOPIC_PROMPT_PLACEHOLDER = '例如:本群主要是某类设备售后群,请优先按设备部件、故障现象、处理进度来拆分话题;不要按客户名或日期拆分。'
function CopyButton({ text }) {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
return (
<button
onClick={handleCopy}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px' }}
title="复制"
>
{copied ? <Check size={13} color="var(--success, #10b981)" /> : <Copy size={13} />}
</button>
)
}
function AISettingsForm() {
const [form, setForm] = useState({})
const [saving, setSaving] = useState(false)
const [msg, setMsg] = useState('')
useEffect(() => {
fetch('/api/settings')
.then(r => r.json())
.then(data => setForm(data))
.catch(() => {})
}, [])
const handleChange = (key, value) => {
setForm(prev => ({ ...prev, [key]: value }))
}
const handleSave = async () => {
setSaving(true)
setMsg('')
try {
const res = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
})
if (res.ok) {
setMsg('已保存')
setTimeout(() => setMsg(''), 2000)
const updated = await fetch('/api/settings').then(r => r.json())
setForm(updated)
} else {
setMsg('保存失败')
}
} catch {
setMsg('保存失败')
}
setSaving(false)
}
return (
<div style={{ marginBottom: 28 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 10 }}>
AI 模型配置
</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 12 }}>
首次使用请填入你的 API Key 和接口地址保存后立即生效无需重启服务
</div>
{/* 未配置 API Key 时显示橙色警告横条 */}
{!form.ai_api_key && (
<div style={{
marginBottom: 12, padding: '8px 12px',
background: 'rgba(245,158,11,0.1)', border: '1px solid rgba(245,158,11,0.3)',
borderRadius: 8, fontSize: 12, color: '#d97706',
display: 'flex', alignItems: 'center', gap: 6,
}}>
未配置 AI API Key所有 AI 功能不可用请填入您自己的 API Key 并保存
</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)',
}}
/>
</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 }}>
作为全局默认分析口径单个群聊可在 AI 话题分析里单独覆盖
</div>
<textarea
value={form.topic_analysis_prompt || ''}
placeholder={TOPIC_PROMPT_PLACEHOLDER}
onChange={(e) => handleChange('topic_analysis_prompt', e.target.value)}
style={{
width: '100%',
minHeight: 120,
boxSizing: 'border-box',
fontSize: 13,
lineHeight: 1.6,
padding: '10px 12px',
border: '1px solid var(--border)',
borderRadius: 8,
background: 'var(--surface-2)',
color: 'var(--text)',
outline: 'none',
resize: 'vertical',
fontFamily: 'inherit',
}}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 14 }}>
<button
onClick={handleSave}
disabled={saving}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 20px',
background: 'var(--accent, #6366f1)',
color: '#fff',
border: 'none',
borderRadius: 8,
fontSize: 13,
fontWeight: 500,
cursor: saving ? 'not-allowed' : 'pointer',
opacity: saving ? 0.6 : 1,
}}
>
<Save size={14} />
{saving ? '保存中...' : '保存配置'}
</button>
{msg && <span style={{ fontSize: 12, color: msg === '已保存' ? 'var(--success, #10b981)' : '#ef4444' }}>{msg}</span>}
</div>
</div>
)
}
/**
* 万川 AI 平台对接配置
* 支持输入平台地址 + 账号密码,测试连接后可展示平台模块内容(预留)
*/
function WanchuanPlatformForm() {
const STORAGE_KEY = 'chatlab_wanchuan_config'
const [form, setForm] = useState({ platformUrl: '', username: '', password: '' })
const [connecting, setConnecting] = useState(false)
const [connected, setConnected] = useState(false)
const [connectError, setConnectError] = useState('')
const [modules, setModules] = useState([])
const [modulesExpanded, setModulesExpanded] = useState(false)
const [knowledgeBases, setKnowledgeBases] = useState([])
const [selectedKbId, setSelectedKbId] = useState('')
// 本地保存的已选知识库信息:列表尚未拉取时用于即时回显
const [savedKbInfo, setSavedKbInfo] = useState(null)
const [kbLoading, setKbLoading] = useState(false)
const [kbError, setKbError] = useState('')
const [kbExpanded, setKbExpanded] = useState(false)
// 从 localStorage 加载已保存配置
useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) setForm(JSON.parse(raw))
} catch {}
}, [])
// 从 localStorage 加载已选知识库;若有缓存 token + 平台地址则自动拉取列表回显
useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return
const saved = JSON.parse(raw)
if (saved.selectedKbId) setSelectedKbId(saved.selectedKbId)
if (saved.selectedKbInfo) setSavedKbInfo(saved.selectedKbInfo)
// 已登录过(有缓存 token则自动拉取知识库列表无需手动点连接
const token = localStorage.getItem('chatlab_wanchuan_token')
if (token && saved.platformUrl) {
fetchKnowledgeBases(saved.platformUrl)
}
} catch {}
}, [])
const handleChange = (key, value) => {
setForm(prev => ({ ...prev, [key]: value }))
if (connected) setConnected(false)
}
// 知识库的唯一标识:上传接口需要的是 datasetId没有则回退到 id。
// 全程以此作为 selectedKbId保证选择、回显、持久化、上传四处一致。
const kbKey = (kb) => String(kb?.datasetId ?? kb?.id ?? '')
// 持久化整个知识库配置。
// kbIdOverride / listOverride 用于在 React 状态尚未刷新时setState 异步)
// 传入即时值,避免读到旧的 selectedKbId 或旧的知识库列表。
const handleSave = (kbIdOverride, listOverride) => {
try {
const effectiveKbId = kbIdOverride !== undefined ? kbIdOverride : selectedKbId
const kbs = listOverride || knowledgeBases
const selectedKb = kbs.find(k => kbKey(k) === effectiveKbId) || null
const data = {
...form,
selectedKbId: effectiveKbId,
selectedKbInfo: selectedKb ? {
selectedKbId: kbKey(selectedKb),
kbName: selectedKb.relationName || selectedKb.id,
kbDatasetId: selectedKb.datasetId || selectedKb.id,
kbType: selectedKb.kbType,
} : null,
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
} catch {}
}
const handleSelectKb = (key) => {
const kb = knowledgeBases.find(k => kbKey(k) === key)
if (!kb) return
const nextKey = kbKey(kb)
setSelectedKbId(nextKey)
// 同步兜底回显信息
setSavedKbInfo({
selectedKbId: nextKey,
kbName: kb.relationName || kb.id,
kbDatasetId: kb.datasetId || kb.id,
kbType: kb.kbType,
})
// 立即用新值持久化,不依赖尚未刷新的 selectedKbId 状态
handleSave(nextKey)
}
const selectedKb = knowledgeBases.find(kb => kbKey(kb) === selectedKbId)
// 回显用的已选信息:优先用列表中的完整对象,否则用本地保存的兜底信息
const selectedKbName = selectedKb?.relationName || (selectedKbId ? savedKbInfo?.kbName : null)
// 上传文件到知识库(预留,供后续调用)
const handleUploadToKB = async (datasetId, file) => {
if (!form.platformUrl || !file) return
// 优先使用传入的 datasetId否则使用已选知识库的 datasetId
const targetDatasetId = datasetId || selectedKb?.datasetId
if (!targetDatasetId) return
return uploadToKnowledgeBase(form.platformUrl, targetDatasetId, file)
}
// 获取当前绑定的知识库信息(供外部调用)
const getBoundKnowledgeBase = () => {
return selectedKb || null
}
const handleTestConnection = async () => {
if (!form.platformUrl) {
setConnectError('请输入平台地址')
return
}
if (!form.username || !form.password) {
setConnectError('请输入账号和密码')
return
}
setConnecting(true)
setConnectError('')
setModules([])
setConnected(false)
setKnowledgeBases([])
// 登录成功后保留已选知识库 ID如果新列表中包含该 ID
try {
const { token, data } = await wanchuanLogin(form.platformUrl, form.username, form.password)
if (!token) throw new Error('登录成功但未获取到 token')
setModules(data.modules || [])
setConnected(true)
handleSave()
// 登录成功后自动拉取岗位知识库列表
fetchKnowledgeBases(form.platformUrl)
} catch (e) {
setConnectError(e.message || '登录失败')
} finally {
setConnecting(false)
}
}
const fetchKnowledgeBases = async (baseUrl) => {
setKbLoading(true)
setKbError('')
try {
const { list } = await getWanchuanKnowledgeBases(baseUrl, { relationType: 'position', page: 1, pageSize: 100 })
setKnowledgeBases(list)
// 登录后恢复之前保存的已选知识库(按统一的 kbKey 匹配)
const keys = new Set(list.map(kb => kbKey(kb)))
if (selectedKbId && keys.has(selectedKbId)) {
// 已选项仍在列表中:用完整信息重新持久化(登录时曾因列表为空被写成 null
handleSave(selectedKbId, list)
} else if (selectedKbId) {
// 兼容旧数据:之前可能保存的是 kb.id尝试映射到 datasetId
const matchById = list.find(kb => String(kb.id) === selectedKbId)
if (matchById) {
const newKey = kbKey(matchById)
setSelectedKbId(newKey)
handleSave(newKey, list)
} else {
setSelectedKbId('')
handleSave('', list)
}
}
setKbExpanded(true)
} catch (e) {
setKbError(e.message || '加载知识库列表失败')
} finally {
setKbLoading(false)
}
}
const handleRefreshKB = () => {
if (form.platformUrl) fetchKnowledgeBases(form.platformUrl)
}
const kbTypeLabel = (type) => ({
self_built: '自建',
external: '外部',
shared: '共享',
}[type] || type)
const statusBadge = (status) => {
if (status === 0) return { label: '正常', color: 'var(--success, #10b981)' }
if (status === 1) return { label: '停用', color: 'var(--warning, #d97706)' }
return { label: String(status), color: 'var(--text-muted)' }
}
const isConfigured = form.platformUrl && form.username && form.password
return (
<div style={{ marginBottom: 28 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 10 }}>
万川 AI 平台对接
</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 12 }}>
配置第三方平台地址和登录凭证用于推送数据测试连接后可查看平台模块
</div>
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
{/* 平台地址 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', borderBottom: '1px solid var(--border)', gap: 12 }}>
<div style={{ flex: '0 0 130px' }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>平台地址</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>万川 AI 平台 Base URL</div>
</div>
<input
value={form.platformUrl || ''}
placeholder="https://wanchuan.example.com"
onChange={(e) => handleChange('platformUrl', 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.platformUrl ? '已配置' : '未配置'} style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0, background: form.platformUrl ? 'var(--success, #10b981)' : '#ef4444', boxShadow: form.platformUrl ? '0 0 4px rgba(16,185,129,0.5)' : '0 0 4px rgba(239,68,68,0.5)' }} />
</div>
{/* 账号 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', borderBottom: '1px solid var(--border)', gap: 12 }}>
<div style={{ flex: '0 0 130px' }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>账号</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>平台登录账号</div>
</div>
<input
value={form.username || ''}
placeholder="请输入账号"
onChange={(e) => handleChange('username', 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.username ? '已配置' : '未配置'} style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0, background: form.username ? 'var(--success, #10b981)' : '#ef4444', boxShadow: form.username ? '0 0 4px rgba(16,185,129,0.5)' : '0 0 4px rgba(239,68,68,0.5)' }} />
</div>
{/* 密码 */}
<div style={{ display: 'flex', alignItems: 'center', padding: '12px 16px', gap: 12 }}>
<div style={{ flex: '0 0 130px' }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>密码</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>平台登录密码</div>
</div>
<input
type="password"
value={form.password || ''}
placeholder="请输入密码"
onChange={(e) => handleChange('password', 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.password ? '已配置' : '未配置'} style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0, background: form.password ? 'var(--success, #10b981)' : '#ef4444', boxShadow: form.password ? '0 0 4px rgba(16,185,129,0.5)' : '0 0 4px rgba(239,68,68,0.5)' }} />
</div>
</div>
{/* 操作按钮 + 状态 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 14 }}>
<button
onClick={handleTestConnection}
disabled={connecting || !isConfigured}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 20px',
background: connected ? 'var(--success, #10b981)' : 'var(--accent, #6366f1)',
color: '#fff',
border: 'none',
borderRadius: 8,
fontSize: 13,
fontWeight: 500,
cursor: (connecting || !isConfigured) ? 'not-allowed' : 'pointer',
opacity: (connecting || !isConfigured) ? 0.6 : 1,
}}
>
{connecting ? <Loader size={14} style={{ animation: 'spin 1s linear infinite' }} /> : connected ? <Check size={14} /> : <Link2 size={14} />}
{connecting ? '连接中...' : connected ? '已连接' : '测试连接'}
</button>
{connectError && (
<span style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, color: 'var(--danger, #dc2626)' }}>
<AlertCircle size={12} /> {connectError}
</span>
)}
{kbExpanded && !kbLoading && knowledgeBases.length === 0 && (
<div style={{ padding: '20px 16px', textAlign: 'center', fontSize: 12, color: 'var(--text-muted)' }}>
暂无岗位知识库请先点击测试连接登录平台
</div>
)}
</div>
{/* 平台模块列表(预留) */}
{modules.length > 0 && (
<div style={{ marginTop: 16, border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
<div
onClick={() => setModulesExpanded(!modulesExpanded)}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 16px',
background: 'var(--surface-2)',
cursor: 'pointer',
fontSize: 13,
fontWeight: 500,
}}
>
{modulesExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
平台模块{modules.length}
</div>
{modulesExpanded && (
<div style={{ padding: '8px 16px 12px' }}>
{modules.map((mod, i) => (
<div key={mod.id || mod.name || i} style={{ padding: '6px 0', fontSize: 12.5, color: 'var(--text-primary)', borderBottom: i < modules.length - 1 ? '1px solid var(--border)' : 'none' }}>
{mod.name || mod.label || mod.id || '未命名模块'}
{mod.desc && <span style={{ marginLeft: 8, color: 'var(--text-muted)' }}>- {mod.desc}</span>}
</div>
))}
</div>
)}
</div>
)}
{/* 岗位知识库列表 - 始终显示 */}
<div style={{ marginTop: 16, border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
<div
onClick={() => setKbExpanded(!kbExpanded)}
style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '10px 16px',
background: 'var(--surface-2)',
cursor: 'pointer',
fontSize: 13,
fontWeight: 500,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<BookOpen size={14} color="var(--accent-light)" />
岗位知识库列表
{selectedKbName && (
<span style={{ fontSize: 11, color: 'var(--success)', fontWeight: 500 }}>· 已选: {selectedKbName}</span>
)}
{!selectedKbName && knowledgeBases.length > 0 && (
<span style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 400 }}>{knowledgeBases.length}</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{kbLoading && <Loader size={13} style={{ animation: 'spin 1s linear infinite' }} />}
<button
onClick={(e) => { e.stopPropagation(); handleRefreshKB() }}
disabled={kbLoading}
style={{ background: 'none', border: 'none', cursor: kbLoading ? 'not-allowed' : 'pointer', color: 'var(--text-muted)', padding: 2, display: 'flex' }}
title="刷新"
>
<RefreshCw size={13} />
</button>
{kbExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</div>
</div>
{kbExpanded && knowledgeBases.length > 0 && (
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
{knowledgeBases.map((kb, i) => {
const badge = statusBadge(kb.status)
const docCount = kb.content?.document_count ?? 0
const isSelected = kbKey(kb) === selectedKbId
return (
<div
key={kbKey(kb) || i}
onClick={() => handleSelectKb(kbKey(kb))}
style={{
padding: '12px 16px',
borderBottom: i < knowledgeBases.length - 1 ? '1px solid var(--border)' : 'none',
display: 'flex',
alignItems: 'flex-start',
gap: 10,
background: isSelected ? 'var(--accent-dim)' : 'transparent',
cursor: 'pointer',
}}
>
{/* 单选指示器 */}
<div style={{
width: 16, height: 16, borderRadius: '50%', flexShrink: 0, marginTop: 2,
border: `2px solid ${isSelected ? 'var(--accent)' : 'var(--border)'}`,
background: isSelected ? 'var(--accent)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all var(--transition)',
}}>
{isSelected && <Check size={10} color="#fff" />}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<FileText size={13} color="var(--accent-light)" />
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)' }}>
{kb.relationName || `知识库 #${kb.id}`}
</span>
{isSelected && <span style={{ fontSize: 10, color: 'var(--accent)', background: 'var(--accent-dim)', padding: '1px 6px', borderRadius: 8, fontWeight: 600 }}>已选</span>}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, fontSize: 11, color: 'var(--text-muted)' }}>
<span>ID: {kb.id}</span>
<span>类型: {kbTypeLabel(kb.kbType)}</span>
<span>文档数: {docCount}</span>
<span style={{ color: badge.color }}> {badge.label}</span>
{kb.datasetId && <span>Dataset: {kb.datasetId}</span>}
</div>
<div style={{ marginTop: 3, fontSize: 11, color: 'var(--text-muted)' }}>
创建人: {kb.createBy} · {kb.createTime}
</div>
</div>
</div>
)
})}
</div>
)}
</div>
{/* 已选知识库摘要 */}
{selectedKb ? (
<div style={{ marginTop: 10, padding: '8px 14px', fontSize: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>当前推送目标知识库</span>
<span style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{selectedKb.relationName}</span>
<span style={{ fontSize: 11 }}>(ID: {selectedKb.id}, Dataset: {selectedKb.datasetId}, {kbTypeLabel(selectedKb.kbType)}, {selectedKb.content?.document_count ?? 0} 文档)</span>
</div>
) : selectedKbName ? (
<div style={{ marginTop: 10, padding: '8px 14px', fontSize: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>当前推送目标知识库</span>
<span style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{selectedKbName}</span>
{savedKbInfo?.kbDatasetId && <span style={{ fontSize: 11 }}>(Dataset: {savedKbInfo.kbDatasetId}{savedKbInfo.kbType ? `, ${kbTypeLabel(savedKbInfo.kbType)}` : ''})</span>}
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>· 连接后显示完整信息</span>
</div>
) : null}
</div>
)
}
// 导出供外部调用获取已绑定知识库信息
export function getBoundKnowledgeBase() {
try {
const STORAGE_KEY = 'chatlab_wanchuan_config'
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return null
const saved = JSON.parse(raw)
// 返回完整的知识库信息,包括保存的详细信息
if (!saved.selectedKbId) return null
return {
id: saved.selectedKbInfo?.kbDatasetId || saved.selectedKbId,
platformUrl: saved.platformUrl,
kbName: saved.selectedKbInfo?.kbName || saved.selectedKbId,
kbDatasetId: saved.selectedKbInfo?.kbDatasetId || saved.selectedKbId,
kbType: saved.selectedKbInfo?.kbType,
}
} catch {
return null
}
}
export default function SettingsPage() {
return (
<div style={{ flex: 1, overflowY: 'auto', padding: '28px 36px' }}>
<div style={{ maxWidth: 720 }}>
<div style={{ fontWeight: 700, fontSize: 18, marginBottom: 6 }}>设置</div>
<div style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 28 }}>
系统各服务地址及 AI 配置管理
</div>
{/* AI 配置表单 */}
<AISettingsForm />
{/* 万川 AI 平台对接 */}
<WanchuanPlatformForm />
{CONFIG_ITEMS.map((group) => (
<div key={group.group} style={{ marginBottom: 28 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 10 }}>
{group.group}
</div>
<div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
{group.items.map((item, i) => (
<div
key={item.label}
style={{
display: 'flex',
alignItems: 'center',
padding: '12px 16px',
borderBottom: i < group.items.length - 1 ? '1px solid var(--border)' : 'none',
gap: 12,
}}
>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{item.label}</div>
{item.desc && (
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{item.desc}</div>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<code style={{
fontSize: 12,
padding: '3px 10px',
background: 'var(--surface-2)',
border: '1px solid var(--border)',
borderRadius: 6,
color: 'var(--accent-light, #a5b4fc)',
}}>
{item.value}
</code>
<CopyButton text={item.value} />
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}