feat: 添加API运行时配置基础
This commit is contained in:
parent
6433790cf5
commit
5feb22d56b
158
src/runtime/config/runtime-config.service.ts
Normal file
158
src/runtime/config/runtime-config.service.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ToolsService } from '../../common';
|
||||||
|
import {
|
||||||
|
RuntimeAppConfig,
|
||||||
|
RuntimeConfigCheck,
|
||||||
|
RuntimeConfigCheckLevel,
|
||||||
|
RuntimeDatabaseConfig,
|
||||||
|
RuntimeLokiConfig,
|
||||||
|
RuntimeMinioConfig,
|
||||||
|
RuntimeQqbotConfig,
|
||||||
|
RuntimeSafeConfigSnapshot,
|
||||||
|
RuntimeWordpressConfig,
|
||||||
|
} from './runtime-config.types';
|
||||||
|
|
||||||
|
const REQUIRED_CONFIG_KEYS = [
|
||||||
|
'DB_HOST',
|
||||||
|
'DB_PORT',
|
||||||
|
'DB_USERNAME',
|
||||||
|
'DB_PASSWORD',
|
||||||
|
'DB_DATABASE',
|
||||||
|
'ADMIN_TOKEN_SECRET',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const OPTIONAL_CONFIG_KEYS = [
|
||||||
|
'MINIO_ENDPOINT',
|
||||||
|
'MINIO_PORT',
|
||||||
|
'MINIO_ACCESS_KEY',
|
||||||
|
'MINIO_SECRET_KEY',
|
||||||
|
'LOKI_HOST',
|
||||||
|
'WORDPRESS_API_URL',
|
||||||
|
'QQBOT_REVERSE_WS_URL',
|
||||||
|
'NAPCAT_DATA_ROOT',
|
||||||
|
'NAPCAT_SSH_HOST',
|
||||||
|
'NAPCAT_SSH_PORT',
|
||||||
|
'NAPCAT_SSH_USER',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RuntimeConfigService {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly toolsService: ToolsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
readAppProfile(): RuntimeAppConfig {
|
||||||
|
return {
|
||||||
|
nodeEnv: this.getString('NODE_ENV', 'development'),
|
||||||
|
port: this.getPositiveNumber('PORT', 48085),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
readDatabaseProfile(): RuntimeDatabaseConfig {
|
||||||
|
return {
|
||||||
|
host: this.getString('DB_HOST'),
|
||||||
|
port: this.getPositiveNumber('DB_PORT', 3306),
|
||||||
|
database: this.getString('DB_DATABASE'),
|
||||||
|
username: this.getString('DB_USERNAME'),
|
||||||
|
synchronize: this.getBoolean('DB_SYNC', false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
readLokiProfile(): RuntimeLokiConfig {
|
||||||
|
return {
|
||||||
|
enabled: this.getBoolean('LOKI_ENABLED', false),
|
||||||
|
host: this.getString('LOKI_HOST'),
|
||||||
|
basicAuth: this.maskSecret(this.configService.get('LOKI_BASIC_AUTH')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
readMinioProfile(): RuntimeMinioConfig {
|
||||||
|
return {
|
||||||
|
endpoint: this.getString('MINIO_ENDPOINT'),
|
||||||
|
port: this.getPositiveNumber('MINIO_PORT', 9000),
|
||||||
|
useSSL: this.getBoolean('MINIO_USE_SSL', false),
|
||||||
|
accessKey: this.maskSecret(this.configService.get('MINIO_ACCESS_KEY')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
readWordpressProfile(): RuntimeWordpressConfig {
|
||||||
|
return {
|
||||||
|
endpoint: this.getString('WORDPRESS_API_URL'),
|
||||||
|
username: this.maskSecret(this.configService.get('WORDPRESS_USERNAME')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
readQqbotProfile(): RuntimeQqbotConfig {
|
||||||
|
return {
|
||||||
|
reverseWsUrl: this.getString('QQBOT_REVERSE_WS_URL'),
|
||||||
|
napcatDataRoot: this.getString('NAPCAT_DATA_ROOT'),
|
||||||
|
napcatSshHost: this.getString('NAPCAT_SSH_HOST'),
|
||||||
|
napcatSshPort: this.getPositiveNumber('NAPCAT_SSH_PORT', 22),
|
||||||
|
napcatSshUser: this.getString('NAPCAT_SSH_USER'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSafeSnapshot(): RuntimeSafeConfigSnapshot {
|
||||||
|
return {
|
||||||
|
app: this.readAppProfile(),
|
||||||
|
database: this.readDatabaseProfile(),
|
||||||
|
loki: this.readLokiProfile(),
|
||||||
|
minio: this.readMinioProfile(),
|
||||||
|
wordpress: this.readWordpressProfile(),
|
||||||
|
qqbot: this.readQqbotProfile(),
|
||||||
|
checks: this.getConfigChecks(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigChecks(): RuntimeConfigCheck[] {
|
||||||
|
return [
|
||||||
|
...REQUIRED_CONFIG_KEYS.map((key) => this.createCheck(key, 'required')),
|
||||||
|
...OPTIONAL_CONFIG_KEYS.map((key) => this.createCheck(key, 'optional')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
maskSecret(value: unknown): string {
|
||||||
|
const text = this.toolsService.toSecretText(value);
|
||||||
|
if (!text) return '';
|
||||||
|
if (text.length <= 4) return '****';
|
||||||
|
return `${text.slice(0, 2)}***${text.slice(-2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCheck(
|
||||||
|
key: string,
|
||||||
|
level: RuntimeConfigCheckLevel,
|
||||||
|
): RuntimeConfigCheck {
|
||||||
|
const value = this.configService.get(key);
|
||||||
|
const text = this.toolsService.toSecretText(value);
|
||||||
|
const present = !!text;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
level,
|
||||||
|
present,
|
||||||
|
maskedValue: present ? this.maskSecret(value) : undefined,
|
||||||
|
message: present ? undefined : `${key} is not configured`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getString(key: string, fallback = '') {
|
||||||
|
const value = this.toolsService.toTrimmedString(this.configService.get(key));
|
||||||
|
return value || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPositiveNumber(key: string, fallback: number) {
|
||||||
|
return this.toolsService.toPositiveNumber(
|
||||||
|
this.configService.get<string | number>(key),
|
||||||
|
fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBoolean(key: string, fallback: boolean) {
|
||||||
|
return this.toolsService.normalizeBoolean(
|
||||||
|
this.configService.get<string | boolean | number>(key),
|
||||||
|
fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/runtime/config/runtime-config.types.ts
Normal file
58
src/runtime/config/runtime-config.types.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
export type RuntimeConfigCheckLevel = 'required' | 'optional';
|
||||||
|
|
||||||
|
export interface RuntimeConfigCheck {
|
||||||
|
key: string;
|
||||||
|
level: RuntimeConfigCheckLevel;
|
||||||
|
present: boolean;
|
||||||
|
maskedValue?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeAppConfig {
|
||||||
|
nodeEnv: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeDatabaseConfig {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
database: string;
|
||||||
|
username: string;
|
||||||
|
synchronize: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeLokiConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
basicAuth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeMinioConfig {
|
||||||
|
endpoint: string;
|
||||||
|
port: number;
|
||||||
|
useSSL: boolean;
|
||||||
|
accessKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeWordpressConfig {
|
||||||
|
endpoint: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeQqbotConfig {
|
||||||
|
reverseWsUrl: string;
|
||||||
|
napcatDataRoot: string;
|
||||||
|
napcatSshHost: string;
|
||||||
|
napcatSshPort: number;
|
||||||
|
napcatSshUser: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeSafeConfigSnapshot {
|
||||||
|
app: RuntimeAppConfig;
|
||||||
|
database: RuntimeDatabaseConfig;
|
||||||
|
loki: RuntimeLokiConfig;
|
||||||
|
minio: RuntimeMinioConfig;
|
||||||
|
wordpress: RuntimeWordpressConfig;
|
||||||
|
qqbot: RuntimeQqbotConfig;
|
||||||
|
checks: RuntimeConfigCheck[];
|
||||||
|
}
|
||||||
13
src/runtime/errors/runtime-error.types.ts
Normal file
13
src/runtime/errors/runtime-error.types.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export type RuntimeErrorCategory =
|
||||||
|
| 'config_error'
|
||||||
|
| 'dependency_unavailable'
|
||||||
|
| 'operation_failed'
|
||||||
|
| 'cleanup_failed';
|
||||||
|
|
||||||
|
export interface RuntimeClassifiedError {
|
||||||
|
category: RuntimeErrorCategory;
|
||||||
|
operation: string;
|
||||||
|
message: string;
|
||||||
|
cause?: string;
|
||||||
|
retryable: boolean;
|
||||||
|
}
|
||||||
88
test/runtime/runtime-config.service.spec.ts
Normal file
88
test/runtime/runtime-config.service.spec.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ToolsService } from '../../src/common';
|
||||||
|
import { RuntimeConfigService } from '../../src/runtime/config/runtime-config.service';
|
||||||
|
|
||||||
|
function createService(values: Record<string, unknown>) {
|
||||||
|
const configService = {
|
||||||
|
get: jest.fn((key: string) => values[key]),
|
||||||
|
} as unknown as ConfigService;
|
||||||
|
|
||||||
|
return new RuntimeConfigService(configService, new ToolsService());
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RuntimeConfigService', () => {
|
||||||
|
it('parses app and database profiles with stable defaults', () => {
|
||||||
|
const service = createService({
|
||||||
|
DB_HOST: '127.0.0.1',
|
||||||
|
DB_PORT: '3307',
|
||||||
|
DB_DATABASE: 'kt',
|
||||||
|
DB_USERNAME: 'admin',
|
||||||
|
DB_SYNC: 'true',
|
||||||
|
NODE_ENV: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.readAppProfile()).toEqual({
|
||||||
|
nodeEnv: 'test',
|
||||||
|
port: 48085,
|
||||||
|
});
|
||||||
|
expect(service.readDatabaseProfile()).toEqual({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3307,
|
||||||
|
database: 'kt',
|
||||||
|
username: 'admin',
|
||||||
|
synchronize: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('masks secrets in checks and snapshots', () => {
|
||||||
|
const service = createService({
|
||||||
|
ADMIN_TOKEN_SECRET: 'abcdef123456',
|
||||||
|
DB_HOST: 'mysql',
|
||||||
|
DB_PORT: '3306',
|
||||||
|
DB_USERNAME: 'root',
|
||||||
|
DB_PASSWORD: 'password-value',
|
||||||
|
DB_DATABASE: 'kt',
|
||||||
|
MINIO_ACCESS_KEY: 'minio-access-key',
|
||||||
|
WORDPRESS_USERNAME: 'wordpress-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = service.getSafeSnapshot();
|
||||||
|
const snapshotJson = JSON.stringify(snapshot);
|
||||||
|
const adminSecretCheck = snapshot.checks.find(
|
||||||
|
(check) => check.key === 'ADMIN_TOKEN_SECRET',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.maskSecret('abcdef123456')).toBe('ab***56');
|
||||||
|
expect(service.maskSecret('')).toBe('');
|
||||||
|
expect(service.maskSecret('abcd')).toBe('****');
|
||||||
|
expect(adminSecretCheck).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
present: true,
|
||||||
|
maskedValue: 'ab***56',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(snapshotJson).not.toContain('abcdef123456');
|
||||||
|
expect(snapshotJson).not.toContain('password-value');
|
||||||
|
expect(snapshotJson).not.toContain('minio-access-key');
|
||||||
|
expect(snapshotJson).not.toContain('wordpress-user');
|
||||||
|
expect(snapshot.minio.accessKey).toBe('mi***ey');
|
||||||
|
expect(snapshot.wordpress.username).toBe('wo***er');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks missing required config as absent', () => {
|
||||||
|
const service = createService({
|
||||||
|
DB_HOST: 'mysql',
|
||||||
|
});
|
||||||
|
|
||||||
|
const checks = service.getConfigChecks();
|
||||||
|
|
||||||
|
expect(checks).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'DB_PASSWORD',
|
||||||
|
level: 'required',
|
||||||
|
present: false,
|
||||||
|
message: 'DB_PASSWORD is not configured',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user