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

@@ -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();