import { Injectable } from '@nestjs/common'; import { AuditService } from '../audit/audit.service'; import { AuthenticatedUser } from '../auth/auth.types'; import { AppError } from '../common/errors/app-error'; import { redactSensitive } from '../common/security/redact-sensitive'; import { SaveAgentConfigInput } from './agent-config.dto'; import { AgentConfigRepository } from './agent-config.repository'; import { AgentConfigSummary, AgentConnectionTestResult, AgentRuntimeConfig, } from './agent-config.types'; const AGENT_CONFIG_WARNING = '该配置影响系统 Agent 能力,请谨慎修改。密钥仅在服务端加密保存,前端不会返回完整明文。'; @Injectable() export class AgentConfigService { constructor( private readonly agentConfigRepository: AgentConfigRepository, private readonly auditService: AuditService, ) {} async getConfig(user: AuthenticatedUser): Promise { this.assertSuperAdmin(user); return this.toSummary(await this.agentConfigRepository.getRuntimeConfig()); } async saveConfig( user: AuthenticatedUser, input: SaveAgentConfigInput, ): Promise { this.assertSuperAdmin(user); const before = await this.agentConfigRepository.getRuntimeConfig(); const savedConfig = await this.agentConfigRepository.saveConfig(input); await this.auditService.record({ action: 'AGENT_CONFIG_UPDATED', resourceType: 'agent_config', actorId: user.id, actorName: user.account, before: { baseURL: before.baseURL, model: before.model, hasKey: Boolean(before.key), }, after: { baseURL: savedConfig.baseURL, model: savedConfig.model, hasKey: Boolean(savedConfig.key), changedFields: this.changedFields(before, savedConfig, Boolean(input.key)), }, }); return this.toSummary(savedConfig); } async testConnection( user: AuthenticatedUser, ): Promise { this.assertSuperAdmin(user); const config = await this.requireRuntimeConfig(); try { const response = await fetch(`${this.trimSlash(config.baseURL)}/models`, { method: 'GET', headers: { authorization: `Bearer ${config.key}`, }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return { success: true, message: 'Agent 连接测试成功', checkedAt: new Date().toISOString(), }; } catch (error) { const message = error instanceof Error ? error.message : 'Agent 连接测试失败'; const safeMessage = redactSensitive({ message }).message; throw new AppError( 'INTEGRATION_REQUEST_FAILED', `Agent 连接测试失败:${safeMessage}`, 400, ); } } async resolveRuntimeConfig(): Promise { const config = await this.agentConfigRepository.getRuntimeConfig(); if (!config.key || !config.baseURL || !config.model) { return null; } return { key: config.key, baseURL: config.baseURL, model: config.model, }; } async requireRuntimeConfig(): Promise { const config = await this.resolveRuntimeConfig(); if (!config) { throw new AppError( 'CONFIG_MISSING', 'Agent 配置不完整,请联系超级管理员配置 key、baseURL 和 model', 500, ); } return config; } private assertSuperAdmin(user: AuthenticatedUser): void { if (user.role !== 'super_admin') { throw new AppError('FORBIDDEN', '无权限访问 Agent 配置', 403); } } private toSummary( config: Partial & { updatedAt?: string }, ): AgentConfigSummary { return { baseURL: config.baseURL ?? '', model: config.model ?? '', hasKey: Boolean(config.key), maskedKey: config.key ? this.maskKey(config.key) : undefined, warning: AGENT_CONFIG_WARNING, updatedAt: config.updatedAt, }; } private maskKey(key: string): string { if (key.length <= 8) { return `${key.slice(0, 2)}****${key.slice(-2)}`; } return `${key.slice(0, 3)}****${key.slice(-4)}`; } private changedFields( before: Partial, after: Partial, keyProvided: boolean, ): string[] { const fields: string[] = []; if (keyProvided && before.key !== after.key) { fields.push('key'); } if (before.baseURL !== after.baseURL) { fields.push('baseURL'); } if (before.model !== after.model) { fields.push('model'); } return fields; } private trimSlash(value: string): string { return value.replace(/\/+$/, ''); } }