diff --git a/packages/services/conversation-service/src/conversation/conversation.module.ts b/packages/services/conversation-service/src/conversation/conversation.module.ts index e80a654..ec89ab6 100644 --- a/packages/services/conversation-service/src/conversation/conversation.module.ts +++ b/packages/services/conversation-service/src/conversation/conversation.module.ts @@ -4,11 +4,12 @@ import { ConversationEntity } from '../domain/entities/conversation.entity'; import { MessageEntity } from '../domain/entities/message.entity'; import { ConversationService } from './conversation.service'; import { ConversationController } from './conversation.controller'; +import { InternalConversationController } from './internal.controller'; import { ConversationGateway } from './conversation.gateway'; @Module({ imports: [TypeOrmModule.forFeature([ConversationEntity, MessageEntity])], - controllers: [ConversationController], + controllers: [ConversationController, InternalConversationController], providers: [ConversationService, ConversationGateway], exports: [ConversationService], }) diff --git a/packages/services/conversation-service/src/conversation/internal.controller.ts b/packages/services/conversation-service/src/conversation/internal.controller.ts new file mode 100644 index 0000000..388dc00 --- /dev/null +++ b/packages/services/conversation-service/src/conversation/internal.controller.ts @@ -0,0 +1,105 @@ +import { Controller, Get, Query, Param } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, MoreThan, LessThan } from 'typeorm'; +import { ConversationEntity } from '../domain/entities/conversation.entity'; +import { MessageEntity } from '../domain/entities/message.entity'; + +/** + * 内部 API - 供其他微服务调用 + * 不暴露给外部用户 + */ +@Controller('internal/conversations') +export class InternalConversationController { + constructor( + @InjectRepository(ConversationEntity) + private conversationRepo: Repository, + @InjectRepository(MessageEntity) + private messageRepo: Repository, + ) {} + + /** + * 查询对话列表(供 evolution-service 使用) + */ + @Get() + async findConversations( + @Query('status') status?: string, + @Query('hoursBack') hoursBack?: string, + @Query('minMessageCount') minMessageCount?: string, + @Query('limit') limit?: string, + ) { + const queryBuilder = this.conversationRepo.createQueryBuilder('c'); + + if (status) { + queryBuilder.andWhere('c.status = :status', { status }); + } + + if (hoursBack) { + const cutoffTime = new Date(); + cutoffTime.setHours(cutoffTime.getHours() - parseInt(hoursBack)); + queryBuilder.andWhere('c.created_at > :cutoffTime', { cutoffTime }); + } + + if (minMessageCount) { + queryBuilder.andWhere('c.message_count > :minCount', { + minCount: parseInt(minMessageCount), + }); + } + + queryBuilder.orderBy('c.created_at', 'DESC'); + + if (limit) { + queryBuilder.take(parseInt(limit)); + } + + const conversations = await queryBuilder.getMany(); + + return { + success: true, + data: conversations, + }; + } + + /** + * 获取对话的消息(供 evolution-service 使用) + */ + @Get(':id/messages') + async getMessages(@Param('id') conversationId: string) { + const messages = await this.messageRepo.find({ + where: { conversationId }, + order: { createdAt: 'ASC' }, + }); + + return { + success: true, + data: messages, + }; + } + + /** + * 统计对话数量(供 evolution-service 使用) + */ + @Get('count') + async countConversations( + @Query('status') status?: string, + @Query('daysBack') daysBack?: string, + ) { + const queryBuilder = this.conversationRepo.createQueryBuilder('c'); + + if (status) { + queryBuilder.andWhere('c.status = :status', { status }); + } + + if (daysBack) { + const cutoffTime = new Date(); + cutoffTime.setDate(cutoffTime.getDate() - parseInt(daysBack)); + queryBuilder.andWhere('c.updated_at > :cutoffTime', { cutoffTime }); + } + + const count = await queryBuilder.getCount(); + + return { + success: true, + data: { count }, + }; + } +} diff --git a/packages/services/evolution-service/src/evolution/evolution.module.ts b/packages/services/evolution-service/src/evolution/evolution.module.ts index 661bddb..94bd5cf 100644 --- a/packages/services/evolution-service/src/evolution/evolution.module.ts +++ b/packages/services/evolution-service/src/evolution/evolution.module.ts @@ -3,22 +3,20 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { EvolutionController } from './evolution.controller'; import { EvolutionService } from './evolution.service'; import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; -import { ConversationORM } from '../infrastructure/database/entities/conversation.orm'; -import { MessageORM } from '../infrastructure/database/entities/message.orm'; +import { ConversationClient } from '../infrastructure/clients/conversation.client'; import { SystemExperienceORM } from '../infrastructure/database/entities/system-experience.orm'; @Module({ imports: [ - TypeOrmModule.forFeature([ - ConversationORM, - MessageORM, - SystemExperienceORM, - ]), + // 只保留自己 Bounded Context 的实体 + TypeOrmModule.forFeature([SystemExperienceORM]), ], controllers: [EvolutionController], providers: [ EvolutionService, ExperienceExtractorService, + // 使用 API 客户端访问其他服务的数据 + ConversationClient, ], exports: [EvolutionService], }) diff --git a/packages/services/evolution-service/src/evolution/evolution.service.ts b/packages/services/evolution-service/src/evolution/evolution.service.ts index 1b0549b..d315b51 100644 --- a/packages/services/evolution-service/src/evolution/evolution.service.ts +++ b/packages/services/evolution-service/src/evolution/evolution.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, MoreThan, LessThan } from 'typeorm'; +import { Repository } from 'typeorm'; import { ExperienceExtractorService } from '../infrastructure/claude/experience-extractor.service'; -import { ConversationORM } from '../infrastructure/database/entities/conversation.orm'; -import { MessageORM } from '../infrastructure/database/entities/message.orm'; +import { ConversationClient, ConversationDto, MessageDto } from '../infrastructure/clients/conversation.client'; import { SystemExperienceORM } from '../infrastructure/database/entities/system-experience.orm'; import { v4 as uuidv4 } from 'uuid'; @@ -26,10 +25,7 @@ export interface EvolutionTaskResult { @Injectable() export class EvolutionService { constructor( - @InjectRepository(ConversationORM) - private conversationRepo: Repository, - @InjectRepository(MessageORM) - private messageRepo: Repository, + private conversationClient: ConversationClient, @InjectRepository(SystemExperienceORM) private experienceRepo: Repository, private experienceExtractor: ExperienceExtractorService, @@ -60,18 +56,12 @@ export class EvolutionService { console.log(`[Evolution] Starting task ${taskId}`); try { - // 1. 获取待分析的对话 - const cutoffTime = new Date(); - cutoffTime.setHours(cutoffTime.getHours() - hoursBack); - - const conversations = await this.conversationRepo.find({ - where: { - status: 'ENDED', - createdAt: MoreThan(cutoffTime), - messageCount: MoreThan(minMessageCount), - }, - order: { createdAt: 'DESC' }, - take: limit, + // 1. 通过 API 获取待分析的对话 + const conversations = await this.conversationClient.findConversations({ + status: 'ENDED', + hoursBack, + minMessageCount, + limit, }); console.log(`[Evolution] Found ${conversations.length} conversations to analyze`); @@ -81,11 +71,8 @@ export class EvolutionService { for (const conversation of conversations) { try { - // 获取对话消息 - const messages = await this.messageRepo.find({ - where: { conversationId: conversation.id }, - order: { createdAt: 'ASC' }, - }); + // 通过 API 获取对话消息 + const messages = await this.conversationClient.getMessages(conversation.id); // 分析对话 const analysis = await this.experienceExtractor.analyzeConversation({ @@ -125,8 +112,6 @@ export class EvolutionService { const uniqueGaps = [...new Set(allKnowledgeGaps)]; result.knowledgeGapsFound = uniqueGaps.length; - // 可以在这里调用知识服务创建待处理的知识缺口任务 - console.log(`[Evolution] Task ${taskId} completed:`, result); if (result.errors.length > 0) { @@ -219,14 +204,10 @@ export class EvolutionService { this.experienceRepo.count({ where: { isActive: true } }), ]); - // 获取最近分析的对话数(过去7天) - const weekAgo = new Date(); - weekAgo.setDate(weekAgo.getDate() - 7); - const recentConversations = await this.conversationRepo.count({ - where: { - status: 'ENDED', - updatedAt: MoreThan(weekAgo), - }, + // 通过 API 获取最近分析的对话数(过去7天) + const recentConversations = await this.conversationClient.countConversations({ + status: 'ENDED', + daysBack: 7, }); // 获取经验类型分布 diff --git a/packages/services/evolution-service/src/infrastructure/clients/conversation.client.ts b/packages/services/evolution-service/src/infrastructure/clients/conversation.client.ts new file mode 100644 index 0000000..42ab1fd --- /dev/null +++ b/packages/services/evolution-service/src/infrastructure/clients/conversation.client.ts @@ -0,0 +1,129 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +/** + * 对话数据传输对象 + */ +export interface ConversationDto { + id: string; + userId: string; + status: string; + title: string; + summary: string; + category: string; + messageCount: number; + rating: number; + hasConverted: boolean; + createdAt: Date; + updatedAt: Date; +} + +/** + * 消息数据传输对象 + */ +export interface MessageDto { + id: string; + conversationId: string; + role: 'user' | 'assistant' | 'system'; + content: string; + createdAt: Date; +} + +/** + * Conversation Service 客户端 + * 用于调用 conversation-service 的内部 API + */ +@Injectable() +export class ConversationClient { + private readonly baseUrl: string; + + constructor(private configService: ConfigService) { + this.baseUrl = this.configService.get( + 'CONVERSATION_SERVICE_URL', + 'http://conversation-service:3004', + ); + } + + /** + * 查询对话列表 + */ + async findConversations(options: { + status?: string; + hoursBack?: number; + minMessageCount?: number; + limit?: number; + }): Promise { + const params = new URLSearchParams(); + + if (options.status) params.append('status', options.status); + if (options.hoursBack) params.append('hoursBack', options.hoursBack.toString()); + if (options.minMessageCount) params.append('minMessageCount', options.minMessageCount.toString()); + if (options.limit) params.append('limit', options.limit.toString()); + + const url = `${this.baseUrl}/api/internal/conversations?${params.toString()}`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (!data.success) { + throw new Error('Failed to fetch conversations'); + } + + return data.data; + } catch (error) { + console.error('[ConversationClient] Error fetching conversations:', error); + throw error; + } + } + + /** + * 获取对话的消息 + */ + async getMessages(conversationId: string): Promise { + const url = `${this.baseUrl}/api/internal/conversations/${conversationId}/messages`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (!data.success) { + throw new Error('Failed to fetch messages'); + } + + return data.data; + } catch (error) { + console.error('[ConversationClient] Error fetching messages:', error); + throw error; + } + } + + /** + * 统计对话数量 + */ + async countConversations(options: { + status?: string; + daysBack?: number; + }): Promise { + const params = new URLSearchParams(); + + if (options.status) params.append('status', options.status); + if (options.daysBack) params.append('daysBack', options.daysBack.toString()); + + const url = `${this.baseUrl}/api/internal/conversations/count?${params.toString()}`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (!data.success) { + throw new Error('Failed to count conversations'); + } + + return data.data.count; + } catch (error) { + console.error('[ConversationClient] Error counting conversations:', error); + throw error; + } + } +} diff --git a/packages/services/evolution-service/src/infrastructure/database/entities/conversation.orm.ts b/packages/services/evolution-service/src/infrastructure/database/entities/conversation.orm.ts deleted file mode 100644 index b48692e..0000000 --- a/packages/services/evolution-service/src/infrastructure/database/entities/conversation.orm.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - Entity, - Column, - PrimaryColumn, - CreateDateColumn, - UpdateDateColumn, -} from 'typeorm'; - -@Entity('conversations') -export class ConversationORM { - @PrimaryColumn('uuid') - id: string; - - @Column({ name: 'user_id', nullable: true }) - userId: string; - - @Column({ length: 20, default: 'ACTIVE' }) - status: string; - - @Column({ length: 255, nullable: true }) - title: string; - - @Column('text', { nullable: true }) - summary: string; - - @Column({ length: 50, nullable: true }) - category: string; - - @Column({ name: 'message_count', default: 0 }) - messageCount: number; - - @Column({ name: 'user_message_count', default: 0 }) - userMessageCount: number; - - @Column({ name: 'assistant_message_count', default: 0 }) - assistantMessageCount: number; - - @Column({ name: 'total_input_tokens', default: 0 }) - totalInputTokens: number; - - @Column({ name: 'total_output_tokens', default: 0 }) - totalOutputTokens: number; - - @Column({ type: 'smallint', nullable: true }) - rating: number; - - @Column('text', { nullable: true }) - feedback: string; - - @Column({ name: 'has_converted', default: false }) - hasConverted: boolean; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; - - @Column({ name: 'ended_at', nullable: true }) - endedAt: Date; -} diff --git a/packages/services/evolution-service/src/infrastructure/database/entities/message.orm.ts b/packages/services/evolution-service/src/infrastructure/database/entities/message.orm.ts deleted file mode 100644 index c7f96cc..0000000 --- a/packages/services/evolution-service/src/infrastructure/database/entities/message.orm.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - Entity, - Column, - PrimaryColumn, - CreateDateColumn, -} from 'typeorm'; - -@Entity('messages') -export class MessageORM { - @PrimaryColumn('uuid') - id: string; - - @Column({ name: 'conversation_id' }) - conversationId: string; - - @Column({ length: 20 }) - role: string; - - @Column({ length: 30, default: 'TEXT' }) - type: string; - - @Column('text') - content: string; - - @Column({ name: 'input_tokens', default: 0 }) - inputTokens: number; - - @Column({ name: 'output_tokens', default: 0 }) - outputTokens: number; - - @Column('jsonb', { nullable: true }) - metadata: Record; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; -}