feat(admin): Implement Prompt knowledge base integration

Features:

- PromptService enhancement: enhanceWithKnowledge(), loadFullKnowledge(), ragSearch()

- FULL mode: Load entire knowledge base content

- RAG mode: Vector search based on user query

- Knowledge config API: PUT /:code/knowledge-config

- Test render with knowledge injection support

- Frontend: Knowledge config UI in Prompt editor

Bug fixes:

- Fix knowledge config not returned in getPromptDetail

- Fix publish button 400 error (empty request body)

- Fix cache not invalidated after publish

- Add detailed logging for debugging

Documentation:

- Add development record 2026-01-28

- Update ADMIN module status to Phase 4.6

- Update system status document to v4.5

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-01 20:26:20 +08:00
parent 0d9e6b9922
commit aaa29ea9d3
15 changed files with 1459 additions and 26 deletions

View File

@@ -12,12 +12,17 @@ import {
Timeline,
Alert,
Spin,
Switch,
Select,
InputNumber,
Divider,
} from 'antd'
import {
ArrowLeftOutlined,
SaveOutlined,
RocketOutlined,
LockOutlined,
BookOutlined,
} from '@ant-design/icons'
import { useAuth } from '../../framework/auth'
import PromptEditor from './components/PromptEditor'
@@ -27,8 +32,12 @@ import {
publishPrompt,
rollbackPrompt,
testRender,
saveKnowledgeConfig,
fetchSystemKbList,
type PromptDetail,
type PromptVersion,
type KnowledgeConfig,
type SystemKb,
} from './api/promptApi'
const { TextArea } = Input
@@ -68,10 +77,32 @@ const PromptEditorPage = () => {
visible: false,
version: null,
})
// 知识库配置状态
const [systemKbs, setSystemKbs] = useState<SystemKb[]>([])
const [knowledgeConfig, setKnowledgeConfig] = useState<KnowledgeConfig>({
enabled: false,
kb_codes: [],
injection_mode: 'FULL',
target_variable: 'context',
top_k: 5,
min_score: 0.5,
})
const [savingKbConfig, setSavingKbConfig] = useState(false)
// 权限检查
const canPublish = user?.role === 'SUPER_ADMIN'
// 加载系统知识库列表
const loadSystemKbs = async () => {
try {
const kbs = await fetchSystemKbList()
setSystemKbs(kbs)
} catch (error) {
console.warn('加载系统知识库列表失败', error)
}
}
// 加载 Prompt 详情
const loadPromptDetail = async () => {
if (!code) return
@@ -86,6 +117,11 @@ const PromptEditorPage = () => {
if (latestVersion) {
setContent(latestVersion.content)
}
// 初始化知识库配置
if (data.knowledge_config) {
setKnowledgeConfig(data.knowledge_config)
}
} catch (error: any) {
message.error(error.message || '加载失败')
navigate('/admin/prompts')
@@ -96,6 +132,7 @@ const PromptEditorPage = () => {
useEffect(() => {
loadPromptDetail()
loadSystemKbs()
}, [code])
// 内容变化
@@ -158,12 +195,21 @@ const PromptEditorPage = () => {
})
}
// 测试渲染
// 测试渲染(支持知识库注入)
const handleTestRender = async () => {
try {
const result = await testRender(content, testVariables)
// 如果启用了知识库增强,传入 code 以触发知识库注入
const result = await testRender(
content,
testVariables,
knowledgeConfig.enabled ? code : undefined // 传入 code 触发知识库注入
)
setTestResult(result.rendered)
message.success('渲染成功')
if (result.knowledgeInjected) {
message.success('渲染成功,知识库内容已注入!')
} else {
message.success('渲染成功')
}
} catch (error: any) {
message.error(error.message || '渲染失败')
}
@@ -174,6 +220,21 @@ const PromptEditorPage = () => {
setViewVersionModal({ visible: true, version })
}
// 保存知识库配置
const handleSaveKnowledgeConfig = async () => {
if (!code) return
setSavingKbConfig(true)
try {
await saveKnowledgeConfig(code, knowledgeConfig)
message.success('知识库配置已保存')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
setSavingKbConfig(false)
}
}
// 回滚到指定版本
const handleRollback = (version: PromptVersion) => {
if (!code) return
@@ -387,6 +448,143 @@ const PromptEditorPage = () => {
)}
</Card>
{/* 知识库增强配置 */}
<Card
title={
<Space>
<BookOutlined />
<span></span>
{knowledgeConfig.enabled && (
<Tag color="green"></Tag>
)}
</Space>
}
extra={
<Button
type="primary"
size="small"
onClick={handleSaveKnowledgeConfig}
loading={savingKbConfig}
style={{ background: PRIMARY_COLOR, borderColor: PRIMARY_COLOR }}
>
</Button>
}
>
<div className="space-y-4">
{/* 启用开关 */}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-700"></span>
<Switch
checked={knowledgeConfig.enabled}
onChange={(checked) => setKnowledgeConfig({ ...knowledgeConfig, enabled: checked })}
/>
</div>
{knowledgeConfig.enabled && (
<>
<Divider className="my-3" />
{/* 知识库选择 */}
<div>
<label className="text-sm text-gray-600 block mb-1"></label>
<Select
mode="multiple"
placeholder="选择知识库"
value={knowledgeConfig.kb_codes}
onChange={(values) => setKnowledgeConfig({ ...knowledgeConfig, kb_codes: values })}
style={{ width: '100%' }}
options={systemKbs.map(kb => ({
value: kb.code,
label: `${kb.name} (${kb.documentCount}篇)`,
}))}
/>
</div>
{/* 注入模式 */}
<div>
<label className="text-sm text-gray-600 block mb-1"></label>
<Select
value={knowledgeConfig.injection_mode}
onChange={(value) => setKnowledgeConfig({ ...knowledgeConfig, injection_mode: value })}
style={{ width: '100%' }}
options={[
{ value: 'FULL', label: 'FULL - 全量注入(适合小型核心知识库)' },
{ value: 'RAG', label: 'RAG - 向量检索(适合大型知识库)' },
]}
/>
</div>
{/* 目标变量 */}
<div>
<label className="text-sm text-gray-600 block mb-1"></label>
<Select
value={knowledgeConfig.target_variable}
onChange={(value) => setKnowledgeConfig({ ...knowledgeConfig, target_variable: value })}
style={{ width: '100%' }}
options={[
{ value: 'context', label: 'context推荐' },
...(prompt.variables || []).map(v => ({ value: v, label: v })),
]}
/>
</div>
{/* RAG 模式配置 */}
{knowledgeConfig.injection_mode === 'RAG' && (
<>
<div className="text-xs text-gray-400 mt-2 mb-1 border-t pt-2">RAG </div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-xs text-gray-500">Top K</label>
<InputNumber
value={knowledgeConfig.top_k}
onChange={(value) => setKnowledgeConfig({ ...knowledgeConfig, top_k: value || 5 })}
min={1}
max={20}
size="small"
style={{ width: '100%' }}
/>
</div>
<div>
<label className="text-xs text-gray-500"></label>
<InputNumber
value={knowledgeConfig.min_score}
onChange={(value) => setKnowledgeConfig({ ...knowledgeConfig, min_score: value || 0.5 })}
min={0}
max={1}
step={0.1}
size="small"
style={{ width: '100%' }}
/>
</div>
</div>
</>
)}
{/* FULL 模式配置 */}
{knowledgeConfig.injection_mode === 'FULL' && (
<>
<div className="text-xs text-gray-400 mt-2 mb-1 border-t pt-2">FULL </div>
<div>
<label className="text-xs text-gray-500"> Token </label>
<InputNumber
value={knowledgeConfig.max_tokens}
onChange={(value) => setKnowledgeConfig({ ...knowledgeConfig, max_tokens: value || undefined })}
min={1000}
max={100000}
step={1000}
size="small"
placeholder="默认 50000"
style={{ width: '100%' }}
/>
</div>
</>
)}
</>
)}
</div>
</Card>
{/* 版本历史 */}
<Card title="📜 版本历史">
<Timeline>

View File

@@ -60,6 +60,18 @@ export interface PromptVersion {
createdAt: string
}
// 知识库增强配置
export interface KnowledgeConfig {
enabled: boolean
kb_codes: string[]
injection_mode: 'FULL' | 'RAG'
target_variable: string
query_field?: string
top_k?: number
min_score?: number
max_tokens?: number
}
export interface PromptDetail {
id: number
code: string
@@ -67,6 +79,7 @@ export interface PromptDetail {
module: string
description?: string
variables?: string[]
knowledge_config?: KnowledgeConfig | null
versions: PromptVersion[]
createdAt: string
updatedAt: string
@@ -143,12 +156,19 @@ export async function saveDraft(
* 发布 Prompt
*/
export async function publishPrompt(code: string): Promise<any> {
console.log('[promptApi] publishPrompt:', code)
const headers = getAuthHeaders()
console.log('[promptApi] headers:', headers)
const response = await fetch(`${API_BASE}/${code}/publish`, {
method: 'POST',
headers: getAuthHeaders(),
headers,
body: JSON.stringify({}), // 发送空对象作为请求体
})
console.log('[promptApi] response status:', response.status)
const data = await response.json()
console.log('[promptApi] response data:', data)
if (!data.success) {
throw new Error(data.error || 'Failed to publish prompt')
@@ -213,12 +233,21 @@ export async function getDebugStatus(): Promise<any> {
/**
* 测试渲染
* @param content - Prompt 内容
* @param variables - 变量
* @param code - 可选:传入 code 以测试知识库注入
* @param userQuery - 可选RAG 模式的查询词
*/
export async function testRender(content: string, variables: Record<string, any>): Promise<any> {
export async function testRender(
content: string,
variables: Record<string, any>,
code?: string,
userQuery?: string
): Promise<{ rendered: string; extractedVariables: string[]; validation: any; knowledgeInjected?: boolean }> {
const response = await fetch(`${API_BASE}/test-render`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ content, variables }),
body: JSON.stringify({ content, variables, code, userQuery }),
})
const data = await response.json()
@@ -230,4 +259,53 @@ export async function testRender(content: string, variables: Record<string, any>
return data.data
}
/**
* 保存知识库配置
*/
export async function saveKnowledgeConfig(
code: string,
knowledgeConfig: KnowledgeConfig
): Promise<any> {
const response = await fetch(`${API_BASE}/${code}/knowledge-config`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify({ knowledge_config: knowledgeConfig }),
})
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to save knowledge config')
}
return data.data
}
// 系统知识库类型
export interface SystemKb {
id: string
code: string
name: string
description?: string
category?: string
documentCount: number
totalTokens: number
status: string
}
/**
* 获取系统知识库列表(用于选择)
*/
export async function fetchSystemKbList(): Promise<SystemKb[]> {
const response = await fetch('/api/v1/admin/system-kb', {
headers: getAuthHeaders(),
})
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to fetch system knowledge bases')
}
return data.data
}