feat: 接入真实登录与成员权限
- auth/members/messages: 新增超级管理员登录、成员权限和密码消息流程 - agent-config/agent: 支持服务端保存 Agent 配置并取消未配置 mock 成功结果 - projects/deploy-runs/settings: 按当前用户权限保护真实接口 - prisma: 新增用户、项目权限和平台消息表结构
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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(/\/+$/, '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user