fix(admin): Fix Prompt management list not showing version info and add debug diagnostics

Summary:
- Fix Prompt list API response schema missing activeVersion and draftVersion fields
- Fastify was filtering out undefined schema fields, causing version columns to show empty
- Add detailed diagnostic logging for Prompt debug mode troubleshooting
- Verify debug mode works correctly (DRAFT version is used when debug enabled)

Changes:
- backend/src/common/prompt/prompt.routes.ts: Add activeVersion and draftVersion to response schema
- backend/src/common/prompt/prompt.service.ts: Add diagnostic logs for setDebugMode and get methods
- PKB module: Various authentication and document handling fixes from previous session

Tested: Debug mode verified working - v2 DRAFT version correctly loaded when debug enabled
This commit is contained in:
2026-01-13 22:22:10 +08:00
parent 4088275290
commit 4ed67a8846
272 changed files with 1382 additions and 161 deletions

View File

@@ -0,0 +1,281 @@
/**
* PKB模块 - Chat控制器
*
* 知识库问答功能(全文阅读、逐篇精读模式)
* 强制要求用户认证
*
* 简化版不保存对话历史PKB的全文阅读/逐篇精读是一次性问答)
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { ModelType } from '../../../common/llm/adapters/types.js';
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
/**
* 引用信息接口
*/
interface Citation {
id: number;
fileName: string;
position: number;
score: number;
content: string;
}
/**
* 提取文本片段(用于引用上下文)
*/
function extractContextPreview(text: string, maxLength: number = 100): string {
if (!text) return '';
const cleaned = text.replace(/\s+/g, ' ').trim();
if (cleaned.length <= maxLength) {
return cleaned;
}
const truncated = cleaned.substring(0, maxLength);
const lastPunctuation = Math.max(
truncated.lastIndexOf('。'),
truncated.lastIndexOf(''),
truncated.lastIndexOf(''),
truncated.lastIndexOf('.'),
truncated.lastIndexOf('!'),
truncated.lastIndexOf('?')
);
if (lastPunctuation > maxLength * 0.5) {
return truncated.substring(0, lastPunctuation + 1);
}
return truncated + '...';
}
/**
* 格式化引用清单
*/
function formatCitations(citations: Citation[]): string {
if (citations.length === 0) return '';
let result = '\n\n---\n\n📚 **参考文献**\n\n';
for (const cite of citations) {
const scorePercent = (cite.score * 100).toFixed(0);
const preview = extractContextPreview(cite.content, 100);
result += `<span id="citation-detail-${cite.id}">[${cite.id}]</span> 📄 **${cite.fileName}** - 第${cite.position}段 (相关度${scorePercent}%)\n`;
result += ` "${preview}"\n\n`;
}
return result;
}
interface SendChatMessageBody {
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
documentIds?: string[];
fullTextDocumentIds?: string[];
}
/**
* 发送消息(流式输出)
*
* POST /api/v2/pkb/chat/stream
*/
export async function sendMessageStream(
request: FastifyRequest<{ Body: SendChatMessageBody }>,
reply: FastifyReply
) {
try {
// 从认证中获取userId已由authenticate中间件验证
const userId = request.user!.userId;
const { content, modelType, knowledgeBaseIds, fullTextDocumentIds } = request.body;
logger.info('[PKB Chat] 收到对话请求', {
userId,
modelType,
knowledgeBaseIds: knowledgeBaseIds || [],
fullTextDocumentIds: fullTextDocumentIds || [],
});
// 验证modelType
const validModels = ['deepseek-v3', 'qwen3-72b', 'qwen-long', 'gemini-pro'];
if (!validModels.includes(modelType)) {
reply.code(400).send({
success: false,
message: `不支持的模型类型: ${modelType}`,
});
return;
}
// 检索知识库上下文
let knowledgeBaseContext = '';
const allCitations: Citation[] = [];
// 全文阅读模式
if (fullTextDocumentIds && fullTextDocumentIds.length > 0) {
logger.info('[PKB Chat] 全文阅读模式', { documentCount: fullTextDocumentIds.length });
try {
const documents = await prisma.document.findMany({
where: {
id: { in: fullTextDocumentIds },
},
select: {
id: true,
filename: true,
extractedText: true,
tokensCount: true,
},
orderBy: {
filename: 'asc',
},
});
const validDocuments = documents.filter(doc => doc.extractedText && doc.extractedText.trim().length > 0);
if (validDocuments.length === 0) {
logger.warn('[PKB Chat] 所有文档都没有提取文本');
}
const fullTextParts: string[] = [];
for (let i = 0; i < validDocuments.length; i++) {
const doc = validDocuments[i];
const docNumber = i + 1;
allCitations.push({
id: docNumber,
fileName: doc.filename,
position: 0,
score: 1.0,
content: doc.extractedText?.substring(0, 200) || '(无内容)',
});
fullTextParts.push(
`【文献${docNumber}${doc.filename}\n\n${doc.extractedText}`
);
}
knowledgeBaseContext = fullTextParts.join('\n\n---\n\n');
const totalTokens = validDocuments.reduce((sum, doc) => sum + (doc.tokensCount || 0), 0);
logger.info('[PKB Chat] 全文上下文已组装', {
totalDocuments: validDocuments.length,
totalCharacters: knowledgeBaseContext.length,
totalTokens,
});
// Token限制检查
const QWEN_LONG_INPUT_LIMIT = 1000000;
const SYSTEM_OVERHEAD = 10000;
const SAFE_INPUT_LIMIT = QWEN_LONG_INPUT_LIMIT - SYSTEM_OVERHEAD;
if (totalTokens > SAFE_INPUT_LIMIT) {
const errorMsg = `输入Token数量 (${totalTokens}) 超出限制 (${SAFE_INPUT_LIMIT})。请减少文献数量。`;
logger.error('[PKB Chat] Token超限', { totalTokens, limit: SAFE_INPUT_LIMIT });
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
reply.raw.write(`data: ${JSON.stringify({
content: `\n\n⚠ **Token数量超限**\n\n${errorMsg}`,
role: 'assistant',
error: true,
})}\n\n`);
reply.raw.write('data: [DONE]\n\n');
return reply.raw.end();
}
} catch (error) {
logger.error('[PKB Chat] 加载文献全文失败:', error);
}
}
// 组装消息
let systemPrompt = '你是一个专业、友好的AI助手。';
if (fullTextDocumentIds && fullTextDocumentIds.length > 0) {
systemPrompt = '你是一个专业的学术文献分析助手。用户会提供多篇文献的完整全文每篇文献用【文献N文件名】标记。请认真阅读所有文献进行深入的综合分析。在回答时请引用具体文献使用【文献N】格式。';
}
const messages: any[] = [
{ role: 'system', content: systemPrompt },
];
let userContent = content;
if (knowledgeBaseContext) {
if (fullTextDocumentIds && fullTextDocumentIds.length > 0) {
userContent = `${content}\n\n## 参考资料(文献全文)\n\n${knowledgeBaseContext}`;
} else {
userContent = `${content}\n\n## 参考资料\n\n${knowledgeBaseContext}`;
}
}
messages.push({ role: 'user', content: userContent });
// 设置SSE响应头
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
// 流式输出
const adapter = LLMFactory.getAdapter(modelType);
let fullContent = '';
const maxOutputTokens = fullTextDocumentIds && fullTextDocumentIds.length > 0 ? 6000 : 2000;
logger.info('[PKB Chat] 开始LLM调用', { model: modelType, maxOutputTokens });
for await (const chunk of adapter.chatStream(messages, {
temperature: 0.7,
maxTokens: maxOutputTokens,
})) {
fullContent += chunk.content;
reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
// 追加引用清单
if (allCitations.length > 0) {
const citationsText = formatCitations(allCitations);
fullContent += citationsText;
reply.raw.write(`data: ${JSON.stringify({ content: citationsText, role: 'assistant' })}\n\n`);
}
reply.raw.write('data: [DONE]\n\n');
reply.raw.end();
logger.info('[PKB Chat] 对话完成', {
userId,
responseLength: fullContent.length,
citationsCount: allCitations.length,
});
} catch (error: any) {
logger.error('[PKB Chat] 错误:', error);
// 如果还没有发送响应头返回JSON错误
if (!reply.raw.headersSent) {
reply.code(500).send({
success: false,
message: error.message || '服务器错误',
});
} else {
// 已经发送了SSE头尝试发送错误信息
try {
reply.raw.write(`data: ${JSON.stringify({ error: true, message: error.message })}\n\n`);
reply.raw.write('data: [DONE]\n\n');
reply.raw.end();
} catch (e) {
// 忽略
}
}
}
}

View File

@@ -0,0 +1,17 @@
/**
* PKB模块 - Chat路由
*
* 知识库问答功能(全文阅读、逐篇精读模式)
* 所有路由都需要认证
*/
import { FastifyInstance } from 'fastify';
import { sendMessageStream } from '../controllers/chatController.js';
import { authenticate, requireModule } from '../../../common/auth/auth.middleware.js';
export default async function chatRoutes(fastify: FastifyInstance) {
// 发送消息(流式输出)
fastify.post('/stream', {
preHandler: [authenticate, requireModule('PKB')]
}, sendMessageStream as any);
}

View File

@@ -52,3 +52,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -4,6 +4,7 @@
import { FastifyInstance } from 'fastify';
import knowledgeBaseRoutes from './knowledgeBases.js';
import batchRoutes from './batchRoutes.js';
import chatRoutes from './chatRoutes.js';
import healthRoutes from './health.js';
export default async function pkbRoutes(fastify: FastifyInstance) {
@@ -15,5 +16,8 @@ export default async function pkbRoutes(fastify: FastifyInstance) {
// 注册批处理路由
fastify.register(batchRoutes, { prefix: '/batch-tasks' });
// 注册Chat路由全文阅读、逐篇精读
fastify.register(chatRoutes, { prefix: '/chat' });
}

View File

@@ -39,6 +39,18 @@ export async function uploadDocument(
throw new Error('Document limit exceeded. Maximum 50 documents per knowledge base');
}
// 3. 检查文档是否已存在(同名文件查重)
const existingDoc = await prisma.document.findFirst({
where: {
kbId,
filename: filename,
},
});
if (existingDoc) {
throw new Error(`文档 "${filename}" 已存在,请勿重复上传`);
}
// 3. 在数据库中创建文档记录状态uploading
const document = await prisma.document.create({
data: {