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:
@@ -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 {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user