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:
parent
d3d2944b03
commit
6a3a2130bf
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue