Files
devops-platform-api/src/agent-config/agent-config.service.ts
T
湛兮 bc3aa31289 feat: 接入真实登录与成员权限
- auth/members/messages: 新增超级管理员登录、成员权限和密码消息流程
- agent-config/agent: 支持服务端保存 Agent 配置并取消未配置 mock 成功结果
- projects/deploy-runs/settings: 按当前用户权限保护真实接口
- prisma: 新增用户、项目权限和平台消息表结构
2026-06-12 00:38:23 +08:00

175 lines
4.7 KiB
TypeScript

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<AgentConfigSummary> {
this.assertSuperAdmin(user);
return this.toSummary(await this.agentConfigRepository.getRuntimeConfig());
}
async saveConfig(
user: AuthenticatedUser,
input: SaveAgentConfigInput,
): Promise<AgentConfigSummary> {
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<AgentConnectionTestResult> {
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<AgentRuntimeConfig | null> {
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<AgentRuntimeConfig> {
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<AgentRuntimeConfig> & { 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<AgentRuntimeConfig>,
after: Partial<AgentRuntimeConfig>,
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(/\/+$/, '');
}
}