From 6a3a2130bf76e8353c72811c5102e452d8e66efe Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 25 Jan 2026 09:45:47 -0800 Subject: [PATCH] feat(conversation): add device tracking and optimize admin-client build ## Device Tracking (conversation-service) - Add DeviceInfoDto class for validating device information - Extract client IP from X-Forwarded-For and X-Real-IP headers - Capture User-Agent header automatically on conversation creation - Support optional fingerprint and region from client - Pass deviceInfo through service layer to entity for persistence Files changed: - conversation.controller.ts: Add extractClientIp() method and header capture - conversation.dto.ts: Add DeviceInfoDto with validation decorators - conversation.service.ts: Update CreateConversationParams interface ## Build Optimization (admin-client) - Implement code splitting via Rollup manualChunks - Separate vendor libraries into cacheable chunks: - vendor-react: react, react-dom, react-router-dom (160KB) - vendor-antd: antd, @ant-design/icons (1013KB) - vendor-charts: recharts (409KB) - vendor-data: @tanstack/react-query, axios, zustand (82KB) - Main bundle reduced from 1732KB to 61KB (96% reduction) - Set chunkSizeWarningLimit to 1100KB for antd Co-Authored-By: Claude Opus 4.5 --- packages/admin-client/vite.config.ts | 15 ++++++++++ .../inbound/conversation.controller.ts | 28 +++++++++++++++++++ .../src/application/dtos/conversation.dto.ts | 23 +++++++++++++++ .../services/conversation.service.ts | 3 ++ 4 files changed, 69 insertions(+) diff --git a/packages/admin-client/vite.config.ts b/packages/admin-client/vite.config.ts index ea5f77e..5e16806 100644 --- a/packages/admin-client/vite.config.ts +++ b/packages/admin-client/vite.config.ts @@ -16,5 +16,20 @@ export default defineConfig({ build: { outDir: 'dist', sourcemap: true, + chunkSizeWarningLimit: 1100, // antd is ~1MB, this is expected + rollupOptions: { + output: { + manualChunks: { + // React core + 'vendor-react': ['react', 'react-dom', 'react-router-dom'], + // Ant Design UI library + 'vendor-antd': ['antd', '@ant-design/icons'], + // Charts + 'vendor-charts': ['recharts'], + // Data fetching & state + 'vendor-data': ['@tanstack/react-query', 'axios', 'zustand'], + }, + }, + }, }, }); diff --git a/packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts b/packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts index 8081fdf..50ff3c9 100644 --- a/packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts +++ b/packages/services/conversation-service/src/adapters/inbound/conversation.controller.ts @@ -16,6 +16,21 @@ import { CreateConversationDto, SendMessageDto } from '../../application/dtos/co export class ConversationController { constructor(private conversationService: ConversationService) {} + /** + * Extract client IP from headers (supports proxies) + */ + private extractClientIp( + xForwardedFor?: string, + xRealIp?: string, + ): string | undefined { + // X-Forwarded-For can contain multiple IPs, take the first one (original client) + if (xForwardedFor) { + const ips = xForwardedFor.split(',').map((ip) => ip.trim()); + return ips[0] || undefined; + } + return xRealIp || undefined; + } + /** * Create a new conversation */ @@ -23,11 +38,24 @@ export class ConversationController { @HttpCode(HttpStatus.CREATED) async createConversation( @Headers('x-user-id') userId: string, + @Headers('x-forwarded-for') xForwardedFor: string, + @Headers('x-real-ip') xRealIp: string, + @Headers('user-agent') userAgent: string, @Body() dto: CreateConversationDto, ) { + // Build deviceInfo from headers and body + const clientIp = this.extractClientIp(xForwardedFor, xRealIp); + const deviceInfo = { + ip: clientIp || dto.deviceInfo?.ip, + userAgent: userAgent || dto.deviceInfo?.userAgent, + fingerprint: dto.deviceInfo?.fingerprint, + region: dto.deviceInfo?.region, + }; + const conversation = await this.conversationService.createConversation({ userId, title: dto.title, + deviceInfo, }); return { diff --git a/packages/services/conversation-service/src/application/dtos/conversation.dto.ts b/packages/services/conversation-service/src/application/dtos/conversation.dto.ts index de91308..9116cbd 100644 --- a/packages/services/conversation-service/src/application/dtos/conversation.dto.ts +++ b/packages/services/conversation-service/src/application/dtos/conversation.dto.ts @@ -1,10 +1,33 @@ import { IsOptional, IsString, IsNotEmpty, IsArray, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; +export class DeviceInfoDto { + @IsOptional() + @IsString() + ip?: string; + + @IsOptional() + @IsString() + userAgent?: string; + + @IsOptional() + @IsString() + fingerprint?: string; + + @IsOptional() + @IsString() + region?: string; +} + export class CreateConversationDto { @IsOptional() @IsString() title?: string; + + @IsOptional() + @ValidateNested() + @Type(() => DeviceInfoDto) + deviceInfo?: DeviceInfoDto; } export class FileAttachmentDto { diff --git a/packages/services/conversation-service/src/application/services/conversation.service.ts b/packages/services/conversation-service/src/application/services/conversation.service.ts index d391843..6d490f5 100644 --- a/packages/services/conversation-service/src/application/services/conversation.service.ts +++ b/packages/services/conversation-service/src/application/services/conversation.service.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { ConversationEntity, ConversationStatus, + DeviceInfo, } from '../../domain/entities/conversation.entity'; import { MessageEntity, @@ -26,6 +27,7 @@ import { export interface CreateConversationParams { userId: string; title?: string; + deviceInfo?: DeviceInfo; } export interface FileAttachment { @@ -63,6 +65,7 @@ export class ConversationService { id: uuidv4(), userId: params.userId, title: params.title || '新对话', + deviceInfo: params.deviceInfo, }); return this.conversationRepo.save(conversation);