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:
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user