feat: 初始化DevOps平台后端

This commit is contained in:
湛兮
2026-06-11 20:49:59 +08:00
commit caec5a618d
88 changed files with 11893 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvConfig } from '../config/env.schema';
import {
IntegrationConfigKey,
IntegrationConfigState,
IntegrationConfigStatus,
IntegrationConfigStatusResponse,
} from './settings.types';
type EnvStringKey = {
[Key in keyof EnvConfig]-?: Extract<EnvConfig[Key], string> extends never
? never
: Key;
}[keyof EnvConfig];
type EnvKey = keyof EnvConfig;
type IntegrationDefinition = {
key: IntegrationConfigKey;
name: string;
required: EnvStringKey[];
optional?: EnvKey[];
note?: string;
};
const INTEGRATION_DEFINITIONS: IntegrationDefinition[] = [
{
key: 'jenkins',
name: 'Jenkins',
required: ['JENKINS_BASE_URL', 'JENKINS_USERNAME', 'JENKINS_API_TOKEN'],
},
{
key: 'gitea',
name: 'Gitea',
required: ['GITEA_BASE_URL', 'GITEA_TOKEN'],
optional: ['GITEA_WEBHOOK_SECRET'],
note: 'GITEA_WEBHOOK_SECRET 用于开启 webhook 签名校验。',
},
{
key: 'notification',
name: '通知平台',
required: ['NOTIFICATION_PROVIDER'],
optional: ['WECOM_WEBHOOK_URL', 'FEISHU_WEBHOOK_URL', 'NOTIFICATION_WEBHOOK_URL'],
note: '当前不强制配置机器人;后续可按 provider 接入企微、飞书或通用 webhook。',
},
{
key: 'llm',
name: 'LLM Agent',
required: ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL'],
},
{
key: 'database',
name: 'MySQL / Prisma',
required: ['DATABASE_URL'],
optional: ['USE_DATABASE_READS', 'PRISMA_CONNECT_ON_BOOT'],
},
{
key: 'redis',
name: 'Redis / BullMQ',
required: ['REDIS_URL'],
},
{
key: 'secrets',
name: '服务端密钥加密',
required: ['SECRET_ENCRYPTION_KEY'],
},
];
const PLACEHOLDER_PATTERNS = [
/replace-with-/i,
/\.example\.internal(?:[/:]|$)/i,
];
@Injectable()
export class SettingsService {
constructor(private readonly config: ConfigService<EnvConfig, true>) {}
getIntegrationConfigStatus(): IntegrationConfigStatusResponse {
return {
checkedAt: new Date().toISOString(),
integrations: INTEGRATION_DEFINITIONS.map((definition) =>
this.toIntegrationStatus(definition),
),
};
}
private toIntegrationStatus(
definition: IntegrationDefinition,
): IntegrationConfigStatus {
const configured = definition.required.filter((key) =>
this.isConfiguredValue(this.getConfigValue(key)),
);
const missing = definition.required.filter(
(key) => !this.isConfiguredValue(this.getConfigValue(key)),
);
return {
key: definition.key,
name: definition.name,
status: this.resolveStatus(configured.length, missing.length),
required: [...definition.required],
configured,
missing,
...(definition.optional ? { optional: [...definition.optional] } : {}),
...(definition.note ? { note: definition.note } : {}),
};
}
private getConfigValue(key: EnvStringKey): string | undefined {
const value = this.config.get(key, { infer: true });
return typeof value === 'string' ? value.trim() : undefined;
}
private isConfiguredValue(value: string | undefined): boolean {
if (!value) {
return false;
}
return !PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value));
}
private resolveStatus(
configuredCount: number,
missingCount: number,
): IntegrationConfigState {
if (missingCount === 0) {
return 'configured';
}
return configuredCount > 0 ? 'partial' : 'missing';
}
}