T4: AnySearch 改为全局固定配置 + 修复外部服务弹窗 UI
This commit is contained in:
1 parent
3c9d9518b1
commit
97125d7b65
6 files changed
+634
-441
No files matched your search
@@ -959,6 +959,10 @@ export const adminAPI = {
|
||||
|
||||
checkAdminKnowledgeExtStatus: (id: string) => get<any>(`/admin/knowledge/external/services/${id}/status`),
|
||||
|
||||
getAdminAnySearchConfig: () => get<any>('/admin/knowledge/anysearch/config'),
|
||||
|
||||
saveAdminAnySearchConfig: (data: { api_key: string; is_enabled: boolean }) => post<any>('/admin/knowledge/anysearch/config', data),
|
||||
|
||||
syncAdminKnowledge: (kbId: string, data: any) => post<any>(`/admin/knowledge/external/sync/${kbId}`, data),
|
||||
|
||||
batchDeleteAdminKnowledge: (ids: string[]) => post<any>('/admin/knowledge/batch/delete', ids),
|
||||
|
||||
@@ -71,6 +71,11 @@ const Knowledge: React.FC = () => {
|
||||
const [ingesting, setIngesting] = useState(false);
|
||||
const [ingestResult, setIngestResult] = useState<any>(null);
|
||||
|
||||
// AnySearch global config
|
||||
const [anysearchForm, setAnysearchForm] = useState<{ api_key: string; is_enabled: boolean }>({ api_key: '', is_enabled: false });
|
||||
const [anysearchLoading, setAnysearchLoading] = useState(false);
|
||||
const [anysearchMsg, setAnysearchMsg] = useState('');
|
||||
|
||||
// KB Modal
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingKB, setEditingKB] = useState<KnowledgeBaseItem | null>(null);
|
||||
@@ -123,6 +128,33 @@ const Knowledge: React.FC = () => {
|
||||
try { const res = await adminAPI.getAdminKnowledgeExtServices(); if (res.code === 0) setExtServices(res.data.items || []); } catch (e) {}
|
||||
};
|
||||
|
||||
const fetchAnySearchConfig = async () => {
|
||||
try {
|
||||
const res = await adminAPI.getAdminAnySearchConfig();
|
||||
if (res.code === 0) {
|
||||
const d = res.data || {};
|
||||
setAnysearchForm({ api_key: d.api_key || '', is_enabled: !!d.is_enabled });
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const handleSaveAnySearch = async () => {
|
||||
setAnysearchLoading(true); setAnysearchMsg('');
|
||||
try {
|
||||
const res = await adminAPI.saveAdminAnySearchConfig(anysearchForm);
|
||||
if (res.code === 0) {
|
||||
const conn = res.data?.connection || {};
|
||||
setAnysearchMsg(conn.connected ? '✅ 已保存并连接成功' : `⚠️ 已保存但连接失败:${conn.detail || '未知错误'}`);
|
||||
} else {
|
||||
setAnysearchMsg(`❌ 保存失败:${res.message || '未知错误'}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setAnysearchMsg(`❌ 保存失败:${e?.message || '网络错误'}`);
|
||||
} finally {
|
||||
setAnysearchLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateKB = async () => {
|
||||
try {
|
||||
const res = await adminAPI.createAdminKnowledge(kbForm);
|
||||
@@ -334,7 +366,7 @@ const Knowledge: React.FC = () => {
|
||||
</select>
|
||||
<button onClick={() => { setEditingKB(null); setKBForm({ name: '', description: '', is_official: false }); setShowModal(true); }}
|
||||
style={btnStyle('#6C63FF')}>+ 创建知识库</button>
|
||||
<button onClick={() => setShowExtModal(true)} style={btnStyle('#8B5CF6')}>外部服务配置</button>
|
||||
<button onClick={() => { setShowExtModal(true); fetchAnySearchConfig(); }} style={btnStyle('#8B5CF6')}>外部服务配置</button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
@@ -523,54 +555,62 @@ const Knowledge: React.FC = () => {
|
||||
{/* External Service Modal */}
|
||||
{showExtModal && (
|
||||
<div style={overlayStyle} onClick={() => setShowExtModal(false)}>
|
||||
<div style={{ ...modalStyle, maxWidth: 650 }} onClick={e => e.stopPropagation()}>
|
||||
<h3>外部知识库服务配置</h3>
|
||||
{extServices.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<table className="admin-table" style={{ marginBottom: 0 }}>
|
||||
<thead><tr><th>名称</th><th>类型</th><th>URL</th><th>状态</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
{extServices.map(svc => (
|
||||
<tr key={svc.id}>
|
||||
<td>{svc.name}</td>
|
||||
<td><span style={{ fontSize: 12, padding: '2px 6px', background: '#F3F4FF', color: '#8B5CF6', borderRadius: 3 }}>Dify</span></td>
|
||||
<td style={{ fontSize: 11, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis' }}>{svc.api_url}</td>
|
||||
<td>{statusBadge(svc.status)}</td>
|
||||
<td>
|
||||
<button onClick={() => handleCheckExtStatus(svc.id)} style={btnStyle('#3B82F6', 'small')}>检测</button>
|
||||
<button onClick={() => handleDeleteExtService(svc.id)} style={{ ...btnStyle('#DC2626', 'small'), marginLeft: 4 }}>删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ borderTop: '1px solid #E5E7EB', paddingTop: 16 }}>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: 14 }}>配置 Dify 知识库</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div style={{ ...modalStyle, width: 720, maxWidth: '92vw' }} onClick={e => e.stopPropagation()}>
|
||||
<h3 style={{ margin: '0 0 20px 0', fontSize: 18 }}>外部知识库服务配置</h3>
|
||||
|
||||
{/* Section 1: Dify 外部服务列表 */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: 14, color: '#374151', fontWeight: 600 }}>已接入外部服务</h4>
|
||||
{extServices.length > 0 ? (
|
||||
<div style={{ border: '1px solid #E5E7EB', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<table className="admin-table" style={{ marginBottom: 0, width: '100%' }}>
|
||||
<thead><tr><th style={{ textAlign: 'left' }}>名称</th><th style={{ textAlign: 'left' }}>类型</th><th style={{ textAlign: 'left' }}>URL</th><th style={{ textAlign: 'left' }}>状态</th><th style={{ textAlign: 'center', width: 120 }}>操作</th></tr></thead>
|
||||
<tbody>
|
||||
{extServices.map(svc => (
|
||||
<tr key={svc.id}>
|
||||
<td>{svc.name}</td>
|
||||
<td><span style={{ fontSize: 12, padding: '2px 8px', background: '#F3F4FF', color: '#8B5CF6', borderRadius: 4 }}>Dify</span></td>
|
||||
<td style={{ fontSize: 12, maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{svc.api_url}</td>
|
||||
<td>{statusBadge(svc.status)}</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
<button onClick={() => handleCheckExtStatus(svc.id)} style={btnStyle('#3B82F6', 'small')}>检测</button>
|
||||
<button onClick={() => handleDeleteExtService(svc.id)} style={{ ...btnStyle('#DC2626', 'small'), marginLeft: 6 }}>删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: 16, background: '#F9FAFB', borderRadius: 8, color: '#6B7280', fontSize: 13 }}>暂无外部服务,可在下方添加 Dify 知识库。</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section 2: Dify 配置表单 */}
|
||||
<div style={{ borderTop: '1px solid #E5E7EB', paddingTop: 20, marginBottom: 24 }}>
|
||||
<h4 style={{ margin: '0 0 16px 0', fontSize: 14, color: '#374151', fontWeight: 600 }}>配置 Dify 知识库</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div><label style={labelStyle}>名称</label><input value={extForm.name} onChange={e => setExtForm({ ...extForm, name: e.target.value })} placeholder="如: 生产环境 Dify" style={fullInput} /></div>
|
||||
<div><label style={labelStyle}>知识库名称(Dataset)</label><input value={extForm.dataset_name} onChange={e => setExtForm({ ...extForm, dataset_name: e.target.value })} placeholder="如: qihuai" style={fullInput} /></div>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}><label style={labelStyle}>API URL</label><input value={extForm.api_url} onChange={e => setExtForm({ ...extForm, api_url: e.target.value })} placeholder="https://dify.qihuyun.com/v1" style={fullInput} /></div>
|
||||
<div style={{ marginTop: 12 }}><label style={labelStyle}>API Key(dataset 密钥)</label><input type="password" value={extForm.api_key} onChange={e => setExtForm({ ...extForm, api_key: e.target.value })} placeholder="dataset-xxxx" style={fullInput} /></div>
|
||||
<div style={{ marginTop: 16 }}><label style={labelStyle}>API URL</label><input value={extForm.api_url} onChange={e => setExtForm({ ...extForm, api_url: e.target.value })} placeholder="https://dify.qihuyun.com/v1" style={fullInput} /></div>
|
||||
<div style={{ marginTop: 16 }}><label style={labelStyle}>API Key(dataset 密钥)</label><input type="password" value={extForm.api_key} onChange={e => setExtForm({ ...extForm, api_key: e.target.value })} placeholder="dataset-xxxx" style={fullInput} /></div>
|
||||
|
||||
{extMsg && (
|
||||
<div style={{ marginTop: 12, fontSize: 13, color: extMsg.startsWith('✅') ? '#16A34A' : '#DC2626', padding: '6px 12px', background: extMsg.startsWith('✅') ? '#DCFCE7' : '#FEE2E2', borderRadius: 6 }}>
|
||||
<div style={{ marginTop: 16, fontSize: 13, color: extMsg.startsWith('✅') ? '#16A34A' : '#DC2626', padding: '8px 12px', background: extMsg.startsWith('✅') ? '#DCFCE7' : '#FEE2E2', borderRadius: 6 }}>
|
||||
{extMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 16, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 16, alignItems: 'center' }}>
|
||||
<button onClick={handleCreateExtService} style={btnStyle('#8B5CF6')}>保存并测试连接</button>
|
||||
<button onClick={handleIngestPlatform} disabled={ingesting} style={btnStyle(ingesting ? '#D1D5DB' : '#219B5E')}>
|
||||
{ingesting ? '入库中...' : '入库平台内容'}
|
||||
</button>
|
||||
<button onClick={() => setShowExtModal(false)} style={btnStyle('#6B7280')}>关闭</button>
|
||||
</div>
|
||||
|
||||
{ingestResult && (
|
||||
<div style={{ marginTop: 12, fontSize: 13, padding: 12, background: '#F0FDF4', border: '1px solid #BBF7D0', borderRadius: 8 }}>
|
||||
<div style={{ marginTop: 16, fontSize: 13, padding: 12, background: '#F0FDF4', border: '1px solid #BBF7D0', borderRadius: 8 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 6 }}>入库结果</div>
|
||||
<div>AI 聊天回答:<b>{ingestResult.chat_answers}</b> 条</div>
|
||||
<div>用户上传文档:<b>{ingestResult.user_docs}</b> 条</div>
|
||||
@@ -578,6 +618,45 @@ const Knowledge: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section 3: AnySearch 全局固定配置 */}
|
||||
<div style={{ borderTop: '1px solid #E5E7EB', paddingTop: 20, marginBottom: 8 }}>
|
||||
<h4 style={{ margin: '0 0 16px 0', fontSize: 14, color: '#374151', fontWeight: 600 }}>AnySearch 联网检索(全局配置)</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 16, alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
value={anysearchForm.api_key}
|
||||
onChange={e => setAnysearchForm({ ...anysearchForm, api_key: e.target.value })}
|
||||
placeholder="as_sk_xxxx"
|
||||
style={fullInput}
|
||||
/>
|
||||
</div>
|
||||
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', fontWeight: 500, fontSize: 14, color: '#374151', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={anysearchForm.is_enabled} onChange={e => setAnysearchForm({ ...anysearchForm, is_enabled: e.target.checked })} />
|
||||
启用联网检索
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{anysearchMsg && (
|
||||
<div style={{ marginTop: 16, fontSize: 13, color: anysearchMsg.startsWith('✅') ? '#16A34A' : (anysearchMsg.startsWith('⚠️') ? '#D97706' : '#DC2626'), padding: '8px 12px', background: anysearchMsg.startsWith('✅') ? '#DCFCE7' : (anysearchMsg.startsWith('⚠️') ? '#FEF3C7' : '#FEE2E2'), borderRadius: 6 }}>
|
||||
{anysearchMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 16, alignItems: 'center' }}>
|
||||
<button onClick={handleSaveAnySearch} disabled={anysearchLoading} style={btnStyle(anysearchLoading ? '#D1D5DB' : '#8B5CF6')}>
|
||||
{anysearchLoading ? '保存中...' : '保存并测试连接'}
|
||||
</button>
|
||||
<span style={{ fontSize: 12, color: '#6B7280' }}>配置对平台所有用户生效,AI 聊天将自动注入实时联网结果。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{ borderTop: '1px solid #E5E7EB', marginTop: 24, paddingTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button onClick={() => setShowExtModal(false)} style={btnStyle('#6B7280')}>关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user