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

@@ -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());

View 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());

View 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();

View 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());

View 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());

View 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());

View File

@@ -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',
});
}
}

View File

@@ -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;

View File

@@ -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 版本(带缓存)
*/

View File

@@ -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 模式检索)
}
// 变量校验结果