bc3aa31289
- auth/members/messages: 新增超级管理员登录、成员权限和密码消息流程 - agent-config/agent: 支持服务端保存 Agent 配置并取消未配置 mock 成功结果 - projects/deploy-runs/settings: 按当前用户权限保护真实接口 - prisma: 新增用户、项目权限和平台消息表结构
175 lines
4.7 KiB
TypeScript
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(/\/+$/, '');
|
|
}
|
|
}
|