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

@@ -1,121 +1,475 @@
/**
* 数据库种子数据脚本
* 用于初始化开发环境的测试用户
*/
import { PrismaClient } from '@prisma/client';
import { PrismaClient, UserRole, TenantType, TenantStatus } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 开始初始化数据库种子数据...');
// 默认密码
const DEFAULT_PASSWORD = '123456';
// 创建测试用户
const mockUser = await prisma.user.upsert({
where: { id: 'user-mock-001' },
async function main() {
console.log('🌱 开始创建种子数据...\n');
// 加密默认密码
const hashedDefaultPassword = await bcrypt.hash(DEFAULT_PASSWORD, 10);
// ============================================
// 1. 创建内部租户(运营团队专用)
// ============================================
console.log('📌 创建内部租户...');
const internalTenant = await prisma.tenants.upsert({
where: { code: 'yizhengxun' },
update: {},
create: {
id: 'user-mock-001',
email: 'test@example.com',
password: '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYIkYvKx7ES', // password: "password123"
name: '测试用户',
role: 'user',
status: 'active',
kbQuota: 3,
kbUsed: 0,
isTrial: true,
trialEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后
id: uuidv4(),
code: 'yizhengxun',
name: '壹证循科技',
type: TenantType.INTERNAL,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#1890ff',
systemName: 'AI临床研究平台 - 运营管理端',
},
total_quota: BigInt(999999999),
updated_at: new Date(),
},
});
console.log(` ✅ 内部租户创建成功: ${internalTenant.name}`);
console.log('✅ 测试用户创建成功:', {
id: mockUser.id,
email: mockUser.email,
name: mockUser.name,
});
// 可选:创建管理员用户
const adminUser = await prisma.user.upsert({
where: { email: 'admin@example.com' },
// ============================================
// 1.5 创建公共租户(个人用户池)
// ============================================
console.log('📌 创建公共租户(个人用户)...');
const publicTenant = await prisma.tenants.upsert({
where: { code: 'public' },
update: {},
create: {
id: 'user-admin-001',
email: 'admin@example.com',
password: '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYIkYvKx7ES', // password: "password123"
name: '管理员',
role: 'admin',
id: uuidv4(),
code: 'public',
name: '个人用户',
type: TenantType.PUBLIC,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#1890ff',
systemName: 'AI临床研究平台',
},
total_quota: BigInt(100000),
updated_at: new Date(),
},
});
console.log(` ✅ 公共租户创建成功: ${publicTenant.name}`);
// 为公共租户开放部分模块
const publicModules = ['PKB', 'RVW'];
for (const moduleCode of publicModules) {
await prisma.tenant_modules.upsert({
where: { tenant_id_module_code: { tenant_id: publicTenant.id, module_code: moduleCode } },
update: {},
create: {
id: uuidv4(),
tenant_id: publicTenant.id,
module_code: moduleCode,
is_enabled: true,
},
});
}
console.log(` ✅ 公共租户模块订阅: ${publicModules.join(', ')}`);
// ============================================
// 2. 创建超级管理员
// ============================================
console.log('📌 创建超级管理员...');
const superAdmin = await prisma.User.upsert({
where: { phone: '13800000001' },
update: {},
create: {
phone: '13800000001',
password: hashedDefaultPassword,
is_default_password: false,
name: '超级管理员',
tenant_id: internalTenant.id,
role: UserRole.SUPER_ADMIN,
status: 'active',
kbQuota: 10,
kbUsed: 0,
isTrial: false,
},
});
console.log(` ✅ 超级管理员创建成功: ${superAdmin.phone}`);
console.log('✅ 管理员用户创建成功:', {
id: adminUser.id,
email: adminUser.email,
name: adminUser.name,
// ============================================
// 3. 创建Prompt工程师账号
// ============================================
console.log('📌 创建Prompt工程师账号...');
const promptEngineer = await prisma.User.upsert({
where: { phone: '13800000002' },
update: {},
create: {
phone: '13800000002',
password: hashedDefaultPassword,
is_default_password: false,
name: 'Prompt工程师',
tenant_id: internalTenant.id,
role: UserRole.PROMPT_ENGINEER,
status: 'active',
isTrial: false,
},
});
console.log(` ✅ Prompt工程师创建成功: ${promptEngineer.phone}`);
// ============================================
// 4. 创建示例租户(医院)
// ============================================
console.log('📌 创建示例租户(医院)...');
const hospitalTenant = await prisma.tenants.upsert({
where: { code: 'demo-hospital' },
update: {},
create: {
id: uuidv4(),
code: 'demo-hospital',
name: '示范医院',
type: TenantType.HOSPITAL,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#1890ff',
systemName: '示范医院临床研究平台',
},
total_quota: BigInt(1000000),
contact_name: '张主任',
contact_phone: '13800138000',
contact_email: 'zhang@demo-hospital.com',
updated_at: new Date(),
},
});
console.log(` ✅ 示例医院租户创建成功: ${hospitalTenant.name}`);
// ============================================
// 5. 创建医院科室
// ============================================
console.log('📌 创建医院科室...');
const cardiology = await prisma.departments.upsert({
where: { id: 'dept-cardiology' },
update: {},
create: {
id: 'dept-cardiology',
tenant_id: hospitalTenant.id,
name: '心内科',
description: '心血管内科',
updated_at: new Date(),
},
});
const neurology = await prisma.departments.upsert({
where: { id: 'dept-neurology' },
update: {},
create: {
id: 'dept-neurology',
tenant_id: hospitalTenant.id,
name: '神经内科',
description: '神经内科',
updated_at: new Date(),
},
});
console.log(` ✅ 科室创建成功: 心内科、神经内科`);
// ============================================
// 6. 创建示例租户(药企)
// ============================================
console.log('📌 创建示例租户(药企)...');
const pharmaTenant = await prisma.tenants.upsert({
where: { code: 'demo-pharma' },
update: {},
create: {
id: uuidv4(),
code: 'demo-pharma',
name: '示范药企',
type: TenantType.PHARMA,
status: TenantStatus.ACTIVE,
config: {
logo: null,
backgroundImage: null,
primaryColor: '#52c41a',
systemName: '示范药企IIT管理平台',
},
total_quota: BigInt(2000000),
contact_name: '李经理',
contact_phone: '13900139000',
contact_email: 'li@demo-pharma.com',
updated_at: new Date(),
},
});
console.log(` ✅ 示例药企租户创建成功: ${pharmaTenant.name}`);
// ============================================
// 7. 创建医院管理员
// ============================================
console.log('📌 创建医院管理员...');
const hospitalAdmin = await prisma.User.upsert({
where: { phone: '13800138001' },
update: {},
create: {
phone: '13800138001',
password: hashedDefaultPassword,
is_default_password: true,
name: '张主任',
tenant_id: hospitalTenant.id,
department_id: cardiology.id,
role: UserRole.HOSPITAL_ADMIN,
status: 'active',
isTrial: false,
},
});
console.log(` ✅ 医院管理员创建成功: ${hospitalAdmin.phone} (${hospitalAdmin.name})`);
// 创建租户成员关系
await prisma.tenant_members.upsert({
where: { tenant_id_user_id: { tenant_id: hospitalTenant.id, user_id: hospitalAdmin.id } },
update: {},
create: {
id: uuidv4(),
tenant_id: hospitalTenant.id,
user_id: hospitalAdmin.id,
role: UserRole.HOSPITAL_ADMIN,
},
});
console.log('\n🎉 数据库种子数据初始化完成!\n');
console.log('📝 测试账号信息:');
console.log(' 邮箱: test@example.com');
console.log(' 密码: password123');
console.log(' 用户ID: user-mock-001\n');
console.log('📝 管理员账号信息:');
console.log(' 邮箱: admin@example.com');
console.log(' 密码: password123');
console.log(' 用户ID: user-admin-001\n');
// ============================================
// 8. 创建普通医生用户
// ============================================
console.log('📌 创建普通医生用户...');
const doctor1 = await prisma.User.upsert({
where: { phone: '13800138002' },
update: {},
create: {
phone: '13800138002',
password: hashedDefaultPassword,
is_default_password: true,
name: '李医生',
tenant_id: hospitalTenant.id,
department_id: cardiology.id,
role: UserRole.USER,
status: 'active',
isTrial: false,
},
});
const doctor2 = await prisma.User.upsert({
where: { phone: '13800138003' },
update: {},
create: {
phone: '13800138003',
password: hashedDefaultPassword,
is_default_password: true,
name: '王医生',
tenant_id: hospitalTenant.id,
department_id: neurology.id,
role: UserRole.USER,
status: 'active',
isTrial: false,
},
});
console.log(` ✅ 普通用户创建成功: ${doctor1.name}${doctor2.name}`);
// 创建租户成员关系
for (const user of [doctor1, doctor2]) {
await prisma.tenant_members.upsert({
where: { tenant_id_user_id: { tenant_id: hospitalTenant.id, user_id: user.id } },
update: {},
create: {
id: uuidv4(),
tenant_id: hospitalTenant.id,
user_id: user.id,
role: UserRole.USER,
},
});
}
// ============================================
// 9. 创建权限数据
// ============================================
console.log('📌 创建权限数据...');
const permissionsData = [
// Prompt管理权限
{ code: 'prompt:view', name: '查看Prompt', description: '查看Prompt列表和历史版本', module: 'prompt' },
{ code: 'prompt:edit', name: '编辑Prompt', description: '创建/修改DRAFT版本', module: 'prompt' },
{ code: 'prompt:debug', name: '调试Prompt', description: '开启调试模式(核心权限)', module: 'prompt' },
{ code: 'prompt:publish', name: '发布Prompt', description: '发布DRAFT为ACTIVE', module: 'prompt' },
// 租户管理权限
{ code: 'tenant:view', name: '查看租户', description: '查看租户列表', module: 'tenant' },
{ code: 'tenant:create', name: '创建租户', description: '创建新租户', module: 'tenant' },
{ code: 'tenant:edit', name: '编辑租户', description: '编辑租户信息', module: 'tenant' },
{ code: 'tenant:delete', name: '删除租户', description: '删除租户', module: 'tenant' },
// 用户管理权限
{ code: 'user:view', name: '查看用户', description: '查看用户列表', module: 'user' },
{ code: 'user:create', name: '创建用户', description: '创建新用户', module: 'user' },
{ code: 'user:edit', name: '编辑用户', description: '编辑用户信息', module: 'user' },
{ code: 'user:delete', name: '删除用户', description: '删除用户', module: 'user' },
// 配额管理权限
{ code: 'quota:view', name: '查看配额', description: '查看配额使用情况', module: 'quota' },
{ code: 'quota:allocate', name: '分配配额', description: '分配配额给科室/用户', module: 'quota' },
// 审计日志权限
{ code: 'audit:view', name: '查看审计日志', description: '查看操作审计日志', module: 'audit' },
];
for (const perm of permissionsData) {
await prisma.permissions.upsert({
where: { code: perm.code },
update: {},
create: perm,
});
}
console.log(`${permissionsData.length} 个权限创建成功`);
// ============================================
// 10. 创建角色-权限关联
// ============================================
console.log('📌 创建角色-权限关联...');
const allPermissions = await prisma.permissions.findMany();
const permissionMap = new Map(allPermissions.map(p => [p.code, p.id]));
// SUPER_ADMIN 拥有所有权限
for (const perm of allPermissions) {
await prisma.role_permissions.upsert({
where: { role_permission_id: { role: UserRole.SUPER_ADMIN, permission_id: perm.id } },
update: {},
create: {
role: UserRole.SUPER_ADMIN,
permission_id: perm.id,
},
});
}
// PROMPT_ENGINEER 拥有Prompt相关权限
const promptPermCodes = ['prompt:view', 'prompt:edit', 'prompt:debug', 'prompt:publish'];
for (const code of promptPermCodes) {
const permId = permissionMap.get(code);
if (permId) {
await prisma.role_permissions.upsert({
where: { role_permission_id: { role: UserRole.PROMPT_ENGINEER, permission_id: permId } },
update: {},
create: {
role: UserRole.PROMPT_ENGINEER,
permission_id: permId,
},
});
}
}
// HOSPITAL_ADMIN 拥有用户、配额、审计权限
const hospitalAdminPermCodes = ['user:view', 'user:create', 'user:edit', 'quota:view', 'quota:allocate', 'audit:view'];
for (const code of hospitalAdminPermCodes) {
const permId = permissionMap.get(code);
if (permId) {
await prisma.role_permissions.upsert({
where: { role_permission_id: { role: UserRole.HOSPITAL_ADMIN, permission_id: permId } },
update: {},
create: {
role: UserRole.HOSPITAL_ADMIN,
permission_id: permId,
},
});
}
}
console.log(' ✅ 角色-权限关联创建成功');
// ============================================
// 11. 跳过Prompt模板表尚未创建
// ============================================
console.log('📌 跳过Prompt模板创建capability_schema.prompt_templates 尚未创建)');
// ============================================
// 12. 创建租户模块订阅
// ============================================
console.log('📌 创建租户模块订阅...');
const hospitalModules = ['ASL', 'DC', 'PKB', 'AIA', 'RVW'];
for (const moduleCode of hospitalModules) {
await prisma.tenant_modules.upsert({
where: { tenant_id_module_code: { tenant_id: hospitalTenant.id, module_code: moduleCode } },
update: {},
create: {
id: uuidv4(),
tenant_id: hospitalTenant.id,
module_code: moduleCode,
is_enabled: true,
},
});
}
// 药企只订阅IIT和DC
const pharmaModules = ['IIT', 'DC'];
for (const moduleCode of pharmaModules) {
await prisma.tenant_modules.upsert({
where: { tenant_id_module_code: { tenant_id: pharmaTenant.id, module_code: moduleCode } },
update: {},
create: {
id: uuidv4(),
tenant_id: pharmaTenant.id,
module_code: moduleCode,
is_enabled: true,
},
});
}
console.log(' ✅ 租户模块订阅创建成功');
// ============================================
// 完成
// ============================================
console.log('\n🎉 种子数据创建完成!\n');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📝 测试账号信息 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 🔐 登录方式1手机号 + 验证码 ║');
console.log('║ 🔐 登录方式2手机号 + 密码 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【运营管理员】 ║');
console.log('║ 超级管理员: 13800000001 / 123456 ║');
console.log('║ Prompt工程师: 13800000002 / 123456 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【医院端 - demo-hospital】 ║');
console.log('║ 医院管理员: 13800138001 / 123456 (张主任) ║');
console.log('║ 普通医生: 13800138002 / 123456 (李医生·心内科) ║');
console.log('║ 普通医生: 13800138003 / 123456 (王医生·神内科) ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【药企端 - demo-pharma】 ║');
console.log('║ (暂无用户,可通过管理端添加) ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log('║ 【个人用户 - public】 ║');
console.log('║ 通用登录入口: /login ║');
console.log('║ 可用模块: PKB, RVW ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('\n📌 租户专属登录URL');
console.log(' - 通用登录: /login');
console.log(' - 医院端: /t/demo-hospital/login');
console.log(' - 药企端: /t/demo-pharma/login');
console.log('\n⚠ 提示:普通用户首次登录会提示修改默认密码');
}
main()
.catch((e) => {
console.error('❌ 初始化种子数据失败:', e);
process.exit(1);
})
.finally(async () => {
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error('❌ 种子数据创建失败:', e);
await prisma.$disconnect();
process.exit(1);
});