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: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
sourcemap: true,
|
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 {
|
export class ConversationController {
|
||||||
constructor(private conversationService: ConversationService) {}
|
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
|
* Create a new conversation
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,11 +38,24 @@ export class ConversationController {
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
async createConversation(
|
async createConversation(
|
||||||
@Headers('x-user-id') userId: string,
|
@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,
|
@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({
|
const conversation = await this.conversationService.createConversation({
|
||||||
userId,
|
userId,
|
||||||
title: dto.title,
|
title: dto.title,
|
||||||
|
deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,33 @@
|
||||||
import { IsOptional, IsString, IsNotEmpty, IsArray, ValidateNested } from 'class-validator';
|
import { IsOptional, IsString, IsNotEmpty, IsArray, ValidateNested } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
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 {
|
export class CreateConversationDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DeviceInfoDto)
|
||||||
|
deviceInfo?: DeviceInfoDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileAttachmentDto {
|
export class FileAttachmentDto {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
import {
|
import {
|
||||||
ConversationEntity,
|
ConversationEntity,
|
||||||
ConversationStatus,
|
ConversationStatus,
|
||||||
|
DeviceInfo,
|
||||||
} from '../../domain/entities/conversation.entity';
|
} from '../../domain/entities/conversation.entity';
|
||||||
import {
|
import {
|
||||||
MessageEntity,
|
MessageEntity,
|
||||||
|
|
@ -26,6 +27,7 @@ import {
|
||||||
export interface CreateConversationParams {
|
export interface CreateConversationParams {
|
||||||
userId: string;
|
userId: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
deviceInfo?: DeviceInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileAttachment {
|
export interface FileAttachment {
|
||||||
|
|
@ -63,6 +65,7 @@ export class ConversationService {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
title: params.title || '新对话',
|
title: params.title || '新对话',
|
||||||
|
deviceInfo: params.deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.conversationRepo.save(conversation);
|
return this.conversationRepo.save(conversation);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue