From 303a8648bc8326f27df8f0534260d08cb5b27fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E5=85=AE?= Date: Fri, 12 Jun 2026 06:22:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=20Agent=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=98=E6=9B=B4=E5=AE=A1=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agent-config/agent-config.controller.ts | 34 ++++++++++++-- src/agent-config/agent-config.service.spec.ts | 19 ++++++-- src/agent-config/agent-config.service.ts | 44 ++++++++++++++----- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/agent-config/agent-config.controller.ts b/src/agent-config/agent-config.controller.ts index 59f9364..51d4f26 100644 --- a/src/agent-config/agent-config.controller.ts +++ b/src/agent-config/agent-config.controller.ts @@ -1,11 +1,24 @@ -import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiBody, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AdminOnlyMessage } from '../auth/admin-only-message.decorator'; import { AuthGuard } from '../auth/auth.guard'; -import { AuthenticatedUser } from '../auth/auth.types'; +import { + AuthenticatedUser, + AuthRequestContext, + RequestWithUser, +} from '../auth/auth.types'; import { CurrentUser } from '../auth/current-user.decorator'; import { PasswordChangeGuard } from '../auth/password-change.guard'; import { SuperAdminGuard } from '../auth/super-admin.guard'; +import { resolveRequestId } from '../common/http/request-id'; import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe'; import { SaveAgentConfigDto, @@ -37,11 +50,16 @@ export class AgentConfigController { @ApiBody({ type: SaveAgentConfigDto }) @ApiOkResponse({ description: '保存 Agent 配置。' }) async saveConfig( + @Req() request: RequestWithUser, @CurrentUser() user: AuthenticatedUser, @Body(new ZodValidationPipe(saveAgentConfigSchema)) body: SaveAgentConfigInput, ): Promise { - return this.agentConfigService.saveConfig(user, body); + return this.agentConfigService.saveConfig( + user, + body, + this.auditContextFor(request), + ); } @Post('test') @@ -51,4 +69,14 @@ export class AgentConfigController { ): Promise { return this.agentConfigService.testConnection(user); } + + private auditContextFor(request: RequestWithUser): AuthRequestContext { + const userAgent = request.headers['user-agent']; + + return { + requestId: resolveRequestId(request.headers), + sourceIp: request.ip ?? request.socket?.remoteAddress, + userAgent: Array.isArray(userAgent) ? userAgent[0] : userAgent, + }; + } } diff --git a/src/agent-config/agent-config.service.spec.ts b/src/agent-config/agent-config.service.spec.ts index 33c13f4..cd89959 100644 --- a/src/agent-config/agent-config.service.spec.ts +++ b/src/agent-config/agent-config.service.spec.ts @@ -84,13 +84,26 @@ describe('AgentConfigService', () => { expect(audit.record).toHaveBeenCalledWith( expect.objectContaining({ + before: expect.objectContaining({ + hasBaseURL: true, + hasKey: true, + model: 'gpt-test', + }), after: expect.objectContaining({ + hasBaseURL: true, + hasKey: true, + model: 'gpt-new', changedFields: expect.arrayContaining(['key', 'model']), }), + parameterDigest: expect.objectContaining({ + keyProvided: true, + baseURLProvided: true, + }), }), ); - expect(JSON.stringify(audit.record.mock.calls)).not.toContain( - 'sk-new-abcdef123456', - ); + const auditPayload = JSON.stringify(audit.record.mock.calls); + + expect(auditPayload).not.toContain('sk-new-abcdef123456'); + expect(auditPayload).not.toContain('https://api.example.com/v1'); }); }); diff --git a/src/agent-config/agent-config.service.ts b/src/agent-config/agent-config.service.ts index 4536c06..7e11eb0 100644 --- a/src/agent-config/agent-config.service.ts +++ b/src/agent-config/agent-config.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { AuditService } from '../audit/audit.service'; -import { AuthenticatedUser } from '../auth/auth.types'; +import { AuthenticatedUser, AuthRequestContext } from '../auth/auth.types'; import { AppError } from '../common/errors/app-error'; import { redactSensitive } from '../common/security/redact-sensitive'; import { SaveAgentConfigInput } from './agent-config.dto'; @@ -29,26 +29,36 @@ export class AgentConfigService { async saveConfig( user: AuthenticatedUser, input: SaveAgentConfigInput, + context: AuthRequestContext = {}, ): Promise { this.assertSuperAdmin(user); const before = await this.agentConfigRepository.getRuntimeConfig(); const savedConfig = await this.agentConfigRepository.saveConfig(input); + const changedFields = this.changedFields( + before, + savedConfig, + Boolean(input.key), + ); await this.auditService.record({ action: 'AGENT_CONFIG_UPDATED', resourceType: 'agent_config', + resourceId: 'system-agent-config', actorId: user.id, actorName: user.account, - before: { - baseURL: before.baseURL, - model: before.model, - hasKey: Boolean(before.key), - }, + requestId: context.requestId, + sourceIp: context.sourceIp, + userAgent: context.userAgent, + before: this.auditConfigSummary(before), after: { - baseURL: savedConfig.baseURL, - model: savedConfig.model, - hasKey: Boolean(savedConfig.key), - changedFields: this.changedFields(before, savedConfig, Boolean(input.key)), + ...this.auditConfigSummary(savedConfig), + changedFields, + }, + parameterDigest: { + changedFields, + keyProvided: Boolean(input.key), + baseURLProvided: Boolean(input.baseURL), + model: input.model, }, }); @@ -168,6 +178,20 @@ export class AgentConfigService { return fields; } + /** + * Agent 配置审计只记录状态和模型名,禁止把 key 或 baseURL 原文写入审计表。 + */ + private auditConfigSummary( + config: Partial & { updatedAt?: string }, + ) { + return { + hasKey: Boolean(config.key), + hasBaseURL: Boolean(config.baseURL), + model: config.model, + updatedAt: config.updatedAt, + }; + } + private trimSlash(value: string): string { return value.replace(/\/+$/, ''); }