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:
@@ -46,6 +46,9 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -276,6 +276,9 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -224,5 +224,8 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -153,5 +153,8 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,4 +55,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -314,5 +314,8 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -176,5 +176,8 @@ npm run dev
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,3 +55,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -49,3 +49,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,3 +44,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -76,3 +76,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -39,3 +39,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,3 +80,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,3 +27,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -115,3 +115,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -86,3 +86,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -72,3 +72,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -114,3 +114,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,3 +25,6 @@ ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,3 +57,6 @@ ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ WHERE table_schema = 'dc_schema'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -109,6 +109,9 @@ ORDER BY ordinal_position;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ runMigration()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -233,6 +233,9 @@ function extractCodeBlocks(obj, blocks = []) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -34,3 +34,6 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -108,3 +108,6 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -252,6 +252,9 @@ checkDCTables();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +9,6 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -204,6 +204,9 @@ createAiHistoryTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -191,6 +191,9 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -188,6 +188,9 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
317
backend/scripts/migrate-aia-prompts.ts
Normal file
317
backend/scripts/migrate-aia-prompts.ts
Normal 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());
|
||||
|
||||
@@ -119,3 +119,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -339,3 +339,6 @@ runTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,3 +85,6 @@ testAPI().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -304,3 +304,6 @@ verifySchemas()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -192,3 +192,6 @@ export const jwtService = new JWTService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -320,6 +320,9 @@ export function getBatchItems<T>(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -75,3 +75,6 @@ export interface VariableValidation {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -196,3 +196,6 @@ export function createOpenAIStreamAdapter(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -202,3 +202,6 @@ export async function streamChat(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,3 +20,6 @@ export { THINKING_TAGS } from './types';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,3 +95,6 @@ export type SSEEventType =
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -81,3 +81,6 @@ export async function moduleRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -111,3 +111,6 @@ export interface PaginatedResponse<T> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -158,3 +158,6 @@ export const ROLE_DISPLAY_NAMES: Record<UserRole, string> = {
|
||||
USER: '普通用户',
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -233,3 +233,6 @@ async function matchIntent(query: string): Promise<{
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
92
backend/src/modules/aia/controllers/attachmentController.ts
Normal file
92
backend/src/modules/aia/controllers/attachmentController.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息(流式输出)
|
||||
|
||||
@@ -16,3 +16,6 @@ export { aiaRoutes };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -356,6 +356,9 @@ runTests().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -297,6 +297,9 @@ runTest()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -335,6 +335,9 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -271,6 +271,9 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -221,6 +221,9 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -275,6 +275,9 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -184,6 +184,9 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@ checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,9 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,9 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -546,5 +546,8 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -181,5 +181,8 @@ console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -498,5 +498,8 @@ export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -142,6 +142,9 @@ testDifyIntegration().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -158,5 +158,8 @@ if (hasError) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -184,5 +184,8 @@ async function testUrlVerification() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -263,6 +263,9 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -149,5 +149,8 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -240,6 +240,9 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,3 +57,6 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -135,3 +135,6 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -120,3 +120,6 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -34,3 +34,6 @@ export * from './services/utils.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -125,3 +125,6 @@ export function validateAgentSelection(agents: string[]): void {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -421,6 +421,9 @@ SET session_replication_role = 'origin';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ WHERE key = 'verify_test';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -266,6 +266,9 @@ verifyDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
3
backend/src/types/global.d.ts
vendored
3
backend/src/types/global.d.ts
vendored
@@ -56,6 +56,9 @@ export {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +79,9 @@ Write-Host "✅ 完成!" -ForegroundColor Green
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,3 +8,6 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -171,3 +171,6 @@ DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -366,6 +366,9 @@ runAdvancedTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -432,6 +432,9 @@ runAllTests()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -390,6 +390,9 @@ runAllTests()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,3 +28,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,3 +26,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,3 +38,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,3 +27,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -167,3 +167,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -174,6 +174,9 @@ Set-Location ..
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user