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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-25 09:45:47 -08:00
parent d3d2944b03
commit 6a3a2130bf
4 changed files with 69 additions and 0 deletions

View File

@ -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'],
},
},
},
},
});

View File

@ -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 {

View File

@ -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 {

View File

@ -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);