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:
235
backend/src/common/auth/auth.controller.ts
Normal file
235
backend/src/common/auth/auth.controller.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Auth Controller
|
||||
*
|
||||
* 认证相关的 HTTP 请求处理
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { authService } from './auth.service.js';
|
||||
import type { PasswordLoginRequest, VerificationCodeLoginRequest, ChangePasswordRequest } from './auth.service.js';
|
||||
import { logger } from '../logging/index.js';
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
*
|
||||
* POST /api/v1/auth/login/password
|
||||
*/
|
||||
export async function loginWithPassword(
|
||||
request: FastifyRequest<{ Body: PasswordLoginRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const result = await authService.loginWithPassword(request.body);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '登录失败';
|
||||
logger.warn('登录失败', { error: message, phone: request.body.phone });
|
||||
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录
|
||||
*
|
||||
* POST /api/v1/auth/login/code
|
||||
*/
|
||||
export async function loginWithVerificationCode(
|
||||
request: FastifyRequest<{ Body: VerificationCodeLoginRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const result = await authService.loginWithVerificationCode(request.body);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '登录失败';
|
||||
logger.warn('验证码登录失败', { error: message, phone: request.body.phone });
|
||||
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* POST /api/v1/auth/verification-code
|
||||
*/
|
||||
export async function sendVerificationCode(
|
||||
request: FastifyRequest<{ Body: { phone: string; type?: 'LOGIN' | 'RESET_PASSWORD' } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { phone, type = 'LOGIN' } = request.body;
|
||||
|
||||
const result = await authService.sendVerificationCode(phone, type);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: {
|
||||
message: '验证码已发送',
|
||||
expiresIn: result.expiresIn,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '发送失败';
|
||||
logger.warn('发送验证码失败', { error: message, phone: request.body.phone });
|
||||
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*
|
||||
* GET /api/v1/auth/me
|
||||
*/
|
||||
export async function getCurrentUser(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await authService.getCurrentUser(request.user.userId);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: user,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '获取用户信息失败';
|
||||
logger.error('获取用户信息失败', { error: message, userId: request.user?.userId });
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: 'InternalServerError',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* POST /api/v1/auth/change-password
|
||||
*/
|
||||
export async function changePassword(
|
||||
request: FastifyRequest<{ Body: ChangePasswordRequest }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.changePassword(request.user.userId, request.body);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: {
|
||||
message: '密码修改成功',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '修改密码失败';
|
||||
logger.warn('修改密码失败', { error: message, userId: request.user?.userId });
|
||||
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*
|
||||
* POST /api/v1/auth/refresh
|
||||
*/
|
||||
export async function refreshToken(
|
||||
request: FastifyRequest<{ Body: { refreshToken: string } }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { refreshToken } = request.body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'BadRequest',
|
||||
message: '请提供 refreshToken',
|
||||
});
|
||||
}
|
||||
|
||||
const tokens = await authService.refreshToken(refreshToken);
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: tokens,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '刷新Token失败';
|
||||
logger.warn('刷新Token失败', { error: message });
|
||||
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*
|
||||
* POST /api/v1/auth/logout
|
||||
*
|
||||
* 注意:JWT 是无状态的,登出主要是前端清除Token
|
||||
* 如果需要服务端登出,需要维护 Token 黑名单
|
||||
*/
|
||||
export async function logout(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
// TODO: 如果需要服务端登出,可以将Token加入黑名单
|
||||
logger.info('用户登出', { userId: request.user?.userId });
|
||||
|
||||
return reply.status(200).send({
|
||||
success: true,
|
||||
data: {
|
||||
message: '登出成功',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
256
backend/src/common/auth/auth.middleware.ts
Normal file
256
backend/src/common/auth/auth.middleware.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Auth Middleware
|
||||
*
|
||||
* Fastify 认证中间件
|
||||
*
|
||||
* 功能:
|
||||
* - 验证 JWT Token
|
||||
* - 注入用户信息到请求
|
||||
* - 权限检查
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply, FastifyInstance, preHandlerHookHandler } from 'fastify';
|
||||
import { jwtService } from './jwt.service.js';
|
||||
import type { DecodedToken } from './jwt.service.js';
|
||||
import { logger } from '../logging/index.js';
|
||||
|
||||
/**
|
||||
* 扩展 Fastify Request 类型
|
||||
*/
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
user?: DecodedToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证错误类
|
||||
*/
|
||||
export class AuthenticationError extends Error {
|
||||
public statusCode: number;
|
||||
|
||||
constructor(message: string, statusCode: number = 401) {
|
||||
super(message);
|
||||
this.name = 'AuthenticationError';
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权错误类
|
||||
*/
|
||||
export class AuthorizationError extends Error {
|
||||
public statusCode: number;
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AuthorizationError';
|
||||
this.statusCode = 403;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证中间件
|
||||
*
|
||||
* 验证 JWT Token 并注入用户信息
|
||||
*/
|
||||
export const authenticate: preHandlerHookHandler = async (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) => {
|
||||
try {
|
||||
// 1. 获取 Authorization Header
|
||||
const authHeader = request.headers.authorization;
|
||||
const token = jwtService.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError('未提供认证令牌');
|
||||
}
|
||||
|
||||
// 2. 验证 Token
|
||||
const decoded = jwtService.verifyToken(token);
|
||||
|
||||
// 3. 注入用户信息
|
||||
request.user = decoded;
|
||||
|
||||
logger.debug('认证成功', {
|
||||
userId: decoded.userId,
|
||||
role: decoded.role,
|
||||
tenantId: decoded.tenantId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: 'Unauthorized',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return reply.status(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: '认证失败',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 可选认证中间件
|
||||
*
|
||||
* 如果有 Token 则验证,没有也放行
|
||||
*/
|
||||
export const optionalAuthenticate: preHandlerHookHandler = async (
|
||||
request: FastifyRequest,
|
||||
_reply: FastifyReply
|
||||
) => {
|
||||
try {
|
||||
const authHeader = request.headers.authorization;
|
||||
const token = jwtService.extractTokenFromHeader(authHeader);
|
||||
|
||||
if (token) {
|
||||
const decoded = jwtService.verifyToken(token);
|
||||
request.user = decoded;
|
||||
}
|
||||
} catch (error) {
|
||||
// 可选认证,忽略错误
|
||||
logger.debug('可选认证:Token无效或已过期');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 角色检查中间件工厂
|
||||
*
|
||||
* @param allowedRoles 允许的角色列表
|
||||
*/
|
||||
export function requireRoles(...allowedRoles: string[]): preHandlerHookHandler {
|
||||
return async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
if (!allowedRoles.includes(request.user.role)) {
|
||||
logger.warn('权限不足', {
|
||||
userId: request.user.userId,
|
||||
role: request.user.role,
|
||||
requiredRoles: allowedRoles,
|
||||
});
|
||||
|
||||
return reply.status(403).send({
|
||||
error: 'Forbidden',
|
||||
message: '权限不足',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查中间件工厂
|
||||
*
|
||||
* @param requiredPermission 需要的权限code
|
||||
*/
|
||||
export function requirePermission(requiredPermission: string): preHandlerHookHandler {
|
||||
return async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 从缓存或数据库获取用户权限
|
||||
// 目前简化处理:超级管理员拥有所有权限
|
||||
if (request.user.role === 'SUPER_ADMIN') {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 实现权限检查逻辑
|
||||
// const hasPermission = await checkUserPermission(request.user.userId, requiredPermission);
|
||||
// if (!hasPermission) {
|
||||
// return reply.status(403).send({
|
||||
// error: 'Forbidden',
|
||||
// message: `需要权限: ${requiredPermission}`,
|
||||
// });
|
||||
// }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户检查中间件
|
||||
*
|
||||
* 确保用户只能访问自己租户的数据
|
||||
*/
|
||||
export const requireSameTenant: preHandlerHookHandler = async (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) => {
|
||||
if (!request.user) {
|
||||
return reply.status(401).send({
|
||||
error: 'Unauthorized',
|
||||
message: '未认证',
|
||||
});
|
||||
}
|
||||
|
||||
// 超级管理员可以访问所有租户
|
||||
if (request.user.role === 'SUPER_ADMIN') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从请求参数或body中获取tenantId
|
||||
const requestTenantId =
|
||||
(request.params as any)?.tenantId ||
|
||||
(request.body as any)?.tenantId ||
|
||||
(request.query as any)?.tenantId;
|
||||
|
||||
if (requestTenantId && requestTenantId !== request.user.tenantId) {
|
||||
logger.warn('租户不匹配', {
|
||||
userId: request.user.userId,
|
||||
userTenantId: request.user.tenantId,
|
||||
requestTenantId,
|
||||
});
|
||||
|
||||
return reply.status(403).send({
|
||||
error: 'Forbidden',
|
||||
message: '无权访问此租户数据',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册认证插件到 Fastify
|
||||
*/
|
||||
export async function registerAuthPlugin(fastify: FastifyInstance): Promise<void> {
|
||||
// 添加 decorate 以支持 request.user
|
||||
fastify.decorateRequest('user', undefined);
|
||||
|
||||
// 注册全局错误处理
|
||||
fastify.setErrorHandler((error, request, reply) => {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: 'Unauthorized',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof AuthorizationError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: 'Forbidden',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
// 其他错误交给默认处理
|
||||
throw error;
|
||||
});
|
||||
|
||||
logger.info('✅ 认证插件已注册');
|
||||
}
|
||||
|
||||
140
backend/src/common/auth/auth.routes.ts
Normal file
140
backend/src/common/auth/auth.routes.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Auth Routes
|
||||
*
|
||||
* 认证相关路由定义
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
import {
|
||||
loginWithPassword,
|
||||
loginWithVerificationCode,
|
||||
sendVerificationCode,
|
||||
getCurrentUser,
|
||||
changePassword,
|
||||
refreshToken,
|
||||
logout,
|
||||
} from './auth.controller.js';
|
||||
import { authenticate } from './auth.middleware.js';
|
||||
|
||||
/**
|
||||
* 登录请求 Schema
|
||||
*/
|
||||
const passwordLoginSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['phone', 'password'],
|
||||
properties: {
|
||||
phone: { type: 'string', pattern: '^1[3-9]\\d{9}$', description: '手机号' },
|
||||
password: { type: 'string', minLength: 6, description: '密码' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const codeLoginSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['phone', 'code'],
|
||||
properties: {
|
||||
phone: { type: 'string', pattern: '^1[3-9]\\d{9}$', description: '手机号' },
|
||||
code: { type: 'string', minLength: 6, maxLength: 6, description: '验证码' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const sendCodeSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['phone'],
|
||||
properties: {
|
||||
phone: { type: 'string', pattern: '^1[3-9]\\d{9}$', description: '手机号' },
|
||||
type: { type: 'string', enum: ['LOGIN', 'RESET_PASSWORD'], default: 'LOGIN' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const changePasswordSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['newPassword', 'confirmPassword'],
|
||||
properties: {
|
||||
oldPassword: { type: 'string', description: '原密码(可选,验证码修改时不需要)' },
|
||||
newPassword: { type: 'string', minLength: 6, description: '新密码' },
|
||||
confirmPassword: { type: 'string', minLength: 6, description: '确认密码' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const refreshTokenSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['refreshToken'],
|
||||
properties: {
|
||||
refreshToken: { type: 'string', description: 'Refresh Token' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册认证路由
|
||||
*/
|
||||
export async function authRoutes(
|
||||
fastify: FastifyInstance,
|
||||
_options: FastifyPluginOptions
|
||||
): Promise<void> {
|
||||
// ========== 公开路由(无需认证)==========
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
*/
|
||||
fastify.post('/login/password', {
|
||||
schema: passwordLoginSchema,
|
||||
}, loginWithPassword);
|
||||
|
||||
/**
|
||||
* 验证码登录
|
||||
*/
|
||||
fastify.post('/login/code', {
|
||||
schema: codeLoginSchema,
|
||||
}, loginWithVerificationCode);
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
fastify.post('/verification-code', {
|
||||
schema: sendCodeSchema,
|
||||
}, sendVerificationCode);
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*/
|
||||
fastify.post('/refresh', {
|
||||
schema: refreshTokenSchema,
|
||||
}, refreshToken);
|
||||
|
||||
// ========== 需要认证的路由 ==========
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
fastify.get('/me', {
|
||||
preHandler: [authenticate],
|
||||
}, getCurrentUser);
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
fastify.post('/change-password', {
|
||||
preHandler: [authenticate],
|
||||
schema: changePasswordSchema,
|
||||
}, changePassword as any);
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
fastify.post('/logout', {
|
||||
preHandler: [authenticate],
|
||||
}, logout);
|
||||
}
|
||||
|
||||
export default authRoutes;
|
||||
|
||||
436
backend/src/common/auth/auth.service.ts
Normal file
436
backend/src/common/auth/auth.service.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
/**
|
||||
* Auth Service
|
||||
*
|
||||
* 认证业务逻辑
|
||||
*
|
||||
* 支持两种登录方式:
|
||||
* 1. 手机号 + 验证码
|
||||
* 2. 手机号 + 密码
|
||||
*/
|
||||
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { prisma } from '../../config/database.js';
|
||||
import { jwtService } from './jwt.service.js';
|
||||
import type { JWTPayload, TokenResponse } from './jwt.service.js';
|
||||
import { logger } from '../logging/index.js';
|
||||
|
||||
/**
|
||||
* 登录请求 - 密码方式
|
||||
*/
|
||||
export interface PasswordLoginRequest {
|
||||
phone: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录请求 - 验证码方式
|
||||
*/
|
||||
export interface VerificationCodeLoginRequest {
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码请求
|
||||
*/
|
||||
export interface ChangePasswordRequest {
|
||||
oldPassword?: string; // 如果用验证码修改,可不提供
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息响应
|
||||
*/
|
||||
export interface UserInfoResponse {
|
||||
id: string;
|
||||
phone: string;
|
||||
name: string;
|
||||
email?: string | null;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
tenantCode?: string;
|
||||
tenantName?: string;
|
||||
departmentId?: string | null;
|
||||
departmentName?: string | null;
|
||||
isDefaultPassword: boolean;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
user: UserInfoResponse;
|
||||
tokens: TokenResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth Service 类
|
||||
*/
|
||||
export class AuthService {
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
*/
|
||||
async loginWithPassword(request: PasswordLoginRequest): Promise<LoginResponse> {
|
||||
const { phone, password } = request;
|
||||
|
||||
// 1. 查找用户
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { phone },
|
||||
include: {
|
||||
tenants: true,
|
||||
departments: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logger.warn('登录失败:用户不存在', { phone });
|
||||
throw new Error('手机号或密码错误');
|
||||
}
|
||||
|
||||
// 2. 验证密码
|
||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||
if (!isValidPassword) {
|
||||
logger.warn('登录失败:密码错误', { phone, userId: user.id });
|
||||
throw new Error('手机号或密码错误');
|
||||
}
|
||||
|
||||
// 3. 检查用户状态
|
||||
if (user.status !== 'active') {
|
||||
logger.warn('登录失败:用户已禁用', { phone, userId: user.id, status: user.status });
|
||||
throw new Error('账号已被禁用,请联系管理员');
|
||||
}
|
||||
|
||||
// 4. 获取用户权限
|
||||
const permissions = await this.getUserPermissions(user.role);
|
||||
|
||||
// 5. 生成 JWT
|
||||
const jwtPayload: JWTPayload = {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
};
|
||||
|
||||
const tokens = jwtService.generateTokens(jwtPayload);
|
||||
|
||||
// 6. 更新最后登录时间
|
||||
await prisma.User.update({
|
||||
where: { id: user.id },
|
||||
data: { updatedAt: new Date() },
|
||||
});
|
||||
|
||||
logger.info('用户登录成功(密码方式)', {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
});
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
tenantName: user.tenants?.name,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.departments?.name,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
permissions,
|
||||
},
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录
|
||||
*/
|
||||
async loginWithVerificationCode(request: VerificationCodeLoginRequest): Promise<LoginResponse> {
|
||||
const { phone, code } = request;
|
||||
|
||||
// 1. 验证验证码
|
||||
const verificationCode = await prisma.verification_codes.findFirst({
|
||||
where: {
|
||||
phone,
|
||||
code,
|
||||
type: 'LOGIN',
|
||||
is_used: false,
|
||||
expires_at: { gt: new Date() },
|
||||
},
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
|
||||
if (!verificationCode) {
|
||||
logger.warn('登录失败:验证码无效', { phone });
|
||||
throw new Error('验证码无效或已过期');
|
||||
}
|
||||
|
||||
// 2. 标记验证码已使用
|
||||
await prisma.verification_codes.update({
|
||||
where: { id: verificationCode.id },
|
||||
data: { is_used: true },
|
||||
});
|
||||
|
||||
// 3. 查找用户
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { phone },
|
||||
include: {
|
||||
tenants: true,
|
||||
departments: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logger.warn('登录失败:用户不存在', { phone });
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
// 4. 检查用户状态
|
||||
if (user.status !== 'active') {
|
||||
logger.warn('登录失败:用户已禁用', { phone, userId: user.id, status: user.status });
|
||||
throw new Error('账号已被禁用,请联系管理员');
|
||||
}
|
||||
|
||||
// 5. 获取用户权限
|
||||
const permissions = await this.getUserPermissions(user.role);
|
||||
|
||||
// 6. 生成 JWT
|
||||
const jwtPayload: JWTPayload = {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
};
|
||||
|
||||
const tokens = jwtService.generateTokens(jwtPayload);
|
||||
|
||||
logger.info('用户登录成功(验证码方式)', {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
});
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
tenantName: user.tenants?.name,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.departments?.name,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
permissions,
|
||||
},
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
async getCurrentUser(userId: string): Promise<UserInfoResponse> {
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
tenants: true,
|
||||
departments: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
const permissions = await this.getUserPermissions(user.role);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
tenantName: user.tenants?.name,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.departments?.name,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
permissions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
async changePassword(userId: string, request: ChangePasswordRequest): Promise<void> {
|
||||
const { oldPassword, newPassword, confirmPassword } = request;
|
||||
|
||||
// 1. 验证新密码
|
||||
if (newPassword !== confirmPassword) {
|
||||
throw new Error('两次输入的密码不一致');
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw new Error('密码长度至少为6位');
|
||||
}
|
||||
|
||||
// 2. 获取用户
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
// 3. 如果提供了旧密码,验证旧密码
|
||||
if (oldPassword) {
|
||||
const isValidPassword = await bcrypt.compare(oldPassword, user.password);
|
||||
if (!isValidPassword) {
|
||||
throw new Error('原密码错误');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 加密新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// 5. 更新密码
|
||||
await prisma.User.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
is_default_password: false,
|
||||
password_changed_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
logger.info('用户修改密码成功', { userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
async sendVerificationCode(phone: string, type: 'LOGIN' | 'RESET_PASSWORD'): Promise<{ expiresIn: number }> {
|
||||
// 1. 检查用户是否存在
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { phone },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
// 2. 检查是否频繁发送(1分钟内只能发一次)
|
||||
const recentCode = await prisma.verification_codes.findFirst({
|
||||
where: {
|
||||
phone,
|
||||
created_at: { gt: new Date(Date.now() - 60 * 1000) },
|
||||
},
|
||||
});
|
||||
|
||||
if (recentCode) {
|
||||
throw new Error('验证码发送过于频繁,请稍后再试');
|
||||
}
|
||||
|
||||
// 3. 生成6位验证码
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
// 4. 设置5分钟过期
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
// 5. 保存验证码
|
||||
await prisma.verification_codes.create({
|
||||
data: {
|
||||
phone,
|
||||
code,
|
||||
type: type as any, // VerificationType enum
|
||||
expires_at: expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: 实际发送短信
|
||||
// 开发环境直接打印验证码
|
||||
logger.info('📱 验证码已生成', { phone, code, type, expiresAt });
|
||||
console.log(`\n📱 验证码: ${code} (有效期5分钟)\n`);
|
||||
|
||||
return { expiresIn: 300 }; // 5分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*/
|
||||
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||
return jwtService.refreshToken(refreshToken, async (userId) => {
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { id: userId },
|
||||
include: { tenants: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
*/
|
||||
private async getUserPermissions(role: string): Promise<string[]> {
|
||||
const rolePermissions = await prisma.role_permissions.findMany({
|
||||
where: { role: role as any },
|
||||
include: { permissions: true },
|
||||
});
|
||||
|
||||
return rolePermissions.map(rp => rp.permissions.code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID获取JWT Payload(用于刷新Token)
|
||||
*/
|
||||
async getUserPayloadById(userId: string): Promise<JWTPayload | null> {
|
||||
const user = await prisma.User.findUnique({
|
||||
where: { id: userId },
|
||||
include: { tenants: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
tenantId: user.tenant_id,
|
||||
tenantCode: user.tenants?.code,
|
||||
isDefaultPassword: user.is_default_password,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const authService = new AuthService();
|
||||
|
||||
35
backend/src/common/auth/index.ts
Normal file
35
backend/src/common/auth/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Auth Module
|
||||
*
|
||||
* 认证模块导出
|
||||
*/
|
||||
|
||||
// JWT Service
|
||||
export { jwtService, JWTService } from './jwt.service.js';
|
||||
export type { JWTPayload, TokenResponse, DecodedToken } from './jwt.service.js';
|
||||
|
||||
// Auth Service
|
||||
export { authService, AuthService } from './auth.service.js';
|
||||
export type {
|
||||
PasswordLoginRequest,
|
||||
VerificationCodeLoginRequest,
|
||||
ChangePasswordRequest,
|
||||
UserInfoResponse,
|
||||
LoginResponse,
|
||||
} from './auth.service.js';
|
||||
|
||||
// Auth Middleware
|
||||
export {
|
||||
authenticate,
|
||||
optionalAuthenticate,
|
||||
requireRoles,
|
||||
requirePermission,
|
||||
requireSameTenant,
|
||||
registerAuthPlugin,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
} from './auth.middleware.js';
|
||||
|
||||
// Auth Routes
|
||||
export { authRoutes } from './auth.routes.js';
|
||||
|
||||
186
backend/src/common/auth/jwt.service.ts
Normal file
186
backend/src/common/auth/jwt.service.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* JWT Service
|
||||
*
|
||||
* JWT令牌的生成、验证和刷新
|
||||
*
|
||||
* 设计原则:
|
||||
* - 使用 jsonwebtoken 库
|
||||
* - Token payload 包含用户ID、角色、租户ID
|
||||
* - 支持 Access Token 和 Refresh Token
|
||||
*/
|
||||
|
||||
import jwt, { SignOptions, JwtPayload } from 'jsonwebtoken';
|
||||
import { config } from '../../config/env.js';
|
||||
|
||||
/**
|
||||
* JWT Payload 接口
|
||||
*/
|
||||
export interface JWTPayload {
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 用户手机号 */
|
||||
phone: string;
|
||||
/** 用户角色 */
|
||||
role: string;
|
||||
/** 租户ID */
|
||||
tenantId: string;
|
||||
/** 租户Code(用于URL路由) */
|
||||
tenantCode?: string;
|
||||
/** 是否为默认密码 */
|
||||
isDefaultPassword?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 响应接口
|
||||
*/
|
||||
export interface TokenResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number; // 秒
|
||||
tokenType: 'Bearer';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码后的 Token 接口
|
||||
*/
|
||||
export interface DecodedToken extends JWTPayload {
|
||||
iat: number; // 签发时间
|
||||
exp: number; // 过期时间
|
||||
}
|
||||
|
||||
// Token 配置
|
||||
const ACCESS_TOKEN_EXPIRES_IN = '2h'; // Access Token 2小时过期
|
||||
const REFRESH_TOKEN_EXPIRES_IN = '7d'; // Refresh Token 7天过期
|
||||
const ACCESS_TOKEN_EXPIRES_SECONDS = 2 * 60 * 60; // 7200秒
|
||||
|
||||
/**
|
||||
* JWT Service 类
|
||||
*/
|
||||
export class JWTService {
|
||||
private readonly secret: string;
|
||||
|
||||
constructor() {
|
||||
this.secret = config.jwtSecret;
|
||||
if (this.secret === 'your-secret-key-change-in-production' && config.nodeEnv === 'production') {
|
||||
throw new Error('JWT_SECRET must be configured in production environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Access Token
|
||||
*/
|
||||
generateAccessToken(payload: JWTPayload): string {
|
||||
const options: SignOptions = {
|
||||
expiresIn: ACCESS_TOKEN_EXPIRES_IN,
|
||||
issuer: 'aiclinical',
|
||||
subject: payload.userId,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, this.secret, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Refresh Token
|
||||
*/
|
||||
generateRefreshToken(payload: JWTPayload): string {
|
||||
// Refresh Token 只包含必要信息
|
||||
const refreshPayload = {
|
||||
userId: payload.userId,
|
||||
type: 'refresh',
|
||||
};
|
||||
|
||||
const options: SignOptions = {
|
||||
expiresIn: REFRESH_TOKEN_EXPIRES_IN,
|
||||
issuer: 'aiclinical',
|
||||
subject: payload.userId,
|
||||
};
|
||||
|
||||
return jwt.sign(refreshPayload, this.secret, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整的 Token 响应
|
||||
*/
|
||||
generateTokens(payload: JWTPayload): TokenResponse {
|
||||
return {
|
||||
accessToken: this.generateAccessToken(payload),
|
||||
refreshToken: this.generateRefreshToken(payload),
|
||||
expiresIn: ACCESS_TOKEN_EXPIRES_SECONDS,
|
||||
tokenType: 'Bearer',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Token
|
||||
*/
|
||||
verifyToken(token: string): DecodedToken {
|
||||
try {
|
||||
const decoded = jwt.verify(token, this.secret, {
|
||||
issuer: 'aiclinical',
|
||||
}) as DecodedToken;
|
||||
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new Error('Token已过期');
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new Error('Token无效');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*
|
||||
* 使用 Refresh Token 获取新的 Access Token
|
||||
*/
|
||||
async refreshToken(refreshToken: string, getUserById: (id: string) => Promise<JWTPayload | null>): Promise<TokenResponse> {
|
||||
// 验证 Refresh Token
|
||||
const decoded = this.verifyToken(refreshToken);
|
||||
|
||||
if ((decoded as any).type !== 'refresh') {
|
||||
throw new Error('无效的Refresh Token');
|
||||
}
|
||||
|
||||
// 获取用户最新信息
|
||||
const user = await getUserById(decoded.userId);
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
// 生成新的 Tokens
|
||||
return this.generateTokens(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Authorization Header 中提取 Token
|
||||
*/
|
||||
extractTokenFromHeader(authHeader: string | undefined): string | null {
|
||||
if (!authHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ');
|
||||
if (type !== 'Bearer' || !token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码 Token(不验证签名)
|
||||
*
|
||||
* 用于调试或获取过期token的payload
|
||||
*/
|
||||
decodeToken(token: string): DecodedToken | null {
|
||||
const decoded = jwt.decode(token);
|
||||
return decoded as DecodedToken | null;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const jwtService = new JWTService();
|
||||
|
||||
@@ -313,5 +313,6 @@ export function getBatchItems<T>(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
33
backend/src/common/prompt/index.ts
Normal file
33
backend/src/common/prompt/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Prompt 管理模块导出
|
||||
*/
|
||||
|
||||
// Service
|
||||
export { PromptService, getPromptService, resetPromptService } from './prompt.service.js';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
PromptStatus,
|
||||
ModelConfig,
|
||||
PromptTemplate,
|
||||
PromptVersion,
|
||||
RenderedPrompt,
|
||||
DebugState,
|
||||
GetPromptOptions,
|
||||
VariableValidation,
|
||||
} from './prompt.types.js';
|
||||
|
||||
// Fallbacks
|
||||
export {
|
||||
getFallbackPrompt,
|
||||
hasFallbackPrompt,
|
||||
getAllFallbackCodes,
|
||||
FALLBACK_PROMPTS,
|
||||
} from './prompt.fallbacks.js';
|
||||
|
||||
// Routes
|
||||
export { promptRoutes } from './prompt.routes.js';
|
||||
|
||||
// Controller (for testing)
|
||||
export * from './prompt.controller.js';
|
||||
|
||||
418
backend/src/common/prompt/prompt.controller.ts
Normal file
418
backend/src/common/prompt/prompt.controller.ts
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Prompt 管理 API 控制器
|
||||
*/
|
||||
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { getPromptService } from './prompt.service.js';
|
||||
import type { ModelConfig } from './prompt.types.js';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 请求类型定义
|
||||
interface ListPromptsQuery {
|
||||
module?: string;
|
||||
}
|
||||
|
||||
interface GetPromptParams {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface SaveDraftBody {
|
||||
content: string;
|
||||
modelConfig?: ModelConfig;
|
||||
changelog?: string;
|
||||
}
|
||||
|
||||
interface PublishBody {
|
||||
// 可扩展
|
||||
}
|
||||
|
||||
interface RollbackBody {
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface SetDebugModeBody {
|
||||
modules: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface TestRenderBody {
|
||||
content: string;
|
||||
variables: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Prompt 列表
|
||||
* GET /api/admin/prompts?module=RVW
|
||||
*/
|
||||
export async function listPrompts(
|
||||
request: FastifyRequest<{ Querystring: ListPromptsQuery }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { module } = request.query;
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
const templates = await promptService.listTemplates(module);
|
||||
|
||||
// 转换为 API 响应格式
|
||||
const result = templates.map(t => ({
|
||||
id: t.id,
|
||||
code: t.code,
|
||||
name: t.name,
|
||||
module: t.module,
|
||||
description: t.description,
|
||||
variables: t.variables,
|
||||
latestVersion: t.versions[0] ? {
|
||||
version: t.versions[0].version,
|
||||
status: t.versions[0].status,
|
||||
createdAt: t.versions[0].created_at,
|
||||
} : null,
|
||||
updatedAt: t.updated_at,
|
||||
}));
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: result,
|
||||
total: result.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[PromptController] listPrompts error:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: 'Failed to list prompts',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Prompt 详情(含版本历史)
|
||||
* GET /api/admin/prompts/:code
|
||||
*/
|
||||
export async function getPromptDetail(
|
||||
request: FastifyRequest<{ Params: GetPromptParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
const template = await promptService.getTemplateDetail(code);
|
||||
|
||||
if (!template) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
error: 'Prompt not found',
|
||||
});
|
||||
}
|
||||
|
||||
// 转换版本历史
|
||||
const versions = template.versions.map(v => ({
|
||||
id: v.id,
|
||||
version: v.version,
|
||||
status: v.status,
|
||||
content: v.content,
|
||||
modelConfig: v.model_config,
|
||||
changelog: v.changelog,
|
||||
createdBy: v.created_by,
|
||||
createdAt: v.created_at,
|
||||
}));
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
id: template.id,
|
||||
code: template.code,
|
||||
name: template.name,
|
||||
module: template.module,
|
||||
description: template.description,
|
||||
variables: template.variables,
|
||||
versions,
|
||||
createdAt: template.created_at,
|
||||
updatedAt: template.updated_at,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[PromptController] getPromptDetail error:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: 'Failed to get prompt detail',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存草稿
|
||||
* POST /api/admin/prompts/:code/draft
|
||||
*
|
||||
* 需要权限: prompt:edit
|
||||
*/
|
||||
export async function saveDraft(
|
||||
request: FastifyRequest<{ Params: GetPromptParams; Body: SaveDraftBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const { content, modelConfig, changelog } = request.body;
|
||||
const userId = (request as any).user?.id;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
// 保存草稿
|
||||
const draft = await promptService.saveDraft(
|
||||
code,
|
||||
content,
|
||||
modelConfig,
|
||||
changelog,
|
||||
userId
|
||||
);
|
||||
|
||||
// 提取变量信息
|
||||
const variables = promptService.extractVariables(content);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
id: draft.id,
|
||||
version: draft.version,
|
||||
status: draft.status,
|
||||
variables,
|
||||
message: 'Draft saved successfully',
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] saveDraft error:', error);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to save draft',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布 Prompt
|
||||
* POST /api/admin/prompts/:code/publish
|
||||
*
|
||||
* 需要权限: prompt:publish
|
||||
*/
|
||||
export async function publishPrompt(
|
||||
request: FastifyRequest<{ Params: GetPromptParams; Body: PublishBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const userId = (request as any).user?.id;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
const published = await promptService.publish(code, userId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
version: published.version,
|
||||
status: 'ACTIVE',
|
||||
message: `Prompt ${code} published successfully (v${published.version})`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] publishPrompt error:', error);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to publish prompt',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚到指定版本
|
||||
* POST /api/admin/prompts/:code/rollback
|
||||
*
|
||||
* 需要权限: prompt:publish
|
||||
*/
|
||||
export async function rollbackPrompt(
|
||||
request: FastifyRequest<{ Params: GetPromptParams; Body: RollbackBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
const { version } = request.body;
|
||||
const userId = (request as any).user?.id;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
const rolled = await promptService.rollback(code, version, userId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
version: rolled.version,
|
||||
status: 'ACTIVE',
|
||||
message: `Prompt ${code} rolled back to v${version}`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] rollbackPrompt error:', error);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to rollback prompt',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置调试模式
|
||||
* POST /api/admin/prompts/debug
|
||||
*
|
||||
* 需要权限: prompt:debug
|
||||
*/
|
||||
export async function setDebugMode(
|
||||
request: FastifyRequest<{ Body: SetDebugModeBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { modules, enabled } = request.body;
|
||||
const userId = (request as any).user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'User not authenticated',
|
||||
});
|
||||
}
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
promptService.setDebugMode(userId, modules, enabled);
|
||||
|
||||
const state = promptService.getDebugState(userId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
userId,
|
||||
modules: state ? Array.from(state.modules) : [],
|
||||
enabled,
|
||||
message: enabled
|
||||
? `Debug mode enabled for modules: [${modules.join(', ')}]`
|
||||
: 'Debug mode disabled',
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] setDebugMode error:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to set debug mode',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的调试状态
|
||||
* GET /api/admin/prompts/debug
|
||||
*/
|
||||
export async function getDebugStatus(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = (request as any).user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
success: false,
|
||||
error: 'User not authenticated',
|
||||
});
|
||||
}
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
const state = promptService.getDebugState(userId);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
userId,
|
||||
isDebugging: !!state,
|
||||
modules: state ? Array.from(state.modules) : [],
|
||||
enabledAt: state?.enabledAt,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] getDebugStatus error:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to get debug status',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试渲染 Prompt
|
||||
* POST /api/admin/prompts/test-render
|
||||
*/
|
||||
export async function testRender(
|
||||
request: FastifyRequest<{ Body: TestRenderBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { content, variables } = request.body;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
|
||||
// 提取变量
|
||||
const extractedVars = promptService.extractVariables(content);
|
||||
|
||||
// 校验变量
|
||||
const validation = promptService.validateVariables(content, variables);
|
||||
|
||||
// 渲染
|
||||
const rendered = promptService.render(content, variables);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
rendered,
|
||||
extractedVariables: extractedVars,
|
||||
validation,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] testRender error:', error);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to render prompt',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
* POST /api/admin/prompts/:code/invalidate-cache
|
||||
*/
|
||||
export async function invalidateCache(
|
||||
request: FastifyRequest<{ Params: GetPromptParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { code } = request.params;
|
||||
|
||||
const promptService = getPromptService(prisma);
|
||||
promptService.invalidateCache(code);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
code,
|
||||
message: `Cache invalidated for ${code}`,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[PromptController] invalidateCache error:', error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
error: error.message || 'Failed to invalidate cache',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
100
backend/src/common/prompt/prompt.fallbacks.ts
Normal file
100
backend/src/common/prompt/prompt.fallbacks.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 兜底 Prompt(Hardcoded Fallbacks)
|
||||
*
|
||||
* 三级容灾机制的最后一道防线:
|
||||
* 1. 正常:从数据库获取 ACTIVE 版本
|
||||
* 2. 缓存:数据库不可用时使用缓存
|
||||
* 3. 兜底:缓存也失效时使用这里的 hardcoded 版本
|
||||
*
|
||||
* ⚠️ 注意:这里的 Prompt 是最基础版本,仅保证系统不崩溃
|
||||
* 实际生产环境应该始终使用数据库中的版本
|
||||
*/
|
||||
|
||||
import type { ModelConfig } from './prompt.types.js';
|
||||
|
||||
interface FallbackPrompt {
|
||||
content: string;
|
||||
modelConfig: ModelConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* RVW 模块兜底 Prompt
|
||||
*/
|
||||
const RVW_FALLBACKS: Record<string, FallbackPrompt> = {
|
||||
RVW_EDITORIAL: {
|
||||
content: `你是一位专业的医学期刊编辑,负责评估稿件的规范性。
|
||||
|
||||
【评估标准】
|
||||
1. 文稿科学性与实用性
|
||||
2. 文题(中文不超过20字,英文不超过10实词)
|
||||
3. 作者格式
|
||||
4. 摘要(300-500字,含目的、方法、结果、结论)
|
||||
5. 关键词(2-5个)
|
||||
6. 医学名词和药物名称
|
||||
7. 缩略语
|
||||
8. 计量单位
|
||||
9. 图片格式
|
||||
10. 动态图像
|
||||
11. 参考文献
|
||||
|
||||
请输出JSON格式的评估结果,包含overall_score和items数组。`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
|
||||
},
|
||||
|
||||
RVW_METHODOLOGY: {
|
||||
content: `你是一位资深的医学统计学专家,负责评估稿件的方法学质量。
|
||||
|
||||
【评估框架】
|
||||
第一部分:科研设计评估(研究类型、对象、对照、质控)
|
||||
第二部分:统计学方法描述(软件、方法、混杂因素)
|
||||
第三部分:统计分析评估(方法正确性、结果描述)
|
||||
|
||||
请输出JSON格式的评估结果,包含overall_score和parts数组。`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* ASL 模块兜底 Prompt(预留)
|
||||
*/
|
||||
const ASL_FALLBACKS: Record<string, FallbackPrompt> = {
|
||||
ASL_SCREENING: {
|
||||
content: `你是一位文献筛选专家,负责根据纳入排除标准筛选文献。
|
||||
|
||||
请根据提供的标准对文献进行筛选,输出JSON格式的结果。`,
|
||||
modelConfig: { model: 'deepseek-v3', temperature: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 所有模块的兜底 Prompt 汇总
|
||||
*/
|
||||
export const FALLBACK_PROMPTS: Record<string, FallbackPrompt> = {
|
||||
...RVW_FALLBACKS,
|
||||
...ASL_FALLBACKS,
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取兜底 Prompt
|
||||
*
|
||||
* @param code Prompt 代码
|
||||
* @returns 兜底 Prompt 或 undefined
|
||||
*/
|
||||
export function getFallbackPrompt(code: string): FallbackPrompt | undefined {
|
||||
return FALLBACK_PROMPTS[code];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有兜底 Prompt
|
||||
*/
|
||||
export function hasFallbackPrompt(code: string): boolean {
|
||||
return code in FALLBACK_PROMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有兜底 Prompt 的代码列表
|
||||
*/
|
||||
export function getAllFallbackCodes(): string[] {
|
||||
return Object.keys(FALLBACK_PROMPTS);
|
||||
}
|
||||
|
||||
223
backend/src/common/prompt/prompt.routes.ts
Normal file
223
backend/src/common/prompt/prompt.routes.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Prompt 管理 API 路由
|
||||
*
|
||||
* 路由前缀: /api/admin/prompts
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import {
|
||||
listPrompts,
|
||||
getPromptDetail,
|
||||
saveDraft,
|
||||
publishPrompt,
|
||||
rollbackPrompt,
|
||||
setDebugMode,
|
||||
getDebugStatus,
|
||||
testRender,
|
||||
invalidateCache,
|
||||
} from './prompt.controller.js';
|
||||
|
||||
// Schema 定义
|
||||
const listPromptsSchema = {
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
module: { type: 'string', description: '过滤模块,如 RVW, ASL, DC' },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
data: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number' },
|
||||
code: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
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' },
|
||||
},
|
||||
},
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getPromptDetailSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
},
|
||||
required: ['code'],
|
||||
},
|
||||
};
|
||||
|
||||
const saveDraftSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
},
|
||||
required: ['code'],
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Prompt 内容(支持 Handlebars)' },
|
||||
modelConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
temperature: { type: 'number' },
|
||||
maxTokens: { type: 'number' },
|
||||
},
|
||||
},
|
||||
changelog: { type: 'string', description: '变更说明' },
|
||||
},
|
||||
required: ['content'],
|
||||
},
|
||||
};
|
||||
|
||||
const publishSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
},
|
||||
required: ['code'],
|
||||
},
|
||||
};
|
||||
|
||||
const rollbackSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
},
|
||||
required: ['code'],
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
version: { type: 'number', description: '目标版本号' },
|
||||
},
|
||||
required: ['version'],
|
||||
},
|
||||
};
|
||||
|
||||
const setDebugModeSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
modules: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: '要调试的模块列表,如 ["RVW"] 或 ["ALL"]',
|
||||
},
|
||||
enabled: { type: 'boolean', description: '是否开启调试模式' },
|
||||
},
|
||||
required: ['modules', 'enabled'],
|
||||
},
|
||||
};
|
||||
|
||||
const testRenderSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Prompt 模板内容' },
|
||||
variables: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
description: '变量键值对',
|
||||
},
|
||||
},
|
||||
required: ['content', 'variables'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册 Prompt 管理路由
|
||||
*/
|
||||
export async function promptRoutes(fastify: FastifyInstance) {
|
||||
// 列表
|
||||
fastify.get('/', {
|
||||
schema: listPromptsSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:view')],
|
||||
handler: listPrompts,
|
||||
});
|
||||
|
||||
// 详情
|
||||
fastify.get('/:code', {
|
||||
schema: getPromptDetailSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:view')],
|
||||
handler: getPromptDetail,
|
||||
});
|
||||
|
||||
// 保存草稿(需要 prompt:edit)
|
||||
fastify.post('/:code/draft', {
|
||||
schema: saveDraftSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
|
||||
handler: saveDraft,
|
||||
});
|
||||
|
||||
// 发布(需要 prompt:publish)
|
||||
fastify.post('/:code/publish', {
|
||||
schema: publishSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:publish')],
|
||||
handler: publishPrompt,
|
||||
});
|
||||
|
||||
// 回滚(需要 prompt:publish)
|
||||
fastify.post('/:code/rollback', {
|
||||
schema: rollbackSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:publish')],
|
||||
handler: rollbackPrompt,
|
||||
});
|
||||
|
||||
// 调试模式 - 获取状态
|
||||
fastify.get('/debug', {
|
||||
// preHandler: [fastify.authenticate],
|
||||
handler: getDebugStatus,
|
||||
});
|
||||
|
||||
// 调试模式 - 设置(需要 prompt:debug)
|
||||
fastify.post('/debug', {
|
||||
schema: setDebugModeSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:debug')],
|
||||
handler: setDebugMode,
|
||||
});
|
||||
|
||||
// 测试渲染
|
||||
fastify.post('/test-render', {
|
||||
schema: testRenderSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
|
||||
handler: testRender,
|
||||
});
|
||||
|
||||
// 清除缓存
|
||||
fastify.post('/:code/invalidate-cache', {
|
||||
schema: getPromptDetailSchema,
|
||||
// preHandler: [fastify.authenticate, fastify.requirePermission('prompt:edit')],
|
||||
handler: invalidateCache,
|
||||
});
|
||||
}
|
||||
|
||||
export default promptRoutes;
|
||||
|
||||
595
backend/src/common/prompt/prompt.service.ts
Normal file
595
backend/src/common/prompt/prompt.service.ts
Normal file
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* Prompt 管理服务
|
||||
*
|
||||
* 核心功能:
|
||||
* 1. 灰度预览:调试模式下返回 DRAFT,正常返回 ACTIVE
|
||||
* 2. 变量渲染:Handlebars 模板引擎
|
||||
* 3. 三级容灾:数据库 → 缓存 → 兜底
|
||||
* 4. 热更新:Postgres LISTEN/NOTIFY
|
||||
* 5. 变量校验:自动提取和验证变量
|
||||
*/
|
||||
|
||||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
import Handlebars from 'handlebars';
|
||||
import type {
|
||||
RenderedPrompt,
|
||||
GetPromptOptions,
|
||||
ModelConfig,
|
||||
VariableValidation,
|
||||
DebugState,
|
||||
} from './prompt.types.js';
|
||||
import { getFallbackPrompt } from './prompt.fallbacks.js';
|
||||
|
||||
// 默认模型配置
|
||||
const DEFAULT_MODEL_CONFIG: ModelConfig = {
|
||||
model: 'deepseek-v3',
|
||||
temperature: 0.3,
|
||||
};
|
||||
|
||||
// 缓存 TTL(秒)
|
||||
const CACHE_TTL = 5 * 60; // 5分钟
|
||||
|
||||
export class PromptService {
|
||||
private prisma: PrismaClient;
|
||||
private cache: Map<string, { data: any; expiresAt: number }>;
|
||||
private debugStates: Map<string, DebugState>; // userId -> DebugState
|
||||
private notifyClient: any; // 用于 LISTEN/NOTIFY
|
||||
|
||||
constructor(prisma: PrismaClient) {
|
||||
this.prisma = prisma;
|
||||
this.cache = new Map();
|
||||
this.debugStates = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 核心方法:获取渲染后的 Prompt
|
||||
*
|
||||
* 灰度逻辑:
|
||||
* - 如果用户开启了该模块的调试模式,返回 DRAFT
|
||||
* - 否则返回 ACTIVE
|
||||
*
|
||||
* @param code Prompt 代码,如 'RVW_EDITORIAL'
|
||||
* @param variables 模板变量
|
||||
* @param options 选项(userId 用于判断调试模式)
|
||||
*/
|
||||
async get(
|
||||
code: string,
|
||||
variables: Record<string, any> = {},
|
||||
options: GetPromptOptions = {}
|
||||
): Promise<RenderedPrompt> {
|
||||
const { userId, skipCache = false } = options;
|
||||
|
||||
try {
|
||||
// 1. 判断是否处于调试模式
|
||||
const isDebugging = userId ? this.isDebugging(userId, code) : false;
|
||||
|
||||
// 2. 获取 Prompt 版本
|
||||
let version;
|
||||
if (isDebugging) {
|
||||
// 调试模式:优先获取 DRAFT
|
||||
version = await this.getDraftVersion(code);
|
||||
if (!version) {
|
||||
// 没有 DRAFT,降级到 ACTIVE
|
||||
version = await this.getActiveVersion(code, skipCache);
|
||||
}
|
||||
} else {
|
||||
// 正常模式:获取 ACTIVE
|
||||
version = await this.getActiveVersion(code, skipCache);
|
||||
}
|
||||
|
||||
// 3. 如果数据库获取失败,使用兜底
|
||||
if (!version) {
|
||||
console.warn(`[PromptService] Fallback to hardcoded prompt: ${code}`);
|
||||
const fallback = getFallbackPrompt(code);
|
||||
if (!fallback) {
|
||||
throw new Error(`Prompt not found and no fallback available: ${code}`);
|
||||
}
|
||||
return {
|
||||
content: this.render(fallback.content, variables),
|
||||
modelConfig: fallback.modelConfig,
|
||||
version: 0,
|
||||
isDraft: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 渲染模板
|
||||
const content = this.render(version.content, variables);
|
||||
const modelConfig = (version.model_config as ModelConfig) || DEFAULT_MODEL_CONFIG;
|
||||
|
||||
return {
|
||||
content,
|
||||
modelConfig,
|
||||
version: version.version,
|
||||
isDraft: version.status === 'DRAFT',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[PromptService] Error getting prompt ${code}:`, error);
|
||||
|
||||
// 最后的兜底
|
||||
const fallback = getFallbackPrompt(code);
|
||||
if (fallback) {
|
||||
return {
|
||||
content: this.render(fallback.content, variables),
|
||||
modelConfig: fallback.modelConfig,
|
||||
version: 0,
|
||||
isDraft: false,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ACTIVE 版本(带缓存)
|
||||
*/
|
||||
async getActiveVersion(code: string, skipCache = false) {
|
||||
const cacheKey = `prompt:active:${code}`;
|
||||
|
||||
// 检查缓存
|
||||
if (!skipCache) {
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
include: {
|
||||
versions: {
|
||||
where: { status: 'ACTIVE' },
|
||||
orderBy: { version: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template || template.versions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const version = template.versions[0];
|
||||
|
||||
// 写入缓存
|
||||
this.setCache(cacheKey, version, CACHE_TTL);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DRAFT 版本(不缓存,总是实时)
|
||||
*/
|
||||
async getDraftVersion(code: string) {
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
include: {
|
||||
versions: {
|
||||
where: { status: 'DRAFT' },
|
||||
orderBy: { version: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template || template.versions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return template.versions[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板(Handlebars)
|
||||
*/
|
||||
render(template: string, variables: Record<string, any>): string {
|
||||
try {
|
||||
const compiled = Handlebars.compile(template, { noEscape: true });
|
||||
return compiled(variables);
|
||||
} catch (error) {
|
||||
console.error('[PromptService] Template render error:', error);
|
||||
// 渲染失败返回原始模板
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从内容中提取变量名
|
||||
*
|
||||
* 支持:
|
||||
* - {{variable}}
|
||||
* - {{#if variable}}...{{/if}}
|
||||
* - {{#each items}}...{{/each}}
|
||||
*/
|
||||
extractVariables(content: string): string[] {
|
||||
const regex = /\{\{([^#/][^}]*)\}\}/g;
|
||||
const variables = new Set<string>();
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const varName = match[1].trim();
|
||||
// 排除 Handlebars 助手函数
|
||||
if (!varName.startsWith('#') && !varName.startsWith('/') && !varName.includes(' ')) {
|
||||
variables.add(varName);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验变量完整性
|
||||
*/
|
||||
validateVariables(
|
||||
content: string,
|
||||
providedVariables: Record<string, any>
|
||||
): VariableValidation {
|
||||
const expectedVars = this.extractVariables(content);
|
||||
const providedKeys = Object.keys(providedVariables);
|
||||
|
||||
const missingVariables = expectedVars.filter(v => !providedKeys.includes(v));
|
||||
const extraVariables = providedKeys.filter(v => !expectedVars.includes(v));
|
||||
|
||||
return {
|
||||
isValid: missingVariables.length === 0,
|
||||
missingVariables,
|
||||
extraVariables,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 调试模式管理 ====================
|
||||
|
||||
/**
|
||||
* 设置调试模式
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param modules 要调试的模块列表,如 ['RVW'] 或 ['ALL']
|
||||
* @param enabled 是否开启
|
||||
*/
|
||||
setDebugMode(userId: string, modules: string[], enabled: boolean): void {
|
||||
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(', ')}]`);
|
||||
} else {
|
||||
this.debugStates.delete(userId);
|
||||
console.log(`[PromptService] Debug mode disabled for user ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在某模块的调试模式
|
||||
*/
|
||||
isDebugging(userId: string, code: string): boolean {
|
||||
const state = this.debugStates.get(userId);
|
||||
if (!state) return false;
|
||||
|
||||
// 提取模块名(如 'RVW_EDITORIAL' -> 'RVW')
|
||||
const module = code.split('_')[0];
|
||||
|
||||
// 检查是否调试全部或指定模块
|
||||
return state.modules.has('ALL') || state.modules.has(module);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的调试状态
|
||||
*/
|
||||
getDebugState(userId: string): DebugState | null {
|
||||
return this.debugStates.get(userId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有调试中的用户
|
||||
*/
|
||||
getAllDebugUsers(): string[] {
|
||||
return Array.from(this.debugStates.keys());
|
||||
}
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
private getFromCache(key: string): any {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
private setCache(key: string, data: any, ttlSeconds: number): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
expiresAt: Date.now() + ttlSeconds * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定 Prompt 的缓存
|
||||
*/
|
||||
invalidateCache(code: string): void {
|
||||
const cacheKey = `prompt:active:${code}`;
|
||||
this.cache.delete(cacheKey);
|
||||
console.log(`[PromptService] Cache invalidated for: ${code}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
clearAllCache(): void {
|
||||
this.cache.clear();
|
||||
console.log('[PromptService] All cache cleared');
|
||||
}
|
||||
|
||||
// ==================== LISTEN/NOTIFY 热更新 ====================
|
||||
|
||||
/**
|
||||
* 启动 Postgres LISTEN/NOTIFY 监听
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 在数据库中创建触发器:
|
||||
* CREATE OR REPLACE FUNCTION notify_prompt_change() RETURNS trigger AS $$
|
||||
* BEGIN
|
||||
* PERFORM pg_notify('prompt_changed', NEW.code);
|
||||
* RETURN NEW;
|
||||
* END;
|
||||
* $$ LANGUAGE plpgsql;
|
||||
*
|
||||
* CREATE TRIGGER prompt_change_trigger
|
||||
* AFTER INSERT OR UPDATE ON capability_schema.prompt_versions
|
||||
* FOR EACH ROW EXECUTE FUNCTION notify_prompt_change();
|
||||
*
|
||||
* 2. 调用 promptService.startListening()
|
||||
*/
|
||||
async startListening(): Promise<void> {
|
||||
// 注意:Prisma 不直接支持 LISTEN,需要使用原生 pg 客户端
|
||||
// 这里先留空,后续如果需要可以实现
|
||||
console.log('[PromptService] LISTEN/NOTIFY not yet implemented - using manual cache invalidation');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止监听
|
||||
*/
|
||||
async stopListening(): Promise<void> {
|
||||
if (this.notifyClient) {
|
||||
await this.notifyClient.end();
|
||||
this.notifyClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 管理功能 ====================
|
||||
|
||||
/**
|
||||
* 获取所有模板列表
|
||||
*/
|
||||
async listTemplates(module?: string) {
|
||||
const where = module ? { module } : {};
|
||||
|
||||
return this.prisma.prompt_templates.findMany({
|
||||
where,
|
||||
include: {
|
||||
versions: {
|
||||
orderBy: { version: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
orderBy: { code: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板详情(含所有版本)
|
||||
*/
|
||||
async getTemplateDetail(code: string) {
|
||||
return this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
include: {
|
||||
versions: {
|
||||
orderBy: { version: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存草稿
|
||||
*/
|
||||
async saveDraft(
|
||||
code: string,
|
||||
content: string,
|
||||
modelConfig?: ModelConfig,
|
||||
changelog?: string,
|
||||
createdBy?: string
|
||||
) {
|
||||
// 获取模板
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
include: {
|
||||
versions: {
|
||||
orderBy: { version: 'desc' },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`Template not found: ${code}`);
|
||||
}
|
||||
|
||||
// 自动提取变量
|
||||
const variables = this.extractVariables(content);
|
||||
|
||||
// 更新模板的变量字段
|
||||
await this.prisma.prompt_templates.update({
|
||||
where: { code },
|
||||
data: { variables },
|
||||
});
|
||||
|
||||
// 计算新版本号
|
||||
const latestVersion = template.versions[0];
|
||||
const newVersion = latestVersion ? latestVersion.version + 1 : 1;
|
||||
|
||||
// 检查是否已有 DRAFT
|
||||
const existingDraft = await this.prisma.prompt_versions.findFirst({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'DRAFT',
|
||||
},
|
||||
});
|
||||
|
||||
if (existingDraft) {
|
||||
// 更新现有 DRAFT
|
||||
return this.prisma.prompt_versions.update({
|
||||
where: { id: existingDraft.id },
|
||||
data: {
|
||||
content,
|
||||
model_config: (modelConfig || existingDraft.model_config) as unknown as Prisma.InputJsonValue,
|
||||
changelog,
|
||||
created_by: createdBy,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 创建新 DRAFT
|
||||
return this.prisma.prompt_versions.create({
|
||||
data: {
|
||||
template_id: template.id,
|
||||
version: newVersion,
|
||||
content,
|
||||
model_config: (modelConfig || DEFAULT_MODEL_CONFIG) as unknown as Prisma.InputJsonValue,
|
||||
status: 'DRAFT',
|
||||
changelog,
|
||||
created_by: createdBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布草稿(DRAFT → ACTIVE)
|
||||
*
|
||||
* 需要 prompt:publish 权限
|
||||
*/
|
||||
async publish(code: string, createdBy?: string) {
|
||||
// 获取 DRAFT
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`Template not found: ${code}`);
|
||||
}
|
||||
|
||||
const draft = await this.prisma.prompt_versions.findFirst({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'DRAFT',
|
||||
},
|
||||
});
|
||||
|
||||
if (!draft) {
|
||||
throw new Error(`No draft found for: ${code}`);
|
||||
}
|
||||
|
||||
// 事务:归档旧 ACTIVE,激活新版本
|
||||
await this.prisma.$transaction([
|
||||
// 1. 归档当前 ACTIVE
|
||||
this.prisma.prompt_versions.updateMany({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
data: {
|
||||
status: 'ARCHIVED',
|
||||
},
|
||||
}),
|
||||
// 2. 激活 DRAFT
|
||||
this.prisma.prompt_versions.update({
|
||||
where: { id: draft.id },
|
||||
data: {
|
||||
status: 'ACTIVE',
|
||||
created_by: createdBy,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// 清除缓存
|
||||
this.invalidateCache(code);
|
||||
|
||||
console.log(`[PromptService] Published: ${code} (v${draft.version})`);
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚到指定版本
|
||||
*/
|
||||
async rollback(code: string, targetVersion: number, createdBy?: string) {
|
||||
const template = await this.prisma.prompt_templates.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`Template not found: ${code}`);
|
||||
}
|
||||
|
||||
// 获取目标版本
|
||||
const targetVersionRecord = await this.prisma.prompt_versions.findFirst({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
version: targetVersion,
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetVersionRecord) {
|
||||
throw new Error(`Version ${targetVersion} not found for: ${code}`);
|
||||
}
|
||||
|
||||
// 事务:归档当前 ACTIVE,重新激活目标版本
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.prompt_versions.updateMany({
|
||||
where: {
|
||||
template_id: template.id,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
data: {
|
||||
status: 'ARCHIVED',
|
||||
},
|
||||
}),
|
||||
this.prisma.prompt_versions.update({
|
||||
where: { id: targetVersionRecord.id },
|
||||
data: {
|
||||
status: 'ACTIVE',
|
||||
created_by: createdBy,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// 清除缓存
|
||||
this.invalidateCache(code);
|
||||
|
||||
console.log(`[PromptService] Rolled back: ${code} to v${targetVersion}`);
|
||||
|
||||
return targetVersionRecord;
|
||||
}
|
||||
}
|
||||
|
||||
// 单例模式
|
||||
let instance: PromptService | null = null;
|
||||
|
||||
export function getPromptService(prisma: PrismaClient): PromptService {
|
||||
if (!instance) {
|
||||
instance = new PromptService(prisma);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function resetPromptService(): void {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
69
backend/src/common/prompt/prompt.types.ts
Normal file
69
backend/src/common/prompt/prompt.types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Prompt 管理系统类型定义
|
||||
*/
|
||||
|
||||
// Prompt 状态
|
||||
export type PromptStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
|
||||
|
||||
// 模型配置
|
||||
export interface ModelConfig {
|
||||
model: string; // 模型名称,如 'deepseek-v3'
|
||||
temperature?: number; // 温度参数
|
||||
maxTokens?: number; // 最大输出token
|
||||
topP?: number; // Top-P采样
|
||||
fallback?: string; // 降级模型
|
||||
}
|
||||
|
||||
// Prompt 模板
|
||||
export interface PromptTemplate {
|
||||
id: number;
|
||||
code: string; // 唯一标识符,如 'RVW_EDITORIAL'
|
||||
name: string; // 人类可读名称
|
||||
module: string; // 所属模块: RVW, ASL, DC, IIT, PKB, AIA
|
||||
description?: string;
|
||||
variables?: string[]; // 预期变量列表
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Prompt 版本
|
||||
export interface PromptVersion {
|
||||
id: number;
|
||||
templateId: number;
|
||||
version: number;
|
||||
content: string; // Prompt 内容(支持 Handlebars)
|
||||
modelConfig?: ModelConfig;
|
||||
status: PromptStatus;
|
||||
changelog?: string;
|
||||
createdBy?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// 渲染后的 Prompt
|
||||
export interface RenderedPrompt {
|
||||
content: string; // 渲染后的内容
|
||||
modelConfig: ModelConfig;
|
||||
version: number;
|
||||
isDraft: boolean; // 是否来自草稿(调试模式)
|
||||
}
|
||||
|
||||
// 调试模式状态
|
||||
export interface DebugState {
|
||||
userId: string;
|
||||
modules: Set<string>; // 'RVW', 'ASL', 'DC', 'ALL'
|
||||
enabledAt: Date;
|
||||
}
|
||||
|
||||
// 获取 Prompt 的选项
|
||||
export interface GetPromptOptions {
|
||||
userId?: string; // 用于判断调试模式
|
||||
skipCache?: boolean; // 跳过缓存
|
||||
}
|
||||
|
||||
// 变量校验结果
|
||||
export interface VariableValidation {
|
||||
isValid: boolean;
|
||||
missingVariables: string[];
|
||||
extraVariables: string[];
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import { registerDCRoutes, initDCModule } from './modules/dc/index.js';
|
||||
import pkbRoutes from './modules/pkb/routes/index.js';
|
||||
import { registerHealthRoutes } from './common/health/index.js';
|
||||
import { logger } from './common/logging/index.js';
|
||||
import { authRoutes, registerAuthPlugin } from './common/auth/index.js';
|
||||
import { promptRoutes } from './common/prompt/index.js';
|
||||
import { registerTestRoutes } from './test-platform-api.js';
|
||||
import { registerScreeningWorkers } from './modules/asl/services/screeningWorker.js';
|
||||
import { registerExtractionWorkers } from './modules/dc/tool-b/workers/extractionWorker.js';
|
||||
@@ -78,6 +80,19 @@ console.log('✅ 文件上传插件已配置: 最大文件大小 10MB');
|
||||
await registerHealthRoutes(fastify);
|
||||
logger.info('✅ 健康检查路由已注册');
|
||||
|
||||
// ============================================
|
||||
// 【平台基础设施】认证模块
|
||||
// ============================================
|
||||
await registerAuthPlugin(fastify);
|
||||
await fastify.register(authRoutes, { prefix: '/api/v1/auth' });
|
||||
logger.info('✅ 认证路由已注册: /api/v1/auth');
|
||||
|
||||
// ============================================
|
||||
// 【运营管理】Prompt管理模块
|
||||
// ============================================
|
||||
await fastify.register(promptRoutes, { prefix: '/api/admin/prompts' });
|
||||
logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
|
||||
|
||||
// ============================================
|
||||
// 【临时】平台基础设施测试API
|
||||
// ============================================
|
||||
|
||||
@@ -349,5 +349,6 @@ runTests().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -290,5 +290,6 @@ runTest()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -328,5 +328,6 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -264,5 +264,6 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -214,5 +214,6 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -268,5 +268,6 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -179,3 +179,4 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -113,3 +113,4 @@ checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -100,3 +100,4 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -82,3 +82,4 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -539,3 +539,4 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -174,3 +174,4 @@ console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -491,3 +491,4 @@ export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -136,3 +136,4 @@ testDifyIntegration().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -165,3 +165,4 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -151,3 +151,4 @@ if (hasError) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -177,3 +177,4 @@ async function testUrlVerification() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -258,3 +258,4 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -142,3 +142,4 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -235,3 +235,4 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,4 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -126,3 +126,4 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -111,3 +111,4 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v2/rvw/tasks/{taskId}" -Foregr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,3 +25,4 @@ export * from './services/utils.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,3 +46,4 @@ export default async function rvwRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,4 @@ export async function reviewEditorialStandards(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,4 @@ export async function reviewMethodology(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -116,3 +116,4 @@ export function validateAgentSelection(agents: string[]): void {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -414,5 +414,6 @@ SET session_replication_role = 'origin';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -116,5 +116,6 @@ WHERE key = 'verify_test';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -259,5 +259,6 @@ verifyDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
backend/src/types/global.d.ts
vendored
1
backend/src/types/global.d.ts
vendored
@@ -49,5 +49,6 @@ export {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user