feat(aia): Complete AIA V2.0 with universal streaming capabilities
Major Updates: - Add StreamingService with OpenAI Compatible format (backend/common/streaming) - Upgrade Chat component V2 with Ant Design X integration - Implement AIA module with 12 intelligent agents - Create AgentHub with 100% prototype V11 restoration - Create ChatWorkspace with streaming response support - Add ThinkingBlock for deep thinking display - Add useAIStream Hook for OpenAI Compatible stream handling Backend Common Capabilities (~400 lines): - OpenAIStreamAdapter: SSE adapter with OpenAI format - StreamingService: unified streaming service - Support content and reasoning_content dual streams - Deep thinking tag processing (<think>...</think>) Frontend Common Capabilities (~2000 lines): - AIStreamChat: modern streaming chat component - ThinkingBlock: collapsible deep thinking display - ConversationList: conversation management with grouping - useAIStream: OpenAI Compatible stream handler Hook - useConversations: conversation state management Hook - Modern design styles (Ultramodern theme) AIA Module Frontend (~1500 lines): - AgentHub: 12 agent cards with timeline design - ChatWorkspace: fullscreen immersive chat interface - AgentCard: theme-colored cards (blue/yellow/teal/purple) - 5 phases, 12 agents configuration - Responsive layout (desktop + mobile) AIA Module Backend (~900 lines): - agentService: 12 agents config with system prompts - conversationService: refactored with StreamingService - attachmentService: file upload skeleton (30k token limit) - 12 API endpoints with authentication - Full CRUD for conversations and messages Documentation: - AIA module status and development guide - Universal capabilities catalog (11 services) - Quick reference card for developers - System overview updates Testing: - Stream response verified (HTTP 200) - Authentication working correctly - Auto conversation creation working - Deep thinking display working - Message input and send working Status: Core features completed (85%), attachment and history loading pending
This commit is contained in:
233
backend/src/modules/aia/controllers/agentController.ts
Normal file
233
backend/src/modules/aia/controllers/agentController.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* AIA 智能问答模块 - 智能体控制器
|
||||
* @module aia/controllers/agentController
|
||||
*
|
||||
* API 端点:
|
||||
* - GET /api/v1/aia/agents 获取智能体列表
|
||||
* - GET /api/v1/aia/agents/:id 获取智能体详情
|
||||
* - POST /api/v1/aia/intent/route 意图路由
|
||||
*/
|
||||
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import * as agentService from '../services/agentService.js';
|
||||
import type { AgentStage, IntentRouteRequest } from '../types/index.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;
|
||||
}
|
||||
|
||||
// ==================== 智能体管理 ====================
|
||||
|
||||
/**
|
||||
* 获取智能体列表
|
||||
* GET /api/v1/aia/agents
|
||||
*/
|
||||
export async function getAgents(
|
||||
request: FastifyRequest<{
|
||||
Querystring: { stage?: AgentStage };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { stage } = request.query;
|
||||
|
||||
logger.info('[AIA:Controller] 获取智能体列表', { stage });
|
||||
|
||||
const agents = await agentService.getAgents(stage);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
data: { agents },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[AIA:Controller] 获取智能体列表失败', { error });
|
||||
return reply.status(500).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体详情
|
||||
* GET /api/v1/aia/agents/:id
|
||||
*/
|
||||
export async function getAgentById(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
|
||||
logger.info('[AIA:Controller] 获取智能体详情', { agentId: id });
|
||||
|
||||
const agent = await agentService.getAgentById(id);
|
||||
|
||||
if (!agent) {
|
||||
return reply.status(404).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: '智能体不存在',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
data: agent,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[AIA:Controller] 获取智能体详情失败', { error });
|
||||
return reply.status(500).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 意图路由 ====================
|
||||
|
||||
/**
|
||||
* 意图路由
|
||||
* POST /api/v1/aia/intent/route
|
||||
*
|
||||
* 根据用户输入识别意图,推荐合适的智能体
|
||||
*/
|
||||
export async function routeIntent(
|
||||
request: FastifyRequest<{
|
||||
Body: IntentRouteRequest;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = getUserId(request);
|
||||
const { query } = request.body;
|
||||
|
||||
logger.info('[AIA:Controller] 意图路由', { userId, queryLength: query?.length });
|
||||
|
||||
if (!query || query.trim().length < 2) {
|
||||
return reply.status(400).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: '查询内容至少需要2个字符',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 简单的关键词匹配意图识别
|
||||
// TODO: 后续可以使用 LLM 或专门的意图识别模型
|
||||
const intent = await matchIntent(query);
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
data: intent,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[AIA:Controller] 意图路由失败', { error });
|
||||
return reply.status(500).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的关键词匹配意图识别
|
||||
*/
|
||||
async function matchIntent(query: string): Promise<{
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
confidence: number;
|
||||
prefillPrompt: string;
|
||||
}> {
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
// 关键词映射
|
||||
const intentPatterns = [
|
||||
{
|
||||
patterns: ['研究设计', '方案设计', 'rct', '队列', '病例对照', '样本量'],
|
||||
agentId: 'research-design',
|
||||
agentName: '科研设计小助手',
|
||||
},
|
||||
{
|
||||
patterns: ['文献', '检索', 'pubmed', '筛选', 'meta', '系统综述'],
|
||||
agentId: 'literature-assistant',
|
||||
agentName: '文献检索助手',
|
||||
},
|
||||
{
|
||||
patterns: ['数据', '清洗', '缺失值', '异常值', '整理'],
|
||||
agentId: 'data-assistant',
|
||||
agentName: '数据管理助手',
|
||||
},
|
||||
{
|
||||
patterns: ['统计', '分析', 'spss', 't检验', '卡方', '回归', 'p值'],
|
||||
agentId: 'stat-assistant',
|
||||
agentName: '统计分析小助手',
|
||||
},
|
||||
{
|
||||
patterns: ['论文', '写作', '润色', '翻译', 'sci', '摘要', 'introduction'],
|
||||
agentId: 'writing-assistant',
|
||||
agentName: '论文撰写助手',
|
||||
},
|
||||
{
|
||||
patterns: ['投稿', '期刊', '审稿', '发表', 'if', '影响因子'],
|
||||
agentId: 'publish-assistant',
|
||||
agentName: '投稿指导助手',
|
||||
},
|
||||
];
|
||||
|
||||
// 计算匹配度
|
||||
let bestMatch = {
|
||||
agentId: 'research-design',
|
||||
agentName: '科研设计小助手',
|
||||
confidence: 0.3, // 默认较低置信度
|
||||
matchCount: 0,
|
||||
};
|
||||
|
||||
for (const pattern of intentPatterns) {
|
||||
let matchCount = 0;
|
||||
for (const keyword of pattern.patterns) {
|
||||
if (queryLower.includes(keyword)) {
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchCount > bestMatch.matchCount) {
|
||||
bestMatch = {
|
||||
agentId: pattern.agentId,
|
||||
agentName: pattern.agentName,
|
||||
confidence: Math.min(0.5 + matchCount * 0.15, 0.95),
|
||||
matchCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agentId: bestMatch.agentId,
|
||||
agentName: bestMatch.agentName,
|
||||
confidence: bestMatch.confidence,
|
||||
prefillPrompt: query, // 直接使用原始查询作为预填充
|
||||
};
|
||||
}
|
||||
|
||||
288
backend/src/modules/aia/controllers/conversationController.ts
Normal file
288
backend/src/modules/aia/controllers/conversationController.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* AIA 智能问答模块 - 对话控制器
|
||||
* @module aia/controllers/conversationController
|
||||
*
|
||||
* API 端点:
|
||||
* - GET /api/v1/aia/conversations 获取对话列表
|
||||
* - POST /api/v1/aia/conversations 创建对话
|
||||
* - GET /api/v1/aia/conversations/:id 获取对话详情
|
||||
* - DELETE /api/v1/aia/conversations/:id 删除对话
|
||||
* - POST /api/v1/aia/conversations/:id/messages/stream 发送消息(流式)
|
||||
*/
|
||||
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import * as conversationService from '../services/conversationService.js';
|
||||
import type { CreateConversationRequest, SendMessageRequest } from '../types/index.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;
|
||||
}
|
||||
|
||||
// ==================== 对话管理 ====================
|
||||
|
||||
/**
|
||||
* 获取对话列表
|
||||
* GET /api/v1/aia/conversations
|
||||
*/
|
||||
export async function getConversations(
|
||||
request: FastifyRequest<{
|
||||
Querystring: {
|
||||
agentId?: string;
|
||||
projectId?: string;
|
||||
page?: string;
|
||||
pageSize?: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = getUserId(request);
|
||||
const { agentId, projectId, page, pageSize } = request.query;
|
||||
|
||||
logger.info('[AIA:Controller] 获取对话列表', { userId, agentId, projectId });
|
||||
|
||||
const result = await conversationService.getConversations(userId, {
|
||||
agentId,
|
||||
projectId: projectId === 'null' ? null : projectId,
|
||||
page: page ? parseInt(page) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize) : 20,
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
data: {
|
||||
conversations: result.conversations,
|
||||
pagination: {
|
||||
total: result.total,
|
||||
page: page ? parseInt(page) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize) : 20,
|
||||
totalPages: Math.ceil(result.total / (pageSize ? parseInt(pageSize) : 20)),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[AIA:Controller] 获取对话列表失败', { error });
|
||||
return reply.status(500).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
* POST /api/v1/aia/conversations
|
||||
*/
|
||||
export async function createConversation(
|
||||
request: FastifyRequest<{
|
||||
Body: CreateConversationRequest;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = getUserId(request);
|
||||
const { agentId, projectId, title } = request.body;
|
||||
|
||||
logger.info('[AIA:Controller] 创建对话', { userId, agentId, projectId });
|
||||
|
||||
if (!agentId) {
|
||||
return reply.status(400).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'agentId 是必填项',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const conversation = await conversationService.createConversation(userId, {
|
||||
agentId,
|
||||
projectId,
|
||||
title,
|
||||
});
|
||||
|
||||
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 : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话详情
|
||||
* GET /api/v1/aia/conversations/:id
|
||||
*/
|
||||
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
|
||||
);
|
||||
|
||||
if (!conversation) {
|
||||
return reply.status(404).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: '对话不存在',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
export async function deleteConversation(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string };
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = getUserId(request);
|
||||
const { id } = request.params;
|
||||
|
||||
logger.info('[AIA:Controller] 删除对话', { userId, conversationId: id });
|
||||
|
||||
const deleted = await conversationService.deleteConversation(userId, id);
|
||||
|
||||
if (!deleted) {
|
||||
return reply.status(404).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: '对话不存在',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
code: 0,
|
||||
data: { deleted: true },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[AIA:Controller] 删除对话失败', { error });
|
||||
return reply.status(500).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : '服务器内部错误',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消息发送 ====================
|
||||
|
||||
/**
|
||||
* 发送消息(流式输出)
|
||||
* POST /api/v1/aia/conversations/:id/messages/stream
|
||||
*/
|
||||
export async function sendMessageStream(
|
||||
request: FastifyRequest<{
|
||||
Params: { id: string };
|
||||
Body: SendMessageRequest;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const userId = getUserId(request);
|
||||
const { id } = request.params;
|
||||
const { content, attachmentIds, enableDeepThinking } = request.body;
|
||||
|
||||
logger.info('[AIA:Controller] 发送消息', {
|
||||
userId,
|
||||
conversationId: id,
|
||||
contentLength: content?.length,
|
||||
attachmentCount: attachmentIds?.length || 0,
|
||||
});
|
||||
|
||||
if (!content || content.trim().length === 0) {
|
||||
return reply.status(400).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: '消息内容不能为空',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流式输出
|
||||
await conversationService.sendMessageStream(
|
||||
userId,
|
||||
id,
|
||||
{ content, attachmentIds, enableDeepThinking },
|
||||
reply
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error('[AIA:Controller] 发送消息失败', {
|
||||
error: errorMessage,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
conversationId: request.params.id,
|
||||
});
|
||||
|
||||
// 如果还没有发送响应头,返回错误
|
||||
if (!reply.sent) {
|
||||
return reply.status(500).send({
|
||||
code: -1,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: errorMessage,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user