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:
84
backend/src/common/prompt/__tests__/check-permissions.cjs
Normal file
84
backend/src/common/prompt/__tests__/check-permissions.cjs
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 检查超级管理员权限
|
||||
*/
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('检查超级管理员权限\n');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 查找超级管理员
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { phone: '18000000002' },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.log('❌ 用户不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('👤 用户:', user.name, '(' + user.phone + ')');
|
||||
console.log('🎭 角色:', user.role);
|
||||
console.log('');
|
||||
|
||||
// 检查 tenant_members 获取权限
|
||||
const tenantMember = await prisma.tenant_members.findFirst({
|
||||
where: { user_id: user.id },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
role_permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tenantMember) {
|
||||
console.log('⚠️ 用户没有 tenant_members 记录');
|
||||
console.log(' 用户角色是直接在 User 表的 role 字段:', user.role);
|
||||
|
||||
// 对于 SUPERADMIN,检查是否有所有权限
|
||||
if (user.role === 'SUPERADMIN') {
|
||||
console.log('');
|
||||
console.log('✅ SUPERADMIN 角色通常拥有所有权限');
|
||||
console.log(' 权限检查可能是在中间件中直接放行的');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取权限
|
||||
const permissions = new Set();
|
||||
tenantMember.roles.forEach(mr => {
|
||||
mr.role.role_permissions.forEach(rp => {
|
||||
permissions.add(rp.permission.code);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('🔑 权限列表:');
|
||||
const permList = Array.from(permissions).sort();
|
||||
permList.forEach(p => {
|
||||
console.log(' -', p);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// 检查关键权限
|
||||
console.log('📋 Prompt 相关权限检查:');
|
||||
const promptPerms = ['prompt:read', 'prompt:edit', 'prompt:publish', 'prompt:debug'];
|
||||
promptPerms.forEach(p => {
|
||||
const has = permissions.has(p);
|
||||
console.log(' ' + (has ? '✅' : '❌') + ' ' + p);
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
59
backend/src/common/prompt/__tests__/check-versions.cjs
Normal file
59
backend/src/common/prompt/__tests__/check-versions.cjs
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 查看 Prompt 版本信息
|
||||
*/
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const code = 'AIA_SCIENTIFIC_QUESTION';
|
||||
|
||||
// 查询模板
|
||||
const template = await prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
console.log('模板不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('模板:', template.name, '(' + code + ')');
|
||||
console.log('='.repeat(60));
|
||||
console.log('');
|
||||
|
||||
const versions = await prisma.prompt_versions.findMany({
|
||||
where: { template_id: template.id },
|
||||
orderBy: { version: 'desc' },
|
||||
select: {
|
||||
version: true,
|
||||
status: true,
|
||||
content: true,
|
||||
created_at: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('所有版本:');
|
||||
console.log('-'.repeat(60));
|
||||
|
||||
versions.forEach(v => {
|
||||
const hasContext = v.content.includes('{{context}}');
|
||||
const statusEmoji = v.status === 'ACTIVE' ? '🟢' : (v.status === 'DRAFT' ? '🟡' : '⚪');
|
||||
|
||||
console.log(`${statusEmoji} v${v.version} [${v.status}]`);
|
||||
console.log(` 包含 {{context}}: ${hasContext ? '✅ 是' : '❌ 否'}`);
|
||||
console.log(` 内容预览:`);
|
||||
console.log(` "${v.content.substring(0, 100).replace(/\n/g, ' ')}..."`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
console.log('-'.repeat(60));
|
||||
console.log('');
|
||||
console.log('📖 版本说明:');
|
||||
console.log(' ACTIVE (🟢): 生产版本,实际业务使用的版本');
|
||||
console.log(' DRAFT (🟡): 草稿版本,仅在调试模式下使用');
|
||||
console.log(' 其他 (⚪): 历史版本,已归档');
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
34
backend/src/common/prompt/__tests__/test-api-publish.cjs
Normal file
34
backend/src/common/prompt/__tests__/test-api-publish.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 测试发布 API
|
||||
* 模拟前端调用
|
||||
*/
|
||||
|
||||
async function testPublishAPI() {
|
||||
const code = 'AIA_SCIENTIFIC_QUESTION';
|
||||
const baseUrl = 'http://localhost:3001'; // 后端直接端口
|
||||
|
||||
// 需要一个有效的 Token
|
||||
// 这里使用一个假的 Token 来测试
|
||||
const fakeToken = 'test-token';
|
||||
|
||||
console.log('测试发布 API...');
|
||||
console.log('URL:', `${baseUrl}/api/admin/prompts/${code}/publish`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/admin/prompts/${code}/publish`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${fakeToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('状态码:', response.status);
|
||||
const data = await response.json();
|
||||
console.log('响应:', JSON.stringify(data, null, 2));
|
||||
} catch (error) {
|
||||
console.log('错误:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testPublishAPI();
|
||||
201
backend/src/common/prompt/__tests__/test-knowledge-injection.cjs
Normal file
201
backend/src/common/prompt/__tests__/test-knowledge-injection.cjs
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 知识库注入功能测试脚本
|
||||
*
|
||||
* 测试 PromptService.get() 的知识库增强功能
|
||||
*
|
||||
* 使用方法:
|
||||
* cd backend
|
||||
* node src/common/prompt/__tests__/test-knowledge-injection.cjs
|
||||
*/
|
||||
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🧪 知识库注入功能测试\n');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 1. 检查系统知识库
|
||||
console.log('\n📚 Step 1: 检查系统知识库...');
|
||||
const systemKbs = await prisma.system_knowledge_bases.findMany({
|
||||
where: { status: 'active' },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
document_count: true,
|
||||
total_tokens: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (systemKbs.length === 0) {
|
||||
console.log('❌ 未找到任何系统知识库!请先创建知识库并上传文档。');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ 找到 ${systemKbs.length} 个系统知识库:`);
|
||||
systemKbs.forEach(kb => {
|
||||
console.log(` - ${kb.code}: ${kb.name} (${kb.document_count} 篇文档, ${kb.total_tokens} tokens)`);
|
||||
});
|
||||
|
||||
// 2. 检查 Prompt 配置
|
||||
console.log('\n📝 Step 2: 检查已配置知识库的 Prompt...');
|
||||
const promptsWithKb = await prisma.prompt_templates.findMany({
|
||||
where: {
|
||||
knowledge_config: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
code: true,
|
||||
name: true,
|
||||
knowledge_config: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (promptsWithKb.length === 0) {
|
||||
console.log('❌ 未找到配置了知识库的 Prompt!');
|
||||
console.log(' 请在 Prompt 编辑页面配置知识库增强。');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ 找到 ${promptsWithKb.length} 个配置了知识库的 Prompt:`);
|
||||
promptsWithKb.forEach(p => {
|
||||
const config = p.knowledge_config;
|
||||
console.log(` - ${p.code}: ${p.name}`);
|
||||
console.log(` enabled: ${config?.enabled}, mode: ${config?.injection_mode}, kb_codes: ${config?.kb_codes?.join(', ')}`);
|
||||
});
|
||||
|
||||
// 3. 选择第一个启用了知识库的 Prompt 进行测试
|
||||
const testPrompt = promptsWithKb.find(p => p.knowledge_config?.enabled);
|
||||
if (!testPrompt) {
|
||||
console.log('❌ 没有启用知识库的 Prompt!请在配置中开启 "启用知识库增强"。');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = testPrompt.knowledge_config;
|
||||
console.log(`\n🎯 Step 3: 测试 Prompt: ${testPrompt.code}`);
|
||||
console.log(` 知识库: ${config.kb_codes?.join(', ')}`);
|
||||
console.log(` 模式: ${config.injection_mode}`);
|
||||
console.log(` 目标变量: ${config.target_variable || 'context'}`);
|
||||
|
||||
// 4. 检查知识库是否有文档和分块
|
||||
console.log('\n📄 Step 4: 检查知识库文档...');
|
||||
for (const kbCode of config.kb_codes || []) {
|
||||
const kb = await prisma.system_knowledge_bases.findUnique({
|
||||
where: { code: kbCode },
|
||||
});
|
||||
|
||||
if (!kb) {
|
||||
console.log(` ❌ 知识库 ${kbCode} 不存在!`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查询 EKB 知识库
|
||||
const ekbKb = await prisma.ekbKnowledgeBase.findUnique({
|
||||
where: { id: kb.id },
|
||||
});
|
||||
|
||||
if (!ekbKb) {
|
||||
console.log(` ❌ EKB 知识库 ${kbCode} 不存在!文档可能未被向量化。`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查询分块数量
|
||||
const chunkCount = await prisma.ekbChunk.count({
|
||||
where: {
|
||||
document: {
|
||||
kbId: ekbKb.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(` ✅ ${kbCode}: EKB 存在, ${chunkCount} 个分块`);
|
||||
|
||||
if (chunkCount === 0) {
|
||||
console.log(` ⚠️ 警告: 知识库 ${kbCode} 没有分块!请检查文档是否成功处理。`);
|
||||
} else {
|
||||
// 显示前 2 个分块
|
||||
const sampleChunks = await prisma.ekbChunk.findMany({
|
||||
where: {
|
||||
document: {
|
||||
kbId: ekbKb.id,
|
||||
},
|
||||
},
|
||||
take: 2,
|
||||
select: {
|
||||
content: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(` 📋 样本分块:`);
|
||||
sampleChunks.forEach((chunk, i) => {
|
||||
const preview = chunk.content.substring(0, 100).replace(/\n/g, ' ');
|
||||
console.log(` [${i + 1}] ${preview}...`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 模拟 PromptService.get() 的知识库加载逻辑
|
||||
console.log('\n🔄 Step 5: 模拟知识库内容加载...');
|
||||
|
||||
if (config.injection_mode === 'FULL') {
|
||||
console.log(' 模式: FULL (全量加载)');
|
||||
|
||||
let totalContent = '';
|
||||
for (const kbCode of config.kb_codes || []) {
|
||||
const kb = await prisma.system_knowledge_bases.findUnique({
|
||||
where: { code: kbCode },
|
||||
});
|
||||
if (!kb) continue;
|
||||
|
||||
const chunks = await prisma.ekbChunk.findMany({
|
||||
where: {
|
||||
document: {
|
||||
kbId: kb.id,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
content: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ documentId: 'asc' },
|
||||
{ chunkIndex: 'asc' },
|
||||
],
|
||||
});
|
||||
|
||||
const kbContent = chunks.map(c => c.content).join('\n\n');
|
||||
totalContent += kbContent;
|
||||
console.log(` ✅ ${kbCode}: 加载了 ${chunks.length} 个分块, ${kbContent.length} 字符`);
|
||||
}
|
||||
|
||||
if (totalContent.length > 0) {
|
||||
console.log(`\n 📊 总加载内容: ${totalContent.length} 字符`);
|
||||
console.log(` 📋 内容预览 (前 300 字符):`);
|
||||
console.log(' ' + '-'.repeat(50));
|
||||
console.log(' ' + totalContent.substring(0, 300).replace(/\n/g, '\n '));
|
||||
console.log(' ' + '-'.repeat(50));
|
||||
console.log('\n ✅ 知识库内容加载成功!');
|
||||
} else {
|
||||
console.log(' ❌ 知识库内容为空!');
|
||||
}
|
||||
} else {
|
||||
console.log(' 模式: RAG (向量检索)');
|
||||
console.log(' ⚠️ RAG 模式需要 userQuery 参数,此脚本暂不测试向量检索。');
|
||||
}
|
||||
|
||||
// 6. 总结
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📋 测试总结:\n');
|
||||
|
||||
console.log('如果以上步骤都显示 ✅,但实际使用仍不生效,请检查:');
|
||||
console.log('1. 业务代码是否调用 promptService.get() 而不是直接渲染');
|
||||
console.log('2. 调用时是否传入了正确的参数 (userId, userQuery)');
|
||||
console.log('3. Prompt 模板中是否使用了 {{context}} 变量');
|
||||
console.log('4. 知识库配置是否已保存 (点击"保存配置"按钮)');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
164
backend/src/common/prompt/__tests__/test-prompt-get.cjs
Normal file
164
backend/src/common/prompt/__tests__/test-prompt-get.cjs
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 端到端测试 PromptService.get() 知识库注入
|
||||
*
|
||||
* 使用方法:
|
||||
* cd backend
|
||||
* node src/common/prompt/__tests__/test-prompt-get.cjs
|
||||
*/
|
||||
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 模拟 PromptService.get() 的逻辑
|
||||
async function testPromptServiceGet(code, variables = {}) {
|
||||
console.log(`\n🎯 测试 promptService.get("${code}", ${JSON.stringify(variables)})\n`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 1. 获取模板
|
||||
const template = await prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
console.log(`❌ Prompt 模板 ${code} 不存在!`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n📝 Prompt 模板: ${template.name}`);
|
||||
console.log(` 知识库配置: ${JSON.stringify(template.knowledge_config, null, 2)}`);
|
||||
|
||||
// 2. 获取版本
|
||||
const version = await prisma.prompt_versions.findFirst({
|
||||
where: { template_id: template.id, status: 'ACTIVE' },
|
||||
orderBy: { version: 'desc' },
|
||||
});
|
||||
|
||||
if (!version) {
|
||||
console.log(`❌ 没有 ACTIVE 版本!`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n📄 Prompt 内容 (v${version.version}):`);
|
||||
console.log(' ' + version.content.substring(0, 200).replace(/\n/g, '\n ') + '...');
|
||||
|
||||
// 3. 知识库增强
|
||||
const config = template.knowledge_config;
|
||||
if (!config || !config.enabled) {
|
||||
console.log('\n⚠️ 知识库增强未启用');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n📚 知识库增强配置:');
|
||||
console.log(` 启用: ${config.enabled}`);
|
||||
console.log(` 知识库: ${config.kb_codes?.join(', ')}`);
|
||||
console.log(` 模式: ${config.injection_mode}`);
|
||||
console.log(` 目标变量: ${config.target_variable || 'context'}`);
|
||||
|
||||
// 4. 加载知识库内容 (FULL 模式)
|
||||
if (config.injection_mode === 'FULL') {
|
||||
console.log('\n🔄 FULL 模式: 加载知识库全文...');
|
||||
|
||||
// 获取 system_knowledge_bases ID
|
||||
const systemKbs = await prisma.system_knowledge_bases.findMany({
|
||||
where: { code: { in: config.kb_codes || [] }, status: 'active' },
|
||||
select: { id: true, code: true, name: true },
|
||||
});
|
||||
|
||||
console.log(` 找到 ${systemKbs.length} 个系统知识库:`, systemKbs.map(kb => kb.code));
|
||||
|
||||
const kbIds = systemKbs.map(kb => kb.id);
|
||||
console.log(` 系统知识库 IDs: ${kbIds.join(', ')}`);
|
||||
|
||||
// 检查 EKB 知识库
|
||||
const ekbKbs = await prisma.ekbKnowledgeBase.findMany({
|
||||
where: { id: { in: kbIds } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
console.log(` EKB 知识库: ${ekbKbs.length} 个`, ekbKbs.map(kb => kb.id));
|
||||
|
||||
// 查询分块
|
||||
const chunks = await prisma.ekbChunk.findMany({
|
||||
where: {
|
||||
document: {
|
||||
kbId: { in: kbIds },
|
||||
},
|
||||
},
|
||||
select: { content: true },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
console.log(` 找到 ${chunks.length} 个分块`);
|
||||
|
||||
if (chunks.length === 0) {
|
||||
console.log('\n❌ 问题定位: ekbChunk 表中没有找到匹配的分块!');
|
||||
console.log(' 这可能是因为 ekbChunk.document.kbId 与 system_knowledge_bases.id 不匹配');
|
||||
|
||||
// 检查 ekbDocument 的 kbId
|
||||
console.log('\n 📋 检查 ekbDocument 表...');
|
||||
const docs = await prisma.ekbDocument.findMany({
|
||||
take: 5,
|
||||
select: { id: true, kbId: true, fileName: true },
|
||||
});
|
||||
console.log(` ekbDocument 样本:`, docs.map(d => ({ id: d.id.substring(0, 8), kbId: d.kbId.substring(0, 8), name: d.fileName })));
|
||||
|
||||
console.log('\n 📋 检查 ekbKnowledgeBase 表...');
|
||||
const allEkb = await prisma.ekbKnowledgeBase.findMany({
|
||||
take: 5,
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
console.log(` ekbKnowledgeBase 样本:`, allEkb.map(kb => ({ id: kb.id.substring(0, 8), name: kb.name })));
|
||||
|
||||
console.log('\n 📋 检查 system_knowledge_bases 表...');
|
||||
const allSystemKbs = await prisma.system_knowledge_bases.findMany({
|
||||
take: 5,
|
||||
select: { id: true, code: true, name: true },
|
||||
});
|
||||
console.log(` system_knowledge_bases 样本:`, allSystemKbs.map(kb => ({ id: kb.id.substring(0, 8), code: kb.code, name: kb.name })));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 合并内容
|
||||
const knowledgeContent = chunks.map(c => c.content).join('\n\n');
|
||||
console.log(`\n✅ 知识库内容加载成功,长度: ${knowledgeContent.length} 字符`);
|
||||
console.log(` 预览: ${knowledgeContent.substring(0, 200).replace(/\n/g, ' ')}...`);
|
||||
|
||||
// 5. 注入到变量
|
||||
const targetVariable = config.target_variable || 'context';
|
||||
const enhancedVariables = { ...variables };
|
||||
enhancedVariables[targetVariable] = knowledgeContent;
|
||||
|
||||
console.log(`\n📊 增强后的变量: { ${Object.keys(enhancedVariables).join(', ')} }`);
|
||||
console.log(` ${targetVariable} 长度: ${enhancedVariables[targetVariable]?.length || 0} 字符`);
|
||||
|
||||
// 6. 渲染模板
|
||||
const rendered = version.content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
||||
return enhancedVariables[key] || '';
|
||||
});
|
||||
|
||||
console.log('\n📄 渲染后的 Prompt:');
|
||||
console.log(' ' + rendered.substring(0, 500).replace(/\n/g, '\n ') + '...');
|
||||
|
||||
// 检查 {{context}} 是否被替换
|
||||
if (rendered.includes('{{context}}')) {
|
||||
console.log('\n❌ 问题: {{context}} 未被替换!');
|
||||
} else {
|
||||
console.log('\n✅ 成功: {{context}} 已被知识库内容替换!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🧪 PromptService.get() 端到端测试\n');
|
||||
|
||||
// 测试已配置知识库的 Prompt
|
||||
await testPromptServiceGet('AIA_SCIENTIFIC_QUESTION', {});
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('测试完成');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
99
backend/src/common/prompt/__tests__/test-publish.cjs
Normal file
99
backend/src/common/prompt/__tests__/test-publish.cjs
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 测试发布功能
|
||||
*/
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const code = 'AIA_SCIENTIFIC_QUESTION';
|
||||
|
||||
console.log('测试发布功能\n');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 1. 查找模板
|
||||
const template = await prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
console.log('❌ 模板不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 模板存在:', template.name);
|
||||
|
||||
// 2. 查找 DRAFT 版本
|
||||
const draft = await prisma.prompt_versions.findFirst({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'DRAFT',
|
||||
},
|
||||
});
|
||||
|
||||
if (!draft) {
|
||||
console.log('❌ 没有 DRAFT 版本可发布');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 找到 DRAFT 版本: v' + draft.version);
|
||||
console.log(' 内容预览:', draft.content.substring(0, 80) + '...');
|
||||
|
||||
// 3. 查找当前 ACTIVE 版本
|
||||
const active = await prisma.prompt_versions.findFirst({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
if (active) {
|
||||
console.log('✅ 当前 ACTIVE 版本: v' + active.version);
|
||||
} else {
|
||||
console.log('⚠️ 没有 ACTIVE 版本');
|
||||
}
|
||||
|
||||
// 4. 尝试发布
|
||||
console.log('\n🚀 尝试发布...');
|
||||
|
||||
try {
|
||||
await prisma.$transaction([
|
||||
// 归档当前 ACTIVE
|
||||
prisma.prompt_versions.updateMany({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
data: {
|
||||
status: 'ARCHIVED',
|
||||
},
|
||||
}),
|
||||
// 激活 DRAFT
|
||||
prisma.prompt_versions.update({
|
||||
where: { id: draft.id },
|
||||
data: {
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log('✅ 发布成功!v' + draft.version + ' 现在是 ACTIVE');
|
||||
|
||||
// 验证
|
||||
const newActive = await prisma.prompt_versions.findFirst({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(' 验证: 当前 ACTIVE 是 v' + newActive.version);
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ 发布失败:', error.message);
|
||||
console.log(' 详细错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(console.error)
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -40,6 +40,8 @@ interface SetDebugModeBody {
|
||||
interface TestRenderBody {
|
||||
content: string;
|
||||
variables: Record<string, any>;
|
||||
code?: string; // 可选:传入 code 以测试知识库注入
|
||||
userQuery?: string; // 可选:RAG 模式的查询词
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +152,7 @@ export async function getPromptDetail(
|
||||
module: template.module,
|
||||
description: template.description,
|
||||
variables: template.variables,
|
||||
knowledge_config: template.knowledge_config, // 🆕 返回知识库配置
|
||||
versions,
|
||||
createdAt: template.created_at,
|
||||
updatedAt: template.updated_at,
|
||||
@@ -225,6 +228,14 @@ export async function publishPrompt(
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const userId = (request as any).user?.userId;
|
||||
const userRole = (request as any).user?.role;
|
||||
|
||||
console.log(`[PromptController] publishPrompt 请求:`, {
|
||||
code,
|
||||
userId,
|
||||
userRole,
|
||||
hasUser: !!(request as any).user,
|
||||
});
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
@@ -372,13 +383,17 @@ export async function getDebugStatus(
|
||||
/**
|
||||
* 测试渲染 Prompt
|
||||
* POST /api/admin/prompts/test-render
|
||||
*
|
||||
* 支持知识库注入测试:
|
||||
* - 传入 code 参数,会自动加载该 Prompt 的知识库配置
|
||||
* - 传入 userQuery 参数,用于 RAG 模式的向量检索
|
||||
*/
|
||||
export async function testRender(
|
||||
request: FastifyRequest<{ Body: TestRenderBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { content, variables } = request.body;
|
||||
const { content, variables, code, userQuery } = request.body;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
@@ -388,8 +403,20 @@ export async function testRender(
|
||||
// 校验变量
|
||||
const validation = promptService.validateVariables(content, variables);
|
||||
|
||||
// 🆕 如果传入了 code,进行知识库增强
|
||||
let enhancedVariables = variables;
|
||||
let knowledgeInjected = false;
|
||||
|
||||
if (code) {
|
||||
console.log(`[PromptController] testRender with knowledge enhancement for ${code}`);
|
||||
enhancedVariables = await promptService.getEnhancedVariables(code, variables, userQuery);
|
||||
// 检查是否有知识库内容被注入
|
||||
const targetVar = 'context'; // 默认目标变量
|
||||
knowledgeInjected = enhancedVariables[targetVar] !== variables[targetVar];
|
||||
}
|
||||
|
||||
// 渲染
|
||||
const rendered = promptService.render(content, variables);
|
||||
const rendered = promptService.render(content, enhancedVariables);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
@@ -397,6 +424,7 @@ export async function testRender(
|
||||
rendered,
|
||||
extractedVariables: extractedVars,
|
||||
validation,
|
||||
knowledgeInjected, // 🆕 告知前端是否成功注入了知识库内容
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
@@ -438,4 +466,58 @@ export async function invalidateCache(
|
||||
}
|
||||
}
|
||||
|
||||
// 知识库配置请求体类型
|
||||
interface SaveKnowledgeConfigBody {
|
||||
knowledge_config: {
|
||||
enabled: boolean;
|
||||
kb_codes: string[];
|
||||
injection_mode: 'FULL' | 'RAG';
|
||||
target_variable: string;
|
||||
query_field?: string;
|
||||
top_k?: number;
|
||||
min_score?: number;
|
||||
max_tokens?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存知识库配置
|
||||
* PUT /api/admin/prompts/:code/knowledge-config
|
||||
*
|
||||
* 需要权限: prompt:edit
|
||||
*/
|
||||
export async function saveKnowledgeConfig(
|
||||
request: FastifyRequest<{ Params: GetPromptParams; Body: SaveKnowledgeConfigBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const { knowledge_config } = request.body;
|
||||
|
||||
// 更新 prompt_templates 表的 knowledge_config 字段
|
||||
const updated = await prisma.prompt_templates.update({
|
||||
where: { code },
|
||||
data: {
|
||||
knowledge_config: knowledge_config as any,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[PromptController] 知识库配置已保存: ${code}`, knowledge_config);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
code,
|
||||
knowledge_config: updated.knowledge_config,
|
||||
message: `Knowledge config saved for ${code}`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] saveKnowledgeConfig error:', error);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to save knowledge config',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getDebugStatus,
|
||||
testRender,
|
||||
invalidateCache,
|
||||
saveKnowledgeConfig,
|
||||
} from './prompt.controller.js';
|
||||
import { authenticate, requirePermission } from '../auth/auth.middleware.js';
|
||||
|
||||
@@ -224,6 +225,41 @@ export async function promptRoutes(fastify: FastifyInstance) {
|
||||
preHandler: [authenticate, requirePermission('prompt:edit')],
|
||||
handler: invalidateCache,
|
||||
});
|
||||
|
||||
// 保存知识库配置(需要 prompt:edit)
|
||||
fastify.put('/:code/knowledge-config', {
|
||||
schema: {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
},
|
||||
required: ['code'],
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
knowledge_config: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
kb_codes: { type: 'array', items: { type: 'string' } },
|
||||
injection_mode: { type: 'string', enum: ['FULL', 'RAG'] },
|
||||
target_variable: { type: 'string' },
|
||||
query_field: { type: 'string' },
|
||||
top_k: { type: 'number' },
|
||||
min_score: { type: 'number' },
|
||||
max_tokens: { type: 'number' },
|
||||
},
|
||||
required: ['enabled', 'kb_codes', 'injection_mode', 'target_variable'],
|
||||
},
|
||||
},
|
||||
required: ['knowledge_config'],
|
||||
},
|
||||
},
|
||||
preHandler: [authenticate, requirePermission('prompt:edit')],
|
||||
handler: saveKnowledgeConfig,
|
||||
});
|
||||
}
|
||||
|
||||
export default promptRoutes;
|
||||
|
||||
@@ -17,8 +17,10 @@ import type {
|
||||
ModelConfig,
|
||||
VariableValidation,
|
||||
DebugState,
|
||||
KnowledgeConfig,
|
||||
} from './prompt.types.js';
|
||||
import { getFallbackPrompt } from './prompt.fallbacks.js';
|
||||
import { getVectorSearchService } from '../rag/index.js';
|
||||
|
||||
// 默认模型配置
|
||||
const DEFAULT_MODEL_CONFIG: ModelConfig = {
|
||||
@@ -48,16 +50,20 @@ export class PromptService {
|
||||
* - 如果用户开启了该模块的调试模式,返回 DRAFT
|
||||
* - 否则返回 ACTIVE
|
||||
*
|
||||
* 知识库增强:
|
||||
* - 如果配置了 knowledge_config,自动注入知识库内容到指定变量
|
||||
* - 支持 FULL(全量)和 RAG(检索)两种模式
|
||||
*
|
||||
* @param code Prompt 代码,如 'RVW_EDITORIAL'
|
||||
* @param variables 模板变量
|
||||
* @param options 选项(userId 用于判断调试模式)
|
||||
* @param options 选项(userId 用于判断调试模式,userQuery 用于 RAG 检索)
|
||||
*/
|
||||
async get(
|
||||
code: string,
|
||||
variables: Record<string, any> = {},
|
||||
options: GetPromptOptions = {}
|
||||
): Promise<RenderedPrompt> {
|
||||
const { userId, skipCache = false } = options;
|
||||
const { userId, skipCache = false, userQuery } = options;
|
||||
|
||||
try {
|
||||
// 🔍 诊断日志:检查调试状态
|
||||
@@ -79,7 +85,12 @@ export class PromptService {
|
||||
const isDebugging = userId ? this.isDebugging(userId, code) : false;
|
||||
console.log(` isDebugging: ${isDebugging}`);
|
||||
|
||||
// 2. 获取 Prompt 版本
|
||||
// 2. 获取 Prompt 模板(包含 knowledge_config)
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
// 3. 获取 Prompt 版本
|
||||
let version;
|
||||
if (isDebugging) {
|
||||
// 调试模式:优先获取 DRAFT
|
||||
@@ -100,7 +111,7 @@ export class PromptService {
|
||||
console.log(` → 返回版本: v${version.version} (${version.status})`);
|
||||
}
|
||||
|
||||
// 3. 如果数据库获取失败,使用兜底
|
||||
// 4. 如果数据库获取失败,使用兜底
|
||||
if (!version) {
|
||||
console.warn(`[PromptService] Fallback to hardcoded prompt: ${code}`);
|
||||
const fallback = getFallbackPrompt(code);
|
||||
@@ -115,8 +126,20 @@ export class PromptService {
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 渲染模板
|
||||
const content = this.render(version.content, variables);
|
||||
// 5. 🆕 知识库增强:注入知识库内容
|
||||
console.log(` → 知识库配置:`, JSON.stringify(template?.knowledge_config));
|
||||
const enhancedVariables = await this.enhanceWithKnowledge(
|
||||
template?.knowledge_config as KnowledgeConfig | null,
|
||||
variables,
|
||||
userQuery
|
||||
);
|
||||
console.log(` → 增强后变量 keys:`, Object.keys(enhancedVariables));
|
||||
if (enhancedVariables.context) {
|
||||
console.log(` → context 长度: ${enhancedVariables.context.length} 字符`);
|
||||
}
|
||||
|
||||
// 6. 渲染模板
|
||||
const content = this.render(version.content, enhancedVariables);
|
||||
const modelConfig = (version.model_config as ModelConfig) || DEFAULT_MODEL_CONFIG;
|
||||
|
||||
return {
|
||||
@@ -143,6 +166,198 @@ export class PromptService {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 知识库增强 ====================
|
||||
|
||||
/**
|
||||
* 🆕 公共方法:获取指定 Prompt 的知识库配置并增强变量
|
||||
* 用于测试渲染功能
|
||||
*/
|
||||
async getEnhancedVariables(
|
||||
code: string,
|
||||
variables: Record<string, any>,
|
||||
userQuery?: string
|
||||
): Promise<Record<string, any>> {
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
select: { knowledge_config: true },
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
console.log(`[PromptService] Prompt ${code} 不存在,跳过知识库增强`);
|
||||
return variables;
|
||||
}
|
||||
|
||||
return this.enhanceWithKnowledge(
|
||||
template.knowledge_config as KnowledgeConfig | null,
|
||||
variables,
|
||||
userQuery
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 知识库增强:根据配置注入知识库内容
|
||||
*/
|
||||
private async enhanceWithKnowledge(
|
||||
config: KnowledgeConfig | null,
|
||||
variables: Record<string, any>,
|
||||
userQuery?: string
|
||||
): Promise<Record<string, any>> {
|
||||
// 详细日志:诊断知识库配置
|
||||
console.log(`[PromptService] enhanceWithKnowledge 检查:`);
|
||||
console.log(` config 存在: ${!!config}`);
|
||||
if (config) {
|
||||
console.log(` config.enabled: ${config.enabled}`);
|
||||
console.log(` config.kb_codes: ${JSON.stringify(config.kb_codes)}`);
|
||||
console.log(` config.injection_mode: ${config.injection_mode}`);
|
||||
}
|
||||
|
||||
// 未启用知识库增强
|
||||
if (!config || !config.enabled || !config.kb_codes || config.kb_codes.length === 0) {
|
||||
console.log(` → 跳过知识库增强(未启用或未配置)`);
|
||||
return variables;
|
||||
}
|
||||
|
||||
console.log(`[PromptService] 📚 知识库增强已启用`);
|
||||
console.log(` 知识库: ${config.kb_codes.join(', ')}`);
|
||||
console.log(` 模式: ${config.injection_mode}`);
|
||||
console.log(` 目标变量: ${config.target_variable || 'context'}`);
|
||||
|
||||
try {
|
||||
let knowledgeContent: string;
|
||||
|
||||
if (config.injection_mode === 'FULL') {
|
||||
// FULL 模式:加载知识库全文
|
||||
knowledgeContent = await this.loadFullKnowledge(config.kb_codes, config.max_tokens);
|
||||
} else {
|
||||
// RAG 模式:向量检索
|
||||
const query = userQuery || variables[config.query_field || 'question'] || '';
|
||||
if (!query) {
|
||||
console.warn('[PromptService] RAG 模式但未提供查询词,跳过知识库增强');
|
||||
return variables;
|
||||
}
|
||||
knowledgeContent = await this.ragSearch(config.kb_codes, query, config.top_k, config.min_score);
|
||||
}
|
||||
|
||||
// 注入到目标变量
|
||||
const targetVariable = config.target_variable || 'context';
|
||||
const enhancedVariables = { ...variables };
|
||||
|
||||
// 如果目标变量已有值,追加知识库内容
|
||||
if (enhancedVariables[targetVariable]) {
|
||||
enhancedVariables[targetVariable] = `${enhancedVariables[targetVariable]}\n\n--- 知识库参考 ---\n\n${knowledgeContent}`;
|
||||
} else {
|
||||
enhancedVariables[targetVariable] = knowledgeContent;
|
||||
}
|
||||
|
||||
console.log(`[PromptService] ✅ 知识库内容已注入到 ${targetVariable},长度: ${knowledgeContent.length} 字符`);
|
||||
return enhancedVariables;
|
||||
} catch (error) {
|
||||
console.error('[PromptService] 知识库增强失败,继续使用原始变量', error);
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 FULL 模式:加载知识库全文
|
||||
*/
|
||||
private async loadFullKnowledge(kbCodes: string[], maxTokens?: number): Promise<string> {
|
||||
console.log(`[PromptService] FULL 模式:加载知识库 ${kbCodes.join(', ')} 全文`);
|
||||
|
||||
// 获取知识库 ID 列表
|
||||
const kbs = await this.prisma.system_knowledge_bases.findMany({
|
||||
where: { code: { in: kbCodes }, status: 'active' },
|
||||
select: { id: true, code: true, name: true },
|
||||
});
|
||||
|
||||
if (kbs.length === 0) {
|
||||
console.warn(`[PromptService] 未找到有效的知识库: ${kbCodes.join(', ')}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const kbIds = kbs.map(kb => kb.id);
|
||||
|
||||
// 查询 EKB 知识库的所有文档块
|
||||
const chunks = await this.prisma.ekbChunk.findMany({
|
||||
where: {
|
||||
document: {
|
||||
kbId: { in: kbIds },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
content: true,
|
||||
document: {
|
||||
select: { filename: true },
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ documentId: 'asc' },
|
||||
{ chunkIndex: 'asc' },
|
||||
],
|
||||
});
|
||||
|
||||
// 组装全文内容(按字符数估算 Token,约 2 字符/Token)
|
||||
let totalChars = 0;
|
||||
const contents: string[] = [];
|
||||
const charLimit = (maxTokens || 50000) * 2; // Token 限制转换为字符限制
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const chars = chunk.content.length;
|
||||
if (totalChars + chars > charLimit) {
|
||||
console.warn(`[PromptService] FULL 模式达到字符限制 (${charLimit}),截断`);
|
||||
break;
|
||||
}
|
||||
contents.push(chunk.content);
|
||||
totalChars += chars;
|
||||
}
|
||||
|
||||
const estimatedTokens = Math.round(totalChars / 2);
|
||||
console.log(`[PromptService] FULL 模式加载完成: ${chunks.length} 块, 约 ${estimatedTokens} tokens`);
|
||||
return contents.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 RAG 模式:向量检索
|
||||
*/
|
||||
private async ragSearch(
|
||||
kbCodes: string[],
|
||||
query: string,
|
||||
topK: number = 5,
|
||||
minScore: number = 0.5
|
||||
): Promise<string> {
|
||||
console.log(`[PromptService] RAG 模式:检索 "${query.substring(0, 50)}..." (Top ${topK})`);
|
||||
|
||||
// 获取知识库 ID 列表
|
||||
const kbs = await this.prisma.system_knowledge_bases.findMany({
|
||||
where: { code: { in: kbCodes }, status: 'active' },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (kbs.length === 0) {
|
||||
console.warn(`[PromptService] 未找到有效的知识库: ${kbCodes.join(', ')}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// 使用 VectorSearchService 检索
|
||||
const vectorService = getVectorSearchService(this.prisma);
|
||||
const results: string[] = [];
|
||||
|
||||
// 对每个知识库进行检索
|
||||
for (const kb of kbs) {
|
||||
const searchResults = await vectorService.vectorSearch(query, {
|
||||
topK,
|
||||
minScore,
|
||||
filter: { kbId: kb.id },
|
||||
});
|
||||
|
||||
for (const result of searchResults) {
|
||||
results.push(`[相似度: ${(result.score * 100).toFixed(1)}%]\n${result.content}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PromptService] RAG 模式检索完成: ${results.length} 条结果`);
|
||||
return results.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ACTIVE 版本(带缓存)
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,25 @@
|
||||
// Prompt 状态
|
||||
export type PromptStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
|
||||
|
||||
// 知识库注入模式
|
||||
export type InjectionMode = 'FULL' | 'RAG';
|
||||
|
||||
// 知识库增强配置
|
||||
export interface KnowledgeConfig {
|
||||
enabled: boolean; // 是否启用知识库增强
|
||||
kb_codes: string[]; // 关联的知识库编码列表
|
||||
injection_mode: InjectionMode; // 注入模式:FULL 全量 | RAG 检索
|
||||
target_variable: string; // 注入的目标变量,默认 'context'
|
||||
|
||||
// RAG 模式配置
|
||||
query_field?: string; // 用于检索的变量名,默认使用 userQuery
|
||||
top_k?: number; // 检索返回数量,默认 5
|
||||
min_score?: number; // 最低相似度分数,默认 0.5
|
||||
|
||||
// FULL 模式配置
|
||||
max_tokens?: number; // 最大 Token 限制,防止超限
|
||||
}
|
||||
|
||||
// 模型配置
|
||||
export interface ModelConfig {
|
||||
model: string; // 模型名称,如 'deepseek-v3'
|
||||
@@ -58,6 +77,7 @@ export interface DebugState {
|
||||
export interface GetPromptOptions {
|
||||
userId?: string; // 用于判断调试模式
|
||||
skipCache?: boolean; // 跳过缓存
|
||||
userQuery?: string; // 用户查询(用于 RAG 模式检索)
|
||||
}
|
||||
|
||||
// 变量校验结果
|
||||
|
||||
Reference in New Issue
Block a user