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,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',
});
}
}