feat(aia): Integrate PromptService for 10 AI agents

Features:
- Migrate 10 agent prompts from hardcoded to database
- Add grayscale preview support (DRAFT/ACTIVE distribution)
- Implement 3-tier fallback (DB -> Cache -> Hardcoded)
- Add version management and rollback capability

Files changed:
- backend/scripts/migrate-aia-prompts.ts (new migration script)
- backend/src/common/prompt/prompt.fallbacks.ts (add AIA fallbacks)
- backend/src/modules/aia/services/agentService.ts (integrate PromptService)
- backend/src/modules/aia/services/conversationService.ts (pass userId)
- backend/src/modules/aia/types/index.ts (fix AgentStage type)

Documentation:
- docs/03-业务模块/AIA-AI智能问答/06-开发记录/2026-01-18-Prompt管理系统集成.md
- docs/02-通用能力层/00-通用能力层清单.md (add FileCard, Prompt management)
- docs/00-系统总体设计/00-系统当前状态与开发指南.md (update to v3.6)

Prompt codes:
- AIA_SCIENTIFIC_QUESTION, AIA_PICO_ANALYSIS, AIA_TOPIC_EVALUATION
- AIA_OUTCOME_DESIGN, AIA_CRF_DESIGN, AIA_SAMPLE_SIZE
- AIA_PROTOCOL_WRITING, AIA_METHODOLOGY_REVIEW
- AIA_PAPER_POLISH, AIA_PAPER_TRANSLATE

Tested: Migration script executed, all 10 prompts inserted successfully
This commit is contained in:
2026-01-18 15:48:53 +08:00
parent 66255368b7
commit 57fdc6ef00
290 changed files with 2950 additions and 106 deletions

View File

@@ -46,6 +46,9 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2

View File

@@ -276,6 +276,9 @@

View File

@@ -224,5 +224,8 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -153,5 +153,8 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -55,4 +55,7 @@

View File

@@ -314,5 +314,8 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts

View File

@@ -176,5 +176,8 @@ npm run dev

View File

@@ -55,3 +55,6 @@ main()

View File

@@ -49,3 +49,6 @@ main()

View File

@@ -44,3 +44,6 @@ main()

View File

@@ -76,3 +76,6 @@ main()

View File

@@ -39,3 +39,6 @@ main()

View File

@@ -80,3 +80,6 @@ main()

View File

@@ -27,3 +27,6 @@ main()

View File

@@ -115,3 +115,6 @@ main()

View File

@@ -86,3 +86,6 @@ main()

View File

@@ -72,3 +72,6 @@ main()

View File

@@ -114,3 +114,6 @@ main()

View File

@@ -25,3 +25,6 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -57,3 +57,6 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -71,6 +71,9 @@ WHERE table_schema = 'dc_schema'

View File

@@ -109,6 +109,9 @@ ORDER BY ordinal_position;

View File

@@ -122,6 +122,9 @@ runMigration()

View File

@@ -56,6 +56,9 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名

View File

@@ -83,6 +83,9 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

View File

@@ -123,6 +123,9 @@ Write-Host ""

View File

@@ -233,6 +233,9 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -34,3 +34,6 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (

View File

@@ -108,3 +108,6 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS

View File

@@ -252,6 +252,9 @@ checkDCTables();

View File

@@ -9,3 +9,6 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;

View File

@@ -204,6 +204,9 @@ createAiHistoryTable()

View File

@@ -191,6 +191,9 @@ createToolCTable()

View File

@@ -188,6 +188,9 @@ createToolCTable()

View File

@@ -0,0 +1,317 @@
/**
* AIA 智能问答模块 Prompt 迁移脚本
*
* 将 agentService.ts 中硬编码的 10 个智能体 Prompt 迁移到数据库
*
* 迁移内容:
* 1. AIA_SCIENTIFIC_QUESTION - 科学问题梳理
* 2. AIA_PICO_ANALYSIS - PICO 梳理
* 3. AIA_TOPIC_EVALUATION - 选题评价
* 4. AIA_OUTCOME_DESIGN - 观察指标设计
* 5. AIA_CRF_DESIGN - 病例报告表设计
* 6. AIA_SAMPLE_SIZE - 样本量计算
* 7. AIA_PROTOCOL_WRITING - 临床研究方案撰写
* 8. AIA_METHODOLOGY_REVIEW - 方法学评审智能体
* 9. AIA_PAPER_POLISH - 论文润色
* 10. AIA_PAPER_TRANSLATE - 论文翻译
*
* 运行方式:
* cd backend && npx tsx scripts/migrate-aia-prompts.ts
*/
import { PrismaClient, PromptStatus } from '@prisma/client';
const prisma = new PrismaClient();
// AIA Prompt 配置10 个智能体)
const aiaPrompts = [
// ==================== Phase 1: 选题优化智能体 ====================
{
code: 'AIA_SCIENTIFIC_QUESTION',
agentId: 'TOPIC_01',
name: '科学问题梳理',
module: 'AIA',
description: '从科学问题的清晰度、系统性、可验证性等角度使用科学理论对科学问题进行全面的评价。',
content: `你是一个专业的临床研究方法学专家,擅长科学问题的梳理与评价。
你的任务是:
1. 从科学问题的清晰度、系统性、可验证性等角度进行全面评价
2. 使用科学理论和方法学原则指导用户完善问题
3. 提供具体、可操作的建议
请用专业但易懂的语言回答,结构清晰,逻辑严密。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'AIA_PICO_ANALYSIS',
agentId: 'TOPIC_02',
name: 'PICO 梳理',
module: 'AIA',
description: '基于科学问题梳理研究对象、干预(暴露)、对照和结局指标,并评价并给出合理化的建议。',
content: `你是一个 PICO 框架专家,擅长将临床问题拆解为结构化的研究要素。
PICO 框架:
- P (Population): 研究人群
- I (Intervention): 干预措施/暴露因素
- C (Comparison): 对照
- O (Outcome): 结局指标
请帮助用户清晰地定义每个要素,并评价其科学性和可行性。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'AIA_TOPIC_EVALUATION',
agentId: 'TOPIC_03',
name: '选题评价',
module: 'AIA',
description: '从创新性、临床价值、科学性和可行性等方面使用科学理论对临床问题进行全面的评价。',
content: `你是一个临床研究选题评审专家,擅长从多维度评价研究选题。
评价维度:
1. 创新性:是否填补知识空白,有新颖性
2. 临床价值:对临床实践的指导意义
3. 科学性:研究设计的严谨性和可行性
4. 可行性:资源、时间、技术条件
请客观评价,指出优势和改进空间。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
// ==================== Phase 2: 方案设计智能体 ====================
{
code: 'AIA_OUTCOME_DESIGN',
agentId: 'DESIGN_04',
name: '观察指标设计',
module: 'AIA',
description: '基于研究设计和因果关系提供可能的观察指标集。',
content: `你是观察指标设计专家,擅长根据研究目的推荐合适的观察指标。
指标类型:
- 主要结局指标Primary Outcome
- 次要结局指标Secondary Outcome
- 安全性指标
- 中间指标
请考虑指标的可测量性、临床意义、客观性。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'AIA_CRF_DESIGN',
agentId: 'DESIGN_05',
name: '病例报告表设计',
module: 'AIA',
description: '基于研究方案设计梳理观察指标集并给出建议的病例报告表框架。',
content: `你是 CRF (Case Report Form) 设计专家。
CRF 设计要点:
1. 基线资料采集
2. 观察指标记录
3. 随访时间点设计
4. 数据质控要求
请提供结构清晰、逻辑合理的 CRF 框架。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'AIA_SAMPLE_SIZE',
agentId: 'DESIGN_06',
name: '样本量计算',
module: 'AIA',
description: '基于研究阶段和研究设计为研究提供科学合理的样本量估算结果。',
content: `你是样本量计算专家,擅长各种研究设计的样本量估算。
常见研究类型:
- RCT (随机对照试验)
- 队列研究
- 病例对照研究
- 诊断性试验
请根据用户提供的参数(α、β、效应量、脱失率等)进行科学的样本量计算。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'AIA_PROTOCOL_WRITING',
agentId: 'DESIGN_07',
name: '临床研究方案撰写',
module: 'AIA',
description: '基于科学问题、PICOS等信息给出一个初步的临床研究设计方案。',
content: `你是临床研究方案撰写专家,可以帮助用户撰写完整的研究方案。
方案结构:
1. 研究背景与目的
2. 研究设计(类型、盲法、随机等)
3. 研究对象(纳入/排除标准)
4. 干预措施
5. 观察指标
6. 统计分析计划
7. 质量控制
8. 伦理考虑
请基于用户提供的信息,给出结构完整、逻辑严密的方案。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
// ==================== Phase 3: 方案预评审 ====================
{
code: 'AIA_METHODOLOGY_REVIEW',
agentId: 'REVIEW_08',
name: '方法学评审智能体',
module: 'AIA',
description: '从研究问题、研究方案和临床意义方面,对研究进行临床研究方法学的全面评价。',
content: `你是一个资深的临床研究方法学评审专家,模拟审稿人视角进行评审。
评审要点:
1. 研究问题是否明确、有价值
2. 研究设计是否科学、严谨
3. 纳入/排除标准是否合理
4. 样本量是否充足
5. 统计方法是否适当
6. 是否存在偏倚风险
请指出优势和需要改进的地方。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
// ==================== Phase 5: 写作助手 ====================
{
code: 'AIA_PAPER_POLISH',
agentId: 'WRITING_11',
name: '论文润色',
module: 'AIA',
description: '结合目标杂志,提供专业化的润色服务。',
content: `You are a professional academic editor specializing in medical research papers.
Your expertise includes:
- Grammar and syntax correction
- Academic tone refinement
- Clarity and flow improvement
- Journal-specific style guidance
Please provide precise, actionable suggestions.`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
{
code: 'AIA_PAPER_TRANSLATE',
agentId: 'WRITING_12',
name: '论文翻译',
module: 'AIA',
description: '结合目标杂志,提供专业化的翻译并进行润色。',
content: `你是一个专业的医学论文翻译专家,精通中英互译。
翻译要求:
1. 准确传达原意
2. 符合医学术语规范
3. 保持学术风格
4. 流畅自然
请提供地道的学术英语翻译。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
];
// 智能体 ID 到 Prompt Code 的映射表(供 agentService 使用)
export const AGENT_TO_PROMPT_CODE: Record<string, string> = {};
aiaPrompts.forEach(p => {
AGENT_TO_PROMPT_CODE[p.agentId] = p.code;
});
async function main() {
console.log('🚀 开始迁移 AIA Prompt 到数据库...\n');
console.log(`📊 共 ${aiaPrompts.length} 个智能体 Prompt\n`);
for (const prompt of aiaPrompts) {
console.log(`📄 处理: ${prompt.code} (${prompt.name})`);
console.log(` 智能体ID: ${prompt.agentId}`);
console.log(` 📝 内容长度: ${prompt.content.length} 字符`);
// 创建或更新模板
const template = await prisma.prompt_templates.upsert({
where: { code: prompt.code },
update: {
name: prompt.name,
description: prompt.description,
updated_at: new Date(),
},
create: {
code: prompt.code,
name: prompt.name,
module: prompt.module,
description: prompt.description,
variables: null, // 暂不使用变量
},
});
// 检查是否已有 ACTIVE 版本
const existingActive = await prisma.prompt_versions.findFirst({
where: {
template_id: template.id,
status: PromptStatus.ACTIVE,
},
});
if (existingActive) {
console.log(` ✅ 已存在 ACTIVE 版本 (v${existingActive.version})`);
} else {
// 创建第一个 ACTIVE 版本
await prisma.prompt_versions.create({
data: {
template_id: template.id,
version: 1,
content: prompt.content,
model_config: prompt.modelConfig,
status: PromptStatus.ACTIVE,
changelog: '从 agentService.ts 迁移的初始版本',
created_by: 'system-migration',
},
});
console.log(` ✅ 创建 ACTIVE 版本 (v1)`);
}
console.log('');
}
// 验证结果
console.log('═══════════════════════════════════════════════════════');
console.log('📊 迁移结果验证\n');
const templates = await prisma.prompt_templates.findMany({
where: { module: 'AIA' },
include: {
versions: {
orderBy: { version: 'desc' },
take: 1,
},
},
orderBy: { code: 'asc' },
});
console.log(`✅ 共迁移 ${templates.length} 个 AIA Prompt:\n`);
for (const t of templates) {
const latestVersion = t.versions[0];
console.log(` 📋 ${t.code}`);
console.log(` 名称: ${t.name}`);
console.log(` 最新版本: v${latestVersion?.version} (${latestVersion?.status})`);
console.log('');
}
// 输出映射表
console.log('═══════════════════════════════════════════════════════');
console.log('📋 智能体ID → Prompt Code 映射表:\n');
for (const prompt of aiaPrompts) {
console.log(` ${prompt.agentId.padEnd(12)}${prompt.code}`);
}
console.log('\n✅ AIA Prompt 迁移完成!');
console.log('\n💡 下一步:');
console.log(' 1. 修改 agentService.ts 使用 PromptService');
console.log(' 2. 在管理端查看和编辑这些 Prompt');
}
main()
.catch((error) => {
console.error('❌ 迁移失败:', error);
process.exit(1);
})
.finally(() => prisma.$disconnect());

View File

@@ -119,3 +119,6 @@ main()

View File

@@ -339,3 +339,6 @@ runTests().catch(error => {

View File

@@ -85,3 +85,6 @@ testAPI().catch(console.error);

View File

@@ -304,3 +304,6 @@ verifySchemas()

View File

@@ -192,3 +192,6 @@ export const jwtService = new JWTService();

View File

@@ -320,6 +320,9 @@ export function getBatchItems<T>(

View File

@@ -66,12 +66,151 @@ const ASL_FALLBACKS: Record<string, FallbackPrompt> = {
},
};
/**
* AIA 智能问答模块兜底 Prompt
*/
const AIA_FALLBACKS: Record<string, FallbackPrompt> = {
// Phase 1: 选题优化智能体
AIA_SCIENTIFIC_QUESTION: {
content: `你是一个专业的临床研究方法学专家,擅长科学问题的梳理与评价。
你的任务是:
1. 从科学问题的清晰度、系统性、可验证性等角度进行全面评价
2. 使用科学理论和方法学原则指导用户完善问题
3. 提供具体、可操作的建议
请用专业但易懂的语言回答,结构清晰,逻辑严密。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
AIA_PICO_ANALYSIS: {
content: `你是一个 PICO 框架专家,擅长将临床问题拆解为结构化的研究要素。
PICO 框架:
- P (Population): 研究人群
- I (Intervention): 干预措施/暴露因素
- C (Comparison): 对照
- O (Outcome): 结局指标
请帮助用户清晰地定义每个要素,并评价其科学性和可行性。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
AIA_TOPIC_EVALUATION: {
content: `你是一个临床研究选题评审专家,擅长从多维度评价研究选题。
评价维度:
1. 创新性:是否填补知识空白,有新颖性
2. 临床价值:对临床实践的指导意义
3. 科学性:研究设计的严谨性和可行性
4. 可行性:资源、时间、技术条件
请客观评价,指出优势和改进空间。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
// Phase 2: 方案设计智能体
AIA_OUTCOME_DESIGN: {
content: `你是观察指标设计专家,擅长根据研究目的推荐合适的观察指标。
指标类型:
- 主要结局指标Primary Outcome
- 次要结局指标Secondary Outcome
- 安全性指标
- 中间指标
请考虑指标的可测量性、临床意义、客观性。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
AIA_CRF_DESIGN: {
content: `你是 CRF (Case Report Form) 设计专家。
CRF 设计要点:
1. 基线资料采集
2. 观察指标记录
3. 随访时间点设计
4. 数据质控要求
请提供结构清晰、逻辑合理的 CRF 框架。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
AIA_SAMPLE_SIZE: {
content: `你是样本量计算专家,擅长各种研究设计的样本量估算。
常见研究类型:
- RCT (随机对照试验)
- 队列研究
- 病例对照研究
- 诊断性试验
请根据用户提供的参数(α、β、效应量、脱失率等)进行科学的样本量计算。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
AIA_PROTOCOL_WRITING: {
content: `你是临床研究方案撰写专家,可以帮助用户撰写完整的研究方案。
方案结构:
1. 研究背景与目的
2. 研究设计(类型、盲法、随机等)
3. 研究对象(纳入/排除标准)
4. 干预措施
5. 观察指标
6. 统计分析计划
7. 质量控制
8. 伦理考虑
请基于用户提供的信息,给出结构完整、逻辑严密的方案。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
// Phase 3: 方案预评审
AIA_METHODOLOGY_REVIEW: {
content: `你是一个资深的临床研究方法学评审专家,模拟审稿人视角进行评审。
评审要点:
1. 研究问题是否明确、有价值
2. 研究设计是否科学、严谨
3. 纳入/排除标准是否合理
4. 样本量是否充足
5. 统计方法是否适当
6. 是否存在偏倚风险
请指出优势和需要改进的地方。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
// Phase 5: 写作助手
AIA_PAPER_POLISH: {
content: `You are a professional academic editor specializing in medical research papers.
Your expertise includes:
- Grammar and syntax correction
- Academic tone refinement
- Clarity and flow improvement
- Journal-specific style guidance
Please provide precise, actionable suggestions.`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
AIA_PAPER_TRANSLATE: {
content: `你是一个专业的医学论文翻译专家,精通中英互译。
翻译要求:
1. 准确传达原意
2. 符合医学术语规范
3. 保持学术风格
4. 流畅自然
请提供地道的学术英语翻译。`,
modelConfig: { model: 'deepseek-v3', temperature: 0.3 },
},
};
/**
* 所有模块的兜底 Prompt 汇总
*/
export const FALLBACK_PROMPTS: Record<string, FallbackPrompt> = {
...RVW_FALLBACKS,
...ASL_FALLBACKS,
...AIA_FALLBACKS,
};
/**
@@ -106,3 +245,6 @@ export function getAllFallbackCodes(): string[] {

View File

@@ -75,3 +75,6 @@ export interface VariableValidation {

View File

@@ -196,3 +196,6 @@ export function createOpenAIStreamAdapter(

View File

@@ -202,3 +202,6 @@ export async function streamChat(

View File

@@ -20,3 +20,6 @@ export { THINKING_TAGS } from './types';

View File

@@ -95,3 +95,6 @@ export type SSEEventType =

View File

@@ -81,3 +81,6 @@ export async function moduleRoutes(fastify: FastifyInstance) {

View File

@@ -111,3 +111,6 @@ export interface PaginatedResponse<T> {

View File

@@ -158,3 +158,6 @@ export const ROLE_DISPLAY_NAMES: Record<UserRole, string> = {
USER: '普通用户',
};

View File

@@ -233,3 +233,6 @@ async function matchIntent(query: string): Promise<{

View File

@@ -0,0 +1,92 @@
/**
* AIA 智能问答模块 - 附件控制器
* @module aia/controllers/attachmentController
*
* API 端点:
* - POST /api/v1/aia/conversations/:id/attachments 上传附件
*/
import type { FastifyRequest, FastifyReply } from 'fastify';
import { logger } from '../../../common/logging/index.js';
import * as attachmentService from '../services/attachmentService.js';
/**
* 从 JWT Token 获取用户 ID
*/
function getUserId(request: FastifyRequest): string {
const userId = (request as any).user?.userId;
if (!userId) {
throw new Error('User not authenticated');
}
return userId;
}
/**
* 上传附件
* POST /api/v1/aia/conversations/:id/attachments
*/
export async function uploadAttachment(
request: FastifyRequest<{
Params: { id: string };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { id: conversationId } = request.params;
// 获取上传的文件
const data = await request.file();
if (!data) {
return reply.status(400).send({
code: -1,
error: {
code: 'VALIDATION_ERROR',
message: '请上传文件',
},
});
}
const buffer = await data.toBuffer();
logger.info('[AIA:AttachmentController] 上传附件', {
userId,
conversationId,
filename: data.filename,
mimetype: data.mimetype,
size: buffer.length,
});
const attachment = await attachmentService.uploadAttachment(
userId,
conversationId,
{
filename: data.filename,
mimetype: data.mimetype,
buffer,
}
);
return reply.send({
code: 0,
data: attachment,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('[AIA:AttachmentController] 上传附件失败', {
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
});
return reply.status(500).send({
code: -1,
error: {
code: 'INTERNAL_ERROR',
message: errorMessage,
},
});
}
}

View File

@@ -135,22 +135,16 @@ export async function createConversation(
export async function getConversationById(
request: FastifyRequest<{
Params: { id: string };
Querystring: { limit?: string };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { id } = request.params;
const { limit } = request.query;
logger.info('[AIA:Controller] 获取对话详情', { userId, conversationId: id });
const conversation = await conversationService.getConversationById(
userId,
id,
limit ? parseInt(limit) : 50
);
const conversation = await conversationService.getConversationById(userId, id);
if (!conversation) {
return reply.status(404).send({
@@ -178,6 +172,54 @@ export async function getConversationById(
}
}
/**
* 更新对话(标题等)
* PATCH /api/v1/aia/conversations/:id
*/
export async function updateConversation(
request: FastifyRequest<{
Params: { id: string };
Body: { title?: string };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { id } = request.params;
const { title } = request.body;
logger.info('[AIA:Controller] 更新对话', { userId, conversationId: id, title });
if (!title || title.trim().length === 0) {
return reply.status(400).send({
code: -1,
error: {
code: 'VALIDATION_ERROR',
message: '标题不能为空',
},
});
}
const conversation = await conversationService.updateConversation(userId, id, {
title: title.trim(),
});
return reply.send({
code: 0,
data: conversation,
});
} catch (error) {
logger.error('[AIA:Controller] 更新对话失败', { error });
return reply.status(500).send({
code: -1,
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : '服务器内部错误',
},
});
}
}
/**
* 删除对话
* DELETE /api/v1/aia/conversations/:id
@@ -222,7 +264,57 @@ export async function deleteConversation(
}
}
// ==================== 消息发送 ====================
// ==================== 消息管理 ====================
/**
* 获取对话消息列表(历史消息)
* GET /api/v1/aia/conversations/:id/messages
*/
export async function getMessages(
request: FastifyRequest<{
Params: { id: string };
Querystring: {
page?: string;
pageSize?: string;
};
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { id } = request.params;
const { page, pageSize } = request.query;
logger.info('[AIA:Controller] 获取消息列表', { userId, conversationId: id });
const result = await conversationService.getMessages(userId, id, {
page: page ? parseInt(page) : 1,
pageSize: pageSize ? parseInt(pageSize) : 50,
});
return reply.send({
code: 0,
data: {
messages: result.messages,
pagination: {
total: result.total,
page: page ? parseInt(page) : 1,
pageSize: pageSize ? parseInt(pageSize) : 50,
totalPages: Math.ceil(result.total / (pageSize ? parseInt(pageSize) : 50)),
},
},
});
} catch (error) {
logger.error('[AIA:Controller] 获取消息列表失败', { error });
return reply.status(500).send({
code: -1,
error: {
code: 'INTERNAL_ERROR',
message: error instanceof Error ? error.message : '服务器内部错误',
},
});
}
}
/**
* 发送消息(流式输出)

View File

@@ -16,3 +16,6 @@ export { aiaRoutes };

View File

@@ -8,6 +8,7 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import * as conversationController from '../controllers/conversationController.js';
import * as agentController from '../controllers/agentController.js';
import * as attachmentController from '../controllers/attachmentController.js';
import { authenticate } from '../../../common/auth/auth.middleware.js';
export default async function aiaRoutes(fastify: FastifyInstance) {
@@ -53,17 +54,37 @@ export default async function aiaRoutes(fastify: FastifyInstance) {
return conversationController.getConversationById(request as any, reply);
});
// 更新对话(标题等)
// PATCH /api/v1/aia/conversations/:id
fastify.patch('/conversations/:id', { preHandler: [authenticate] }, async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.updateConversation(request as any, reply);
});
// 删除对话
// DELETE /api/v1/aia/conversations/:id
fastify.delete('/conversations/:id', { preHandler: [authenticate] }, async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.deleteConversation(request as any, reply);
});
// ==================== 消息发送 ====================
// ==================== 消息管理 ====================
// 获取对话消息列表(历史消息)
// GET /api/v1/aia/conversations/:id/messages
fastify.get('/conversations/:id/messages', { preHandler: [authenticate] }, async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.getMessages(request as any, reply);
});
// 发送消息(流式输出)
// POST /api/v1/aia/conversations/:id/messages/stream
fastify.post('/conversations/:id/messages/stream', { preHandler: [authenticate] }, async (request: FastifyRequest, reply: FastifyReply) => {
return conversationController.sendMessageStream(request as any, reply);
});
// ==================== 附件管理 ====================
// 上传附件
// POST /api/v1/aia/conversations/:id/attachments
fastify.post('/conversations/:id/attachments', { preHandler: [authenticate] }, async (request: FastifyRequest, reply: FastifyReply) => {
return attachmentController.uploadAttachment(request as any, reply);
});
}

View File

@@ -4,12 +4,38 @@
*
* 负责智能体配置管理、Prompt 获取
* 12个智能体配置对应前端 AgentHub
*
* Phase 3.5.6 改造:使用 PromptService 替代硬编码
* - 支持灰度预览(调试者看 DRAFT普通用户看 ACTIVE
* - 三级容灾(数据库→缓存→兜底)
* - 在管理端可配置和调试
*/
import { logger } from '../../../common/logging/index.js';
import { cache } from '../../../common/cache/index.js';
import { prisma } from '../../../config/database.js';
import { getPromptService } from '../../../common/prompt/index.js';
import type { Agent, AgentStage } from '../types/index.js';
// ==================== 智能体 ID 到 Prompt Code 映射 ====================
/**
* 智能体 ID → Prompt Code 映射表
* 用于从 PromptService 获取对应的提示词
*/
const AGENT_TO_PROMPT_CODE: Record<string, string> = {
'TOPIC_01': 'AIA_SCIENTIFIC_QUESTION',
'TOPIC_02': 'AIA_PICO_ANALYSIS',
'TOPIC_03': 'AIA_TOPIC_EVALUATION',
'DESIGN_04': 'AIA_OUTCOME_DESIGN',
'DESIGN_05': 'AIA_CRF_DESIGN',
'DESIGN_06': 'AIA_SAMPLE_SIZE',
'DESIGN_07': 'AIA_PROTOCOL_WRITING',
'REVIEW_08': 'AIA_METHODOLOGY_REVIEW',
'WRITING_11': 'AIA_PAPER_POLISH',
'WRITING_12': 'AIA_PAPER_TRANSLATE',
};
// ==================== 智能体配置 ====================
/**
@@ -303,9 +329,25 @@ export async function getAgentsByStage(stage: AgentStage): Promise<Agent[]> {
}
/**
* 获取智能体的系统提示词
* 获取智能体的系统提示词(使用 PromptService
*
* 支持灰度预览:
* - 调试者看 DRAFT 版本
* - 普通用户看 ACTIVE 版本
*
* 三级容灾:
* 1. 数据库PromptService
* 2. 缓存
* 3. 兜底(硬编码的 systemPrompt
*
* @param agentId 智能体 ID
* @param userId 用户 ID用于灰度预览判断
* @returns { content: 提示词内容, isDraft: 是否为草稿版本 }
*/
export async function getAgentSystemPrompt(agentId: string): Promise<string> {
export async function getAgentSystemPrompt(
agentId: string,
userId?: string
): Promise<{ content: string; isDraft: boolean }> {
const agent = await getAgentById(agentId);
if (!agent) {
@@ -316,11 +358,53 @@ export async function getAgentSystemPrompt(agentId: string): Promise<string> {
throw new Error(`智能体 ${agentId} 是工具类,不支持对话`);
}
// 获取 Prompt Code
const promptCode = AGENT_TO_PROMPT_CODE[agentId];
if (promptCode) {
// 使用 PromptService 获取(支持灰度预览)
try {
const promptService = getPromptService(prisma);
const result = await promptService.get(promptCode, {}, { userId });
if (result.isDraft) {
logger.info('[AIA:AgentService] 使用 DRAFT 版本 Prompt调试模式', {
agentId,
promptCode,
userId
});
} else {
logger.debug('[AIA:AgentService] 使用 ACTIVE 版本 Prompt', {
agentId,
promptCode,
version: result.version
});
}
return {
content: result.content,
isDraft: result.isDraft,
};
} catch (error) {
// PromptService 获取失败,降级到硬编码
logger.warn('[AIA:AgentService] PromptService 获取失败,使用兜底', {
agentId,
promptCode,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
// 兜底:使用硬编码的 systemPrompt
if (!agent.systemPrompt) {
throw new Error(`智能体 ${agentId} 未配置系统提示词`);
}
return agent.systemPrompt;
logger.debug('[AIA:AgentService] 使用硬编码 Prompt', { agentId });
return {
content: agent.systemPrompt,
isDraft: false,
};
}
/**

View File

@@ -10,9 +10,15 @@
import { logger } from '../../../common/logging/index.js';
import { storage } from '../../../common/storage/index.js';
import { cache } from '../../../common/cache/index.js';
import { ExtractionClient } from '../../../common/document/ExtractionClient.js';
import type { Attachment } from '../types/index.js';
// 附件缓存前缀和过期时间2小时
const ATTACHMENT_CACHE_PREFIX = 'aia:attachment:text:';
const ATTACHMENT_INFO_CACHE_PREFIX = 'aia:attachment:info:';
const ATTACHMENT_CACHE_TTL = 2 * 60 * 60; // 2小时
// ==================== 常量配置 ====================
const MAX_ATTACHMENTS = 5;
@@ -41,20 +47,51 @@ export async function uploadAttachment(
// 2. 上传到存储服务
const storageKey = `aia/${userId}/${conversationId}/${Date.now()}_${file.filename}`;
const url = await storage.upload(storageKey, file.buffer, {
contentType: file.mimetype,
});
const url = await storage.upload(storageKey, file.buffer);
logger.info('[AIA:AttachmentService] 附件上传成功', {
filename: file.filename,
url,
});
// 3. 提取文本内容(异步处理)
// 3. 提取文本内容
let extractedText = '';
try {
const extractionClient = new ExtractionClient();
extractedText = await extractionClient.extractText(file.buffer, ext);
// 对于 txt 文件,直接读取内容(不依赖 Python 服务)
if (ext === 'txt') {
extractedText = file.buffer.toString('utf-8');
logger.info('[AIA:AttachmentService] TXT文件直接读取成功', {
filename: file.filename,
charCount: extractedText.length,
});
} else {
// 其他文件类型调用 Python 提取服务
const extractionClient = new ExtractionClient();
let result;
if (ext === 'pdf') {
result = await extractionClient.extractPdf(file.buffer, file.filename);
} else if (ext === 'docx' || ext === 'doc') {
result = await extractionClient.extractDocx(file.buffer, file.filename);
} else {
result = await extractionClient.extractDocument(file.buffer, file.filename);
}
if (result.success && result.text) {
extractedText = result.text;
logger.info('[AIA:AttachmentService] 文本提取成功', {
filename: file.filename,
method: result.method,
charCount: result.text.length,
});
} else {
logger.warn('[AIA:AttachmentService] 文本提取返回空', {
filename: file.filename,
error: result.error,
});
extractedText = '[文档内容为空或无法提取]';
}
}
// 4. Token 截断控制
const tokens = estimateTokens(extractedText);
@@ -78,16 +115,48 @@ export async function uploadAttachment(
}
// 5. 构建附件对象
const attachmentId = `att-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const tokenCount = estimateTokens(extractedText);
const truncated = tokenCount > MAX_TOKENS_PER_ATTACHMENT;
const attachment: Attachment = {
id: `att-${Date.now()}`,
name: file.filename,
url,
size: file.buffer.length,
id: attachmentId,
filename: file.filename,
mimeType: file.mimetype,
extractedText,
tokens: estimateTokens(extractedText),
size: file.buffer.length,
ossUrl: url,
textContent: extractedText,
tokenCount,
truncated,
createdAt: new Date().toISOString(),
};
// 6. 将提取的文本存储到缓存(供后续发送消息时使用)
if (extractedText && extractedText !== '[文档内容提取失败]' && extractedText !== '[文档内容为空或无法提取]') {
await cache.set(
`${ATTACHMENT_CACHE_PREFIX}${attachmentId}`,
extractedText,
ATTACHMENT_CACHE_TTL
);
logger.info('[AIA:AttachmentService] 附件文本已缓存', {
attachmentId,
textLength: extractedText.length,
tokenCount,
});
}
// 7. 存储附件基本信息到缓存(供发送消息时保存到数据库)
const attachmentInfo = {
id: attachmentId,
filename: file.filename,
size: file.buffer.length,
};
await cache.set(
`${ATTACHMENT_INFO_CACHE_PREFIX}${attachmentId}`,
JSON.stringify(attachmentInfo),
ATTACHMENT_CACHE_TTL
);
return attachment;
}
@@ -95,11 +164,79 @@ export async function uploadAttachment(
* 批量获取附件文本内容
*/
export async function getAttachmentsText(attachmentIds: string[]): Promise<string> {
// TODO: 从存储中获取附件并提取文本
// 当前版本:简化实现,假设附件文本已在消息的 attachments 字段中
logger.debug('[AIA:AttachmentService] 获取附件文本', { attachmentIds });
return '';
if (!attachmentIds || attachmentIds.length === 0) {
return '';
}
logger.info('[AIA:AttachmentService] 获取附件文本', {
attachmentIds,
count: attachmentIds.length,
});
const texts: string[] = [];
for (const attachmentId of attachmentIds) {
try {
const cacheKey = `${ATTACHMENT_CACHE_PREFIX}${attachmentId}`;
const text = await cache.get(cacheKey);
if (text) {
texts.push(`【附件: ${attachmentId}\n${text}`);
logger.debug('[AIA:AttachmentService] 从缓存获取附件文本成功', {
attachmentId,
textLength: text.length,
});
} else {
logger.warn('[AIA:AttachmentService] 附件文本不在缓存中', { attachmentId });
texts.push(`【附件: ${attachmentId}\n[附件内容已过期或不存在]`);
}
} catch (error) {
logger.error('[AIA:AttachmentService] 获取附件文本失败', {
attachmentId,
error
});
}
}
return texts.join('\n\n---\n\n');
}
/**
* 获取附件详情(从缓存)
* 用于发送消息时保存附件信息到数据库
*/
export async function getAttachmentDetails(
attachmentIds: string[]
): Promise<Array<{ id: string; filename: string; size: number }>> {
if (!attachmentIds || attachmentIds.length === 0) {
return [];
}
const details: Array<{ id: string; filename: string; size: number }> = [];
for (const attachmentId of attachmentIds) {
try {
const cacheKey = `${ATTACHMENT_INFO_CACHE_PREFIX}${attachmentId}`;
const infoJson = await cache.get(cacheKey);
if (infoJson) {
const info = JSON.parse(infoJson);
details.push(info);
} else {
logger.warn('[AIA:AttachmentService] 附件信息不在缓存中', { attachmentId });
// 如果缓存中没有,添加一个占位信息
details.push({
id: attachmentId,
filename: '未知文件',
size: 0,
});
}
} catch (error) {
logger.error('[AIA:AttachmentService] 获取附件信息失败', { attachmentId, error });
}
}
return details;
}
/**
@@ -113,3 +250,5 @@ function estimateTokens(text: string): number {

View File

@@ -16,6 +16,7 @@ import { prisma } from '../../../config/database.js';
import { streamChat, createStreamingService } from '../../../common/streaming/index.js';
import type { OpenAIMessage, StreamOptions } from '../../../common/streaming/index.js';
import * as agentService from './agentService.js';
import * as attachmentService from './attachmentService.js';
import type {
Conversation,
Message,
@@ -178,16 +179,17 @@ export async function updateConversation(
export async function deleteConversation(
userId: string,
conversationId: string
): Promise<void> {
): Promise<boolean> {
const result = await prisma.conversation.deleteMany({
where: { id: conversationId, userId },
});
if (result.count === 0) {
throw new Error('对话不存在');
return false;
}
logger.info('[AIA:ConversationService] 删除对话', { conversationId });
return true;
}
// ==================== 消息管理 ====================
@@ -222,17 +224,25 @@ export async function getMessages(
]);
return {
messages: messages.map(m => ({
id: m.id,
conversationId: m.conversationId,
role: m.role as 'user' | 'assistant',
content: m.content,
thinkingContent: m.thinkingContent || undefined,
attachments: (m.attachments as any)?.ids as string[] | undefined,
model: m.model || undefined,
tokens: m.tokens || undefined,
createdAt: m.createdAt.toISOString(),
})),
messages: messages.map(m => {
const attachmentsJson = m.attachments as any;
const attachmentIds = attachmentsJson?.ids as string[] | undefined;
// 直接从 JSON 字段读取附件详情(不查询数据库)
const attachmentDetails = attachmentsJson?.details as Array<{ id: string; filename: string; size: number }> | undefined;
return {
id: m.id,
conversationId: m.conversationId,
role: m.role as 'user' | 'assistant',
content: m.content,
thinkingContent: m.thinkingContent || undefined,
attachments: attachmentIds,
attachmentDetails: attachmentDetails && attachmentDetails.length > 0 ? attachmentDetails : undefined,
model: m.model || undefined,
tokens: m.tokens || undefined,
createdAt: m.createdAt.toISOString(),
};
}),
total,
};
}
@@ -259,16 +269,36 @@ export async function sendMessageStream(
throw new Error('对话不存在');
}
// 2. 获取智能体系统提示词
const systemPrompt = await agentService.getAgentSystemPrompt(conversation.agentId);
// 2. 获取智能体系统提示词(支持灰度预览)
const { content: systemPrompt, isDraft } = await agentService.getAgentSystemPrompt(
conversation.agentId,
userId // 传递 userId 以支持灰度预览
);
if (isDraft) {
logger.info('[AIA:Conversation] 使用 DRAFT 版本 Prompt调试模式', {
userId,
agentId: conversation.agentId
});
}
// 3. 保存用户消息
// 3. 保存用户消息(包含附件详情)
let attachmentsData = undefined;
if (attachmentIds && attachmentIds.length > 0) {
// 从缓存获取附件详情
const attachmentDetails = await attachmentService.getAttachmentDetails(attachmentIds);
attachmentsData = {
ids: attachmentIds,
details: attachmentDetails,
};
}
await prisma.message.create({
data: {
conversationId,
role: 'user',
content,
attachments: attachmentIds ? { ids: attachmentIds } : undefined,
attachments: attachmentsData,
},
});
@@ -380,12 +410,11 @@ async function buildContextMessages(
/**
* 获取附件文本内容
* TODO: 对接文档处理服务
* 从缓存中获取上传时提取的文本
*/
async function getAttachmentText(attachmentIds: string[]): Promise<string> {
// 预留:从文档处理引擎获取附件文本
logger.debug('[AIA:ConversationService] 获取附件文本', { attachmentIds });
return '';
logger.info('[AIA:ConversationService] 获取附件文本', { attachmentIds });
return attachmentService.getAttachmentsText(attachmentIds);
}
/**

View File

@@ -7,8 +7,13 @@
/**
* 智能体阶段
* - topic: 选题优化
* - design: 方案设计
* - review: 方案预评审
* - data: 数据处理
* - writing: 论文写作
*/
export type AgentStage = 'design' | 'data' | 'analysis' | 'write' | 'publish';
export type AgentStage = 'topic' | 'design' | 'review' | 'data' | 'writing';
/**
* 智能体配置
@@ -201,3 +206,6 @@ export interface PaginatedResponse<T> {

View File

@@ -356,6 +356,9 @@ runTests().catch((error) => {

View File

@@ -335,6 +335,9 @@ Content-Type: application/json

View File

@@ -271,6 +271,9 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -221,6 +221,9 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -275,6 +275,9 @@ export const streamAIController = new StreamAIController();

View File

@@ -184,6 +184,9 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {

View File

@@ -119,6 +119,9 @@ checkTableStructure();

View File

@@ -105,6 +105,9 @@ checkProjectConfig().catch(console.error);

View File

@@ -87,6 +87,9 @@ main();

View File

@@ -546,5 +546,8 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback

View File

@@ -181,5 +181,8 @@ console.log('');

View File

@@ -498,5 +498,8 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -142,6 +142,9 @@ testDifyIntegration().catch(error => {

View File

@@ -170,6 +170,9 @@ testIitDatabase()

View File

@@ -158,5 +158,8 @@ if (hasError) {

View File

@@ -184,5 +184,8 @@ async function testUrlVerification() {

View File

@@ -263,6 +263,9 @@ main().catch((error) => {

View File

@@ -149,5 +149,8 @@ Write-Host ""

View File

@@ -240,6 +240,9 @@ export interface CachedProtocolRules {

View File

@@ -57,3 +57,6 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -135,3 +135,6 @@ Content-Type: application/json

View File

@@ -120,3 +120,6 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr

View File

@@ -34,3 +34,6 @@ export * from './services/utils.js';

View File

@@ -125,3 +125,6 @@ export function validateAgentSelection(agents: string[]): void {

View File

@@ -421,6 +421,9 @@ SET session_replication_role = 'origin';

View File

@@ -123,6 +123,9 @@ WHERE key = 'verify_test';

View File

@@ -266,6 +266,9 @@ verifyDatabase()

View File

@@ -56,6 +56,9 @@ export {}

View File

@@ -79,6 +79,9 @@ Write-Host "✅ 完成!" -ForegroundColor Green

View File

@@ -8,3 +8,6 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p

View File

@@ -171,3 +171,6 @@ DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}}

View File

@@ -366,6 +366,9 @@ runAdvancedTests().catch(error => {

View File

@@ -432,6 +432,9 @@ runAllTests()

View File

@@ -390,6 +390,9 @@ runAllTests()

View File

@@ -28,3 +28,6 @@ main()

View File

@@ -26,3 +26,6 @@ main()

View File

@@ -38,3 +38,6 @@ main()

View File

@@ -27,3 +27,6 @@ main()

View File

@@ -167,3 +167,6 @@ main()

View File

@@ -174,6 +174,9 @@ Set-Location ..

Some files were not shown because too many files have changed in this diff Show More