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

@@ -43,5 +43,7 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2

View File

@@ -272,6 +272,8 @@

View File

@@ -221,3 +221,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -150,3 +150,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -51,3 +51,5 @@

View File

@@ -311,3 +311,5 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts

View File

@@ -173,3 +173,5 @@ npm run dev

View File

@@ -50,3 +50,5 @@ main()

View File

@@ -44,3 +44,5 @@ main()

View File

@@ -39,3 +39,5 @@ main()

View File

@@ -71,3 +71,5 @@ main()

View File

@@ -34,3 +34,5 @@ main()

View File

@@ -75,3 +75,5 @@ main()

View File

@@ -22,3 +22,5 @@ main()

View File

@@ -110,3 +110,5 @@ main()

View File

@@ -81,3 +81,5 @@ main()

View File

@@ -67,3 +67,5 @@ main()

View File

@@ -109,3 +109,5 @@ main()

View File

@@ -20,3 +20,5 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -52,3 +52,5 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -67,6 +67,8 @@ WHERE table_schema = 'dc_schema'

View File

@@ -105,6 +105,8 @@ ORDER BY ordinal_position;

View File

@@ -118,6 +118,8 @@ runMigration()

View File

@@ -52,6 +52,8 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名

View File

@@ -79,6 +79,8 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

View File

@@ -119,6 +119,8 @@ Write-Host ""

View File

@@ -229,6 +229,8 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -29,3 +29,5 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (

View File

@@ -103,3 +103,5 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS

View File

@@ -248,6 +248,8 @@ checkDCTables();

View File

@@ -4,3 +4,5 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;

View File

@@ -200,6 +200,8 @@ createAiHistoryTable()

View File

@@ -187,6 +187,8 @@ createToolCTable()

View File

@@ -184,6 +184,8 @@ createToolCTable()

View File

@@ -114,3 +114,5 @@ main()

View File

@@ -334,3 +334,5 @@ runTests().catch(error => {

View File

@@ -80,3 +80,5 @@ testAPI().catch(console.error);

View File

@@ -299,3 +299,5 @@ verifySchemas()

View File

@@ -187,3 +187,5 @@ export const jwtService = new JWTService();

View File

@@ -316,6 +316,8 @@ export function getBatchItems<T>(

View File

@@ -101,3 +101,5 @@ export function getAllFallbackCodes(): string[] {

View File

@@ -19,6 +19,16 @@ import {
import { authenticate, requirePermission } from '../auth/auth.middleware.js';
// Schema 定义
const versionSchema = {
type: 'object',
nullable: true,
properties: {
version: { type: 'number' },
status: { type: 'string' },
createdAt: { type: 'string' },
},
};
const listPromptsSchema = {
querystring: {
type: 'object',
@@ -42,14 +52,9 @@ const listPromptsSchema = {
module: { type: 'string' },
description: { type: 'string' },
variables: { type: 'array', items: { type: 'string' } },
latestVersion: {
type: 'object',
properties: {
version: { type: 'number' },
status: { type: 'string' },
createdAt: { type: 'string' },
},
},
activeVersion: versionSchema, // 🆕 生产版本
draftVersion: versionSchema, // 🆕 草稿版本
latestVersion: versionSchema, // 最新版本
updatedAt: { type: 'string' },
},
},

View File

@@ -60,22 +60,45 @@ export class PromptService {
const { userId, skipCache = false } = options;
try {
// 🔍 诊断日志:检查调试状态
console.log(`[PromptService.get] 获取 Prompt: ${code}`);
console.log(` userId: ${userId || '(未提供)'}`);
console.log(` 当前调试用户数: ${this.debugStates.size}`);
if (userId && this.debugStates.size > 0) {
const state = this.debugStates.get(userId);
if (state) {
console.log(` 用户调试状态: 模块=${Array.from(state.modules).join(',')}`);
} else {
console.log(` 用户调试状态: 未找到 (用户ID不在调试列表中)`);
console.log(` 调试列表中的用户: ${Array.from(this.debugStates.keys()).join(', ')}`);
}
}
// 1. 判断是否处于调试模式
const isDebugging = userId ? this.isDebugging(userId, code) : false;
console.log(` isDebugging: ${isDebugging}`);
// 2. 获取 Prompt 版本
let version;
if (isDebugging) {
// 调试模式:优先获取 DRAFT
console.log(` → 调试模式,获取 DRAFT 版本`);
version = await this.getDraftVersion(code);
if (!version) {
// 没有 DRAFT降级到 ACTIVE
console.log(` → DRAFT 不存在,降级到 ACTIVE`);
version = await this.getActiveVersion(code, skipCache);
}
} else {
// 正常模式:获取 ACTIVE
console.log(` → 正常模式,获取 ACTIVE 版本`);
version = await this.getActiveVersion(code, skipCache);
}
if (version) {
console.log(` → 返回版本: v${version.version} (${version.status})`);
}
// 3. 如果数据库获取失败,使用兜底
if (!version) {
@@ -248,16 +271,24 @@ export class PromptService {
* @param enabled 是否开启
*/
setDebugMode(userId: string, modules: string[], enabled: boolean): void {
console.log(`\n🔧 [PromptService.setDebugMode]`);
console.log(` userId: ${userId}`);
console.log(` modules: [${modules.join(', ')}]`);
console.log(` enabled: ${enabled}`);
if (enabled) {
this.debugStates.set(userId, {
userId,
modules: new Set(modules),
enabledAt: new Date(),
});
console.log(`[PromptService] Debug mode enabled for user ${userId}, modules: [${modules.join(', ')}]`);
console.log(` ✅ 调试模式已开启`);
console.log(` 当前调试用户数: ${this.debugStates.size}`);
console.log(` 所有调试用户: ${Array.from(this.debugStates.keys()).join(', ')}`);
} else {
this.debugStates.delete(userId);
console.log(`[PromptService] Debug mode disabled for user ${userId}`);
console.log(` ✅ 调试模式已关闭`);
console.log(` 当前调试用户数: ${this.debugStates.size}`);
}
}

View File

@@ -70,3 +70,5 @@ export interface VariableValidation {

View File

@@ -85,8 +85,8 @@ export class ChatController {
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = 'user-mock-001';
// 从JWT token获取userId,如果没有认证则使用默认用户
const userId = request.user?.userId || 'user-mock-001';
const { content, modelType, knowledgeBaseIds, documentIds, fullTextDocumentIds, conversationId } = request.body;

View File

@@ -1,15 +1,16 @@
import { FastifyInstance } from 'fastify';
import { chatController } from '../controllers/chatController.js';
import { optionalAuthenticate } from '../../common/auth/auth.middleware.js';
export async function chatRoutes(fastify: FastifyInstance) {
// 发送消息(流式输出)
fastify.post('/chat/stream', chatController.sendMessageStream.bind(chatController));
// 发送消息(流式输出)- 使用可选认证优先使用真实用户ID
fastify.post('/chat/stream', { preHandler: [optionalAuthenticate] }, chatController.sendMessageStream.bind(chatController));
// 获取对话列表
fastify.get('/chat/conversations', chatController.getConversations.bind(chatController));
// 获取对话列表 - 使用可选认证
fastify.get('/chat/conversations', { preHandler: [optionalAuthenticate] }, chatController.getConversations.bind(chatController));
// 删除对话
fastify.delete('/chat/conversations/:id', chatController.deleteConversation.bind(chatController));
// 删除对话 - 使用可选认证
fastify.delete('/chat/conversations/:id', { preHandler: [optionalAuthenticate] }, chatController.deleteConversation.bind(chatController));
}

View File

@@ -76,3 +76,5 @@ export async function moduleRoutes(fastify: FastifyInstance) {
}

View File

@@ -106,3 +106,5 @@ export interface PaginatedResponse<T> {
}

View File

@@ -352,6 +352,8 @@ runTests().catch((error) => {

View File

@@ -331,6 +331,8 @@ Content-Type: application/json

View File

@@ -267,6 +267,8 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -217,6 +217,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -271,6 +271,8 @@ export const streamAIController = new StreamAIController();

View File

@@ -183,3 +183,5 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {

View File

@@ -117,3 +117,5 @@ checkTableStructure();

View File

@@ -104,3 +104,5 @@ checkProjectConfig().catch(console.error);

View File

@@ -86,3 +86,5 @@ main();

View File

@@ -543,3 +543,5 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback

View File

@@ -178,3 +178,5 @@ console.log('');

View File

@@ -495,3 +495,5 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -140,3 +140,5 @@ testDifyIntegration().catch(error => {

View File

@@ -167,5 +167,7 @@ testIitDatabase()

View File

@@ -155,3 +155,5 @@ if (hasError) {

View File

@@ -181,3 +181,5 @@ async function testUrlVerification() {

View File

@@ -262,3 +262,5 @@ main().catch((error) => {

View File

@@ -146,3 +146,5 @@ Write-Host ""

View File

@@ -237,5 +237,7 @@ export interface CachedProtocolRules {

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: {

View File

@@ -130,3 +130,5 @@ Content-Type: application/json

View File

@@ -115,3 +115,5 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v2/rvw/tasks/{taskId}" -Foregr

View File

@@ -29,3 +29,5 @@ export * from './services/utils.js';

View File

@@ -120,3 +120,5 @@ export function validateAgentSelection(agents: string[]): void {

View File

@@ -417,6 +417,8 @@ SET session_replication_role = 'origin';

View File

@@ -119,6 +119,8 @@ WHERE key = 'verify_test';

View File

@@ -262,6 +262,8 @@ verifyDatabase()

View File

@@ -52,6 +52,8 @@ export {}

View File

@@ -75,6 +75,8 @@ Write-Host "✅ 完成!" -ForegroundColor Green

View File

@@ -3,3 +3,5 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p

View File

@@ -166,3 +166,5 @@ DELETE {{baseUrl}}/api/v2/pkb/knowledge/knowledge-bases/{{testKbId}}

View File

@@ -362,6 +362,8 @@ runAdvancedTests().catch(error => {

View File

@@ -428,6 +428,8 @@ runAllTests()

View File

@@ -386,6 +386,8 @@ runAllTests()

View File

@@ -23,3 +23,5 @@ main()

View File

@@ -21,3 +21,5 @@ main()

View File

@@ -33,3 +33,5 @@ main()

View File

@@ -22,3 +22,5 @@ main()

View File

@@ -162,3 +162,5 @@ main()

View File

@@ -170,6 +170,8 @@ Set-Location ..

View File

@@ -612,6 +612,8 @@ async saveProcessedData(recordId, newData) {

View File

@@ -799,6 +799,8 @@ export const AsyncProgressBar: React.FC<AsyncProgressBarProps> = ({

View File

@@ -295,3 +295,5 @@ Level 3: 兜底Prompt缓存也失效

View File

@@ -215,3 +215,5 @@ ADMIN-运营管理端/

View File

@@ -314,3 +314,5 @@ INST-机构管理端/

View File

@@ -538,3 +538,5 @@ frontend-v2/src/modules/aia/

View File

@@ -566,3 +566,5 @@ Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

Some files were not shown because too many files have changed in this diff Show More