feat(admin): Complete Phase 3.5.1-3.5.4 Prompt Management System (83%)

Summary:
- Implement Prompt management infrastructure and core services
- Build admin portal frontend with light theme
- Integrate CodeMirror 6 editor for non-technical users

Phase 3.5.1: Infrastructure Setup
- Create capability_schema for Prompt storage
- Add prompt_templates and prompt_versions tables
- Add prompt:view/edit/debug/publish permissions
- Migrate RVW prompts to database (RVW_EDITORIAL, RVW_METHODOLOGY)

Phase 3.5.2: PromptService Core
- Implement gray preview logic (DRAFT for debuggers, ACTIVE for users)
- Module-level debug control (setDebugMode)
- Handlebars template rendering
- Variable extraction and validation (extractVariables, validateVariables)
- Three-level disaster recovery (database -> cache -> hardcoded fallback)

Phase 3.5.3: Management API
- 8 RESTful endpoints (/api/admin/prompts/*)
- Permission control (PROMPT_ENGINEER can edit, SUPER_ADMIN can publish)

Phase 3.5.4: Frontend Management UI
- Build admin portal architecture (AdminLayout, OrgLayout)
- Add route system (/admin/*, /org/*)
- Implement PromptListPage (filter, search, debug switch)
- Implement PromptEditor (CodeMirror 6 simplified for clinical users)
- Implement PromptEditorPage (edit, save, publish, test, version history)

Technical Details:
- Backend: 6 files, ~2044 lines (prompt.service.ts 596 lines)
- Frontend: 9 files, ~1735 lines (PromptEditorPage.tsx 399 lines)
- CodeMirror 6: Line numbers, auto-wrap, variable highlight, search, undo/redo
- Chinese-friendly: 15px font, 1.8 line-height, system fonts

Next Step: Phase 3.5.5 - Integrate RVW module with PromptService

Tested: Backend API tests passed (8/8), Frontend pending user testing
Status: Ready for Phase 3.5.5 RVW integration
This commit is contained in:
2026-01-11 21:25:16 +08:00
parent cdfbc9927a
commit 5523ef36ea
297 changed files with 15914 additions and 1266 deletions

View File

@@ -245,5 +245,6 @@ checkDCTables();

View File

@@ -0,0 +1,3 @@
-- Create capability_schema for Prompt Management System
CREATE SCHEMA IF NOT EXISTS capability_schema;

View File

@@ -197,5 +197,6 @@ createAiHistoryTable()

View File

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

View File

@@ -181,5 +181,6 @@ createToolCTable()

View File

@@ -0,0 +1,160 @@
/**
* RVW模块 Prompt 迁移脚本
*
* 将现有文件Prompt迁移到数据库
*
* 迁移内容:
* 1. RVW_EDITORIAL - 稿约规范性评估 (review_editorial_system.txt)
* 2. RVW_METHODOLOGY - 方法学质量评估 (review_methodology_system.txt)
* 3. RVW_TOPIC_SYSTEM - 选题评估系统提示 (topic_evaluation_system.txt)
* 4. RVW_TOPIC_USER - 选题评估用户模板 (topic_evaluation_user.txt)
*/
import { PrismaClient, PromptStatus } from '@prisma/client';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const prisma = new PrismaClient();
// 变量提取函数
function extractVariables(content: string): string[] {
const regex = /\{\{(\w+)\}\}/g;
const variables = new Set<string>();
let match;
while ((match = regex.exec(content)) !== null) {
// 排除 Handlebars 控制语句如 #if, /if
if (!match[1].startsWith('#') && !match[1].startsWith('/')) {
variables.add(match[1]);
}
}
return Array.from(variables);
}
// RVW Prompt 配置(只有 2 个)
// 注意topic_evaluation_* 是"选题评估"功能,不属于 RVW 审稿模块
const rvwPrompts = [
{
code: 'RVW_EDITORIAL',
name: '稿约规范性评估',
module: 'RVW',
description: '评估医学稿件是否符合期刊稿约规范包括文题、摘要、参考文献等11项标准。输出JSON格式的评分和建议。',
file: 'review_editorial_system.txt',
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'RVW_METHODOLOGY',
name: '方法学质量评估',
module: 'RVW',
description: '评估医学稿件的科研设计、统计学方法和统计分析质量共20个检查点。输出JSON格式的评分和建议。',
file: 'review_methodology_system.txt',
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
];
async function main() {
console.log('🚀 开始迁移 RVW Prompt 到数据库...\n');
const promptsDir = path.join(__dirname, '..', 'prompts');
for (const prompt of rvwPrompts) {
console.log(`📄 处理: ${prompt.code} (${prompt.name})`);
// 读取文件内容
const filePath = path.join(promptsDir, prompt.file);
if (!fs.existsSync(filePath)) {
console.log(` ⚠️ 文件不存在: ${filePath}`);
continue;
}
const content = fs.readFileSync(filePath, 'utf-8').trim();
const variables = extractVariables(content);
console.log(` 📝 内容长度: ${content.length} 字符`);
console.log(` 🔤 提取变量: [${variables.join(', ')}]`);
// 创建或更新模板
const template = await prisma.prompt_templates.upsert({
where: { code: prompt.code },
update: {
name: prompt.name,
description: prompt.description,
variables: variables.length > 0 ? variables : null,
updated_at: new Date(),
},
create: {
code: prompt.code,
name: prompt.name,
module: prompt.module,
description: prompt.description,
variables: variables.length > 0 ? variables : null,
},
});
// 检查是否已有 ACTIVE 版本
const existingActive = await prisma.prompt_versions.findFirst({
where: {
template_id: template.id,
status: PromptStatus.ACTIVE,
},
});
if (existingActive) {
console.log(` ✅ 已存在 ACTIVE 版本 (v${existingActive.version})`);
} else {
// 创建第一个 ACTIVE 版本
await prisma.prompt_versions.create({
data: {
template_id: template.id,
version: 1,
content: content,
model_config: prompt.modelConfig,
status: PromptStatus.ACTIVE,
changelog: '从文件迁移的初始版本',
created_by: 'system-migration',
},
});
console.log(` ✅ 创建 ACTIVE 版本 (v1)`);
}
console.log('');
}
// 验证结果
console.log('═══════════════════════════════════════════════════════');
console.log('📊 迁移结果验证\n');
const templates = await prisma.prompt_templates.findMany({
where: { module: 'RVW' },
include: {
versions: {
orderBy: { version: 'desc' },
take: 1,
},
},
});
console.log(`✅ 共迁移 ${templates.length} 个 RVW Prompt:\n`);
for (const t of templates) {
const latestVersion = t.versions[0];
console.log(` 📋 ${t.code}`);
console.log(` 名称: ${t.name}`);
console.log(` 变量: ${t.variables ? JSON.stringify(t.variables) : '无'}`);
console.log(` 最新版本: v${latestVersion?.version} (${latestVersion?.status})`);
console.log('');
}
console.log('✅ RVW Prompt 迁移完成!');
}
main()
.catch((error) => {
console.error('❌ 迁移失败:', error);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -0,0 +1,113 @@
/**
* Prompt管理系统初始化脚本
*
* 功能:
* 1. 创建 capability_schema
* 2. 添加 prompt:* 权限
* 3. 更新角色权限分配
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🚀 开始初始化 Prompt 管理系统...\n');
// 1. 创建 capability_schema
console.log('📁 Step 1: 创建 capability_schema...');
try {
await prisma.$executeRaw`CREATE SCHEMA IF NOT EXISTS capability_schema`;
console.log(' ✅ capability_schema 创建成功\n');
} catch (error) {
console.log(' ⚠️ capability_schema 可能已存在\n');
}
// 2. 添加 prompt:* 权限
console.log('🔐 Step 2: 添加 prompt:* 权限...');
const promptPermissions = [
{ code: 'prompt:view', name: '查看Prompt', description: '查看Prompt模板列表和详情', module: 'admin' },
{ code: 'prompt:edit', name: '编辑Prompt', description: '创建和修改Prompt草稿', module: 'admin' },
{ code: 'prompt:debug', name: '调试Prompt', description: '开启调试模式,在生产环境测试草稿', module: 'admin' },
{ code: 'prompt:publish', name: '发布Prompt', description: '将草稿发布为正式版', module: 'admin' },
];
for (const perm of promptPermissions) {
try {
await prisma.permissions.upsert({
where: { code: perm.code },
update: { name: perm.name, description: perm.description, module: perm.module },
create: perm,
});
console.log(`${perm.code}`);
} catch (error) {
console.log(` ⚠️ ${perm.code} 添加失败:`, error);
}
}
console.log('');
// 3. 获取权限ID
console.log('🔗 Step 3: 更新角色权限分配...');
const permissions = await prisma.permissions.findMany({
where: { code: { startsWith: 'prompt:' } },
});
const permissionMap = new Map(permissions.map(p => [p.code, p.id]));
// SUPER_ADMIN: 全部权限
const superAdminPermissions = ['prompt:view', 'prompt:edit', 'prompt:debug', 'prompt:publish'];
for (const permCode of superAdminPermissions) {
const permId = permissionMap.get(permCode);
if (permId) {
try {
await prisma.role_permissions.upsert({
where: {
role_permission_id: { role: 'SUPER_ADMIN', permission_id: permId },
},
update: {},
create: { role: 'SUPER_ADMIN', permission_id: permId },
});
} catch (error) {
// 可能已存在
}
}
}
console.log(' ✅ SUPER_ADMIN: prompt:view, prompt:edit, prompt:debug, prompt:publish');
// PROMPT_ENGINEER: 无 publish 权限
const promptEngineerPermissions = ['prompt:view', 'prompt:edit', 'prompt:debug'];
for (const permCode of promptEngineerPermissions) {
const permId = permissionMap.get(permCode);
if (permId) {
try {
await prisma.role_permissions.upsert({
where: {
role_permission_id: { role: 'PROMPT_ENGINEER', permission_id: permId },
},
update: {},
create: { role: 'PROMPT_ENGINEER', permission_id: permId },
});
} catch (error) {
// 可能已存在
}
}
}
console.log(' ✅ PROMPT_ENGINEER: prompt:view, prompt:edit, prompt:debug (无publish)');
console.log('');
// 4. 验证
console.log('✅ Prompt 管理系统初始化完成!\n');
const allPermissions = await prisma.permissions.findMany({
where: { code: { startsWith: 'prompt:' } },
});
console.log('📋 已添加的权限:');
allPermissions.forEach(p => console.log(` - ${p.code}: ${p.name}`));
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -330,3 +330,4 @@ runTests().catch(error => {

View File

@@ -0,0 +1,79 @@
/**
* 测试 Prompt 管理 API
*
* 启动后端后运行: npx tsx scripts/test-prompt-api.ts
*/
const BASE_URL = 'http://localhost:3001/api/admin/prompts';
async function testAPI() {
console.log('🧪 测试 Prompt 管理 API...\n');
// 1. 获取列表
console.log('═══════════════════════════════════════════════════════');
console.log('📋 Test 1: GET /api/admin/prompts\n');
const listRes = await fetch(BASE_URL);
const listData = await listRes.json();
console.log(` 状态: ${listRes.status}`);
console.log(` 总数: ${listData.total}`);
console.log(` Prompts:`);
for (const p of listData.data || []) {
console.log(` - ${p.code} (${p.name}) v${p.latestVersion?.version || 0}`);
}
// 2. 获取详情
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 2: GET /api/admin/prompts/RVW_EDITORIAL\n');
const detailRes = await fetch(`${BASE_URL}/RVW_EDITORIAL`);
const detailData = await detailRes.json();
console.log(` 状态: ${detailRes.status}`);
console.log(` Code: ${detailData.data?.code}`);
console.log(` Name: ${detailData.data?.name}`);
console.log(` 版本数: ${detailData.data?.versions?.length || 0}`);
// 3. 测试渲染
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 3: POST /api/admin/prompts/test-render\n');
const renderRes = await fetch(`${BASE_URL}/test-render`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: '你好,{{name}}!请评估标题:{{title}}',
variables: { name: '张三', title: '测试标题' },
}),
});
const renderData = await renderRes.json();
console.log(` 状态: ${renderRes.status}`);
console.log(` 渲染结果: ${renderData.data?.rendered}`);
console.log(` 提取变量: [${renderData.data?.extractedVariables?.join(', ')}]`);
console.log(` 校验结果: ${renderData.data?.validation?.isValid ? '✅ 通过' : '❌ 缺少变量'}`);
// 4. 按模块筛选
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 4: GET /api/admin/prompts?module=RVW\n');
const rvwRes = await fetch(`${BASE_URL}?module=RVW`);
const rvwData = await rvwRes.json();
console.log(` 状态: ${rvwRes.status}`);
console.log(` RVW模块Prompt数: ${rvwData.total}`);
for (const p of rvwData.data || []) {
console.log(` - ${p.code}`);
}
// 5. 获取调试状态无认证预期返回401
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 5: GET /api/admin/prompts/debug (无认证)\n');
const debugRes = await fetch(`${BASE_URL}/debug`);
const debugData = await debugRes.json();
console.log(` 状态: ${debugRes.status}`);
console.log(` 响应: ${JSON.stringify(debugData)}`);
console.log('\n✅ API 测试完成!');
}
testAPI().catch(console.error);

View File

@@ -0,0 +1,108 @@
/**
* 测试 PromptService
*/
import { PrismaClient } from '@prisma/client';
import { getPromptService } from '../src/common/prompt/index.js';
const prisma = new PrismaClient();
async function main() {
console.log('🧪 测试 PromptService...\n');
const promptService = getPromptService(prisma);
// 1. 测试获取 ACTIVE Prompt
console.log('═══════════════════════════════════════════════════════');
console.log('📋 Test 1: 获取 ACTIVE Prompt (RVW_EDITORIAL)\n');
const editorial = await promptService.get('RVW_EDITORIAL');
console.log(` 版本: v${editorial.version}`);
console.log(` 是否草稿: ${editorial.isDraft}`);
console.log(` 模型配置: ${JSON.stringify(editorial.modelConfig)}`);
console.log(` 内容长度: ${editorial.content.length} 字符`);
console.log(` 内容预览: ${editorial.content.substring(0, 100)}...`);
// 2. 测试变量提取
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 2: 变量提取\n');
const template = '请评估以下稿件:{{title}}\n作者{{author}}\n{{#if abstract}}摘要:{{abstract}}{{/if}}';
const variables = promptService.extractVariables(template);
console.log(` 模板: ${template}`);
console.log(` 提取的变量: [${variables.join(', ')}]`);
// 3. 测试变量校验
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 3: 变量校验\n');
const validation = promptService.validateVariables(template, { title: '测试标题' });
console.log(` 有效: ${validation.isValid}`);
console.log(` 缺失变量: [${validation.missingVariables.join(', ')}]`);
console.log(` 多余变量: [${validation.extraVariables.join(', ')}]`);
// 4. 测试模板渲染
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 4: 模板渲染\n');
const rendered = promptService.render(template, {
title: '测试论文标题',
author: '张三',
abstract: '这是摘要内容',
});
console.log(` 渲染结果:\n${rendered}`);
// 5. 测试调试模式
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 5: 调试模式\n');
const testUserId = 'test-user-123';
console.log(` 设置调试模式: userId=${testUserId}, modules=[RVW]`);
promptService.setDebugMode(testUserId, ['RVW'], true);
const isDebugging = promptService.isDebugging(testUserId, 'RVW_EDITORIAL');
console.log(` 是否在调试 RVW_EDITORIAL: ${isDebugging}`);
const isDebuggingASL = promptService.isDebugging(testUserId, 'ASL_SCREENING');
console.log(` 是否在调试 ASL_SCREENING: ${isDebuggingASL}`);
const debugState = promptService.getDebugState(testUserId);
console.log(` 调试状态: modules=[${Array.from(debugState?.modules || []).join(', ')}]`);
console.log(` 关闭调试模式`);
promptService.setDebugMode(testUserId, [], false);
// 6. 测试列表模板
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 6: 列表所有模板\n');
const templates = await promptService.listTemplates();
console.log(`${templates.length} 个模板:`);
for (const t of templates) {
const latest = t.versions[0];
console.log(` - ${t.code} (${t.name}) - v${latest?.version || 0} ${latest?.status || 'N/A'}`);
}
// 7. 测试兜底 Prompt
console.log('\n═══════════════════════════════════════════════════════');
console.log('📋 Test 7: 兜底 Prompt\n');
try {
// 测试不存在且无兜底的 Prompt应该抛错
await promptService.get('NON_EXISTENT_CODE');
console.log(' ❌ 应该抛出错误但没有');
} catch (e) {
console.log(' ✅ 正确抛出错误不存在的Prompt且无兜底');
}
// 测试有兜底的 ASL_SCREENING (虽然DB里有但演示兜底机制)
console.log(' 兜底Prompt列表: RVW_EDITORIAL, RVW_METHODOLOGY, ASL_SCREENING');
console.log('\n✅ 所有测试完成!');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());

View File

@@ -295,3 +295,4 @@ verifySchemas()