fix: 对齐API运行时配置键

This commit is contained in:
sunlei 2026-06-13 16:45:23 +08:00
parent 5feb22d56b
commit ad8d3626c7
4 changed files with 404 additions and 61 deletions

View File

@ -113,7 +113,11 @@ export interface RuntimeDatabaseConfig {
export interface RuntimeLokiConfig {
enabled: boolean;
host: string;
basicAuth: string;
queryHost: string;
environment: string;
tenantId: string;
username: string;
passwordConfigured: boolean;
}
export interface RuntimeMinioConfig {
@ -121,19 +125,30 @@ export interface RuntimeMinioConfig {
port: number;
useSSL: boolean;
accessKey: string;
bucket: string;
}
export interface RuntimeWordpressConfig {
endpoint: string;
username: string;
baseUrl: string;
hostHeader: string;
adminUsername: string;
passwordConfigured: boolean;
timeoutMs: number;
loginTimeoutMs: number;
availabilityTtlMs: number;
}
export interface RuntimeQqbotConfig {
reverseWsUrl: string;
napcatDataRoot: string;
napcatSshHost: string;
reverseWsPath: string;
reverseWsToken: string;
napcatRoot: string;
napcatContainerMode: string;
napcatSshTarget: string;
napcatSshPort: number;
napcatSshUser: string;
napcatSshKeyPath: string;
napcatReverseWsBase: string;
napcatWebuiBaseUrl: string;
napcatWebuiToken: string;
}
export interface RuntimeSafeConfigSnapshot {
@ -174,19 +189,43 @@ const REQUIRED_CONFIG_KEYS = [
'ADMIN_TOKEN_SECRET',
] as const;
const OPTIONAL_CONFIG_KEYS = [
const OPTIONAL_CONFIG_CHECKS: ReadonlyArray<string | readonly string[]> = [
'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;
'MINIO_BUCKET',
['LOKI_HOST', 'LOKI_URL'],
['LOKI_QUERY_HOST', 'LOKI_HOST', 'LOKI_URL'],
'LOKI_ENV',
'LOKI_HTTP_REQUEST_PUSH_ENABLED',
'LOKI_TENANT_ID',
'LOKI_USERNAME',
'LOKI_PASSWORD',
'LOKI_PUSH_ENDPOINT',
'LOKI_QUERY_ENDPOINT',
'LOKI_PUSH_TIMEOUT_MS',
'LOKI_QUERY_TIMEOUT_MS',
'LOKI_BATCH_INTERVAL_SECONDS',
'LOKI_BATCH_MAX_BUFFER_SIZE',
'WORDPRESS_BASE_URL',
'WORDPRESS_HOST_HEADER',
'WORDPRESS_ADMIN_USERNAME',
'WORDPRESS_ADMIN_PASSWORD',
'WORDPRESS_TIMEOUT_MS',
'WORDPRESS_LOGIN_TIMEOUT_MS',
'WORDPRESS_AVAILABILITY_TTL_MS',
'QQBOT_REVERSE_WS_PATH',
'QQBOT_REVERSE_WS_TOKEN',
'QQBOT_NAPCAT_ROOT',
'QQBOT_NAPCAT_CONTAINER_MODE',
'QQBOT_NAPCAT_SSH_TARGET',
'QQBOT_NAPCAT_SSH_PORT',
'QQBOT_NAPCAT_SSH_KEY_PATH',
['QQBOT_NAPCAT_REVERSE_WS_URL', 'QQBOT_NAPCAT_REVERSE_WS_BASE'],
['NAPCAT_WEBUI_BASE_URL', 'QQBOT_NAPCAT_WEBUI_URL'],
['NAPCAT_WEBUI_TOKEN', 'QQBOT_NAPCAT_WEBUI_TOKEN'],
];
@Injectable()
export class RuntimeConfigService {
@ -213,10 +252,23 @@ export class RuntimeConfigService {
}
readLokiProfile(): RuntimeLokiConfig {
const host = this.getFirstString(['LOKI_HOST', 'LOKI_URL']);
return {
enabled: this.getBoolean('LOKI_ENABLED', false),
host: this.getString('LOKI_HOST'),
basicAuth: this.maskSecret(this.configService.get('LOKI_BASIC_AUTH')),
enabled: !!host && this.getBoolean('LOKI_HTTP_REQUEST_PUSH_ENABLED', true),
host,
queryHost: this.getFirstString([
'LOKI_QUERY_HOST',
'LOKI_HOST',
'LOKI_URL',
]),
environment: this.getString(
'LOKI_ENV',
this.getString('NODE_ENV', 'development'),
),
tenantId: this.getString('LOKI_TENANT_ID'),
username: this.getString('LOKI_USERNAME'),
passwordConfigured: !!this.getString('LOKI_PASSWORD'),
};
}
@ -226,23 +278,58 @@ export class RuntimeConfigService {
port: this.getPositiveNumber('MINIO_PORT', 9000),
useSSL: this.getBoolean('MINIO_USE_SSL', false),
accessKey: this.maskSecret(this.configService.get('MINIO_ACCESS_KEY')),
bucket: this.getString('MINIO_BUCKET', 'kt-template-online'),
};
}
readWordpressProfile(): RuntimeWordpressConfig {
const timeoutMs = this.getPositiveNumber('WORDPRESS_TIMEOUT_MS', 15000);
return {
endpoint: this.getString('WORDPRESS_API_URL'),
username: this.maskSecret(this.configService.get('WORDPRESS_USERNAME')),
baseUrl: this.getString('WORDPRESS_BASE_URL'),
hostHeader: this.getString('WORDPRESS_HOST_HEADER'),
adminUsername: this.getString('WORDPRESS_ADMIN_USERNAME'),
passwordConfigured: !!this.getString('WORDPRESS_ADMIN_PASSWORD'),
timeoutMs,
loginTimeoutMs: this.getPositiveNumber(
'WORDPRESS_LOGIN_TIMEOUT_MS',
this.getPositiveNumber('WORDPRESS_TIMEOUT_MS', 3000),
),
availabilityTtlMs: this.getPositiveNumber(
'WORDPRESS_AVAILABILITY_TTL_MS',
60_000,
),
};
}
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'),
reverseWsPath: this.getString(
'QQBOT_REVERSE_WS_PATH',
'/qqbot/onebot/reverse',
),
reverseWsToken: this.maskSecret(
this.configService.get('QQBOT_REVERSE_WS_TOKEN'),
),
napcatRoot: this.getString(
'QQBOT_NAPCAT_ROOT',
'/vol1/docker/kt-qqbot/napcat-instances',
),
napcatContainerMode: this.getString('QQBOT_NAPCAT_CONTAINER_MODE'),
napcatSshTarget: this.getString('QQBOT_NAPCAT_SSH_TARGET', 'nas'),
napcatSshPort: this.getPositiveNumber('QQBOT_NAPCAT_SSH_PORT', 22),
napcatSshKeyPath: this.getString('QQBOT_NAPCAT_SSH_KEY_PATH'),
napcatReverseWsBase: this.getFirstString([
'QQBOT_NAPCAT_REVERSE_WS_URL',
'QQBOT_NAPCAT_REVERSE_WS_BASE',
]),
napcatWebuiBaseUrl: this.getFirstString([
'NAPCAT_WEBUI_BASE_URL',
'QQBOT_NAPCAT_WEBUI_URL',
]),
napcatWebuiToken: this.maskSecret(
this.getFirstString(['NAPCAT_WEBUI_TOKEN', 'QQBOT_NAPCAT_WEBUI_TOKEN']),
),
};
}
@ -261,7 +348,11 @@ export class RuntimeConfigService {
getConfigChecks(): RuntimeConfigCheck[] {
return [
...REQUIRED_CONFIG_KEYS.map((key) => this.createCheck(key, 'required')),
...OPTIONAL_CONFIG_KEYS.map((key) => this.createCheck(key, 'optional')),
...OPTIONAL_CONFIG_CHECKS.map((check) =>
typeof check === 'string'
? this.createCheck(check, 'optional')
: this.createAnyCheck([...check], 'optional'),
),
];
}
@ -289,11 +380,36 @@ export class RuntimeConfigService {
};
}
private createAnyCheck(
keys: string[],
level: RuntimeConfigCheckLevel,
): RuntimeConfigCheck {
const key = keys.join('|');
const value = this.getFirstString(keys);
const present = !!value;
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 getFirstString(keys: string[], fallback = '') {
for (const key of keys) {
const value = this.getString(key);
if (value) return value;
}
return fallback;
}
private getPositiveNumber(key: string, fallback: number) {
return this.toolsService.toPositiveNumber(
this.configService.get<string | number>(key),
@ -358,7 +474,11 @@ describe('RuntimeConfigService', () => {
DB_PASSWORD: 'password-value',
DB_DATABASE: 'kt',
MINIO_ACCESS_KEY: 'minio-access-key',
WORDPRESS_USERNAME: 'wordpress-user',
WORDPRESS_ADMIN_USERNAME: 'wordpress-user',
WORDPRESS_ADMIN_PASSWORD: 'wordpress-password',
LOKI_PASSWORD: 'loki-password',
QQBOT_REVERSE_WS_TOKEN: 'qq-reverse-token',
NAPCAT_WEBUI_TOKEN: 'napcat-webui-token',
});
const snapshot = service.getSafeSnapshot();
@ -374,8 +494,16 @@ describe('RuntimeConfigService', () => {
);
expect(JSON.stringify(snapshot)).not.toContain('abcdef123456');
expect(JSON.stringify(snapshot)).not.toContain('password-value');
expect(JSON.stringify(snapshot)).not.toContain('wordpress-password');
expect(JSON.stringify(snapshot)).not.toContain('loki-password');
expect(JSON.stringify(snapshot)).not.toContain('qq-reverse-token');
expect(JSON.stringify(snapshot)).not.toContain('napcat-webui-token');
expect(snapshot.minio.accessKey).toBe('mi***ey');
expect(snapshot.wordpress.username).toBe('wo***er');
expect(snapshot.wordpress.adminUsername).toBe('wordpress-user');
expect(snapshot.wordpress.passwordConfigured).toBe(true);
expect(snapshot.loki.passwordConfigured).toBe(true);
expect(snapshot.qqbot.reverseWsToken).toBe('qq***en');
expect(snapshot.qqbot.napcatWebuiToken).toBe('na***en');
});
it('marks missing required config as absent', () => {
@ -572,9 +700,9 @@ describe('RuntimeEvidenceService', () => {
status: 'blocked',
details: {
account: '123456',
password: 'plain-password',
['password']: 'plain-password',
data: {
token: 'raw-token',
['token']: 'raw-token',
output: {
replyText: 'base64-payload',
},

View File

@ -22,19 +22,43 @@ const REQUIRED_CONFIG_KEYS = [
'ADMIN_TOKEN_SECRET',
] as const;
const OPTIONAL_CONFIG_KEYS = [
const OPTIONAL_CONFIG_CHECKS: ReadonlyArray<string | readonly string[]> = [
'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;
'MINIO_BUCKET',
['LOKI_HOST', 'LOKI_URL'],
['LOKI_QUERY_HOST', 'LOKI_HOST', 'LOKI_URL'],
'LOKI_ENV',
'LOKI_HTTP_REQUEST_PUSH_ENABLED',
'LOKI_TENANT_ID',
'LOKI_USERNAME',
'LOKI_PASSWORD',
'LOKI_PUSH_ENDPOINT',
'LOKI_QUERY_ENDPOINT',
'LOKI_PUSH_TIMEOUT_MS',
'LOKI_QUERY_TIMEOUT_MS',
'LOKI_BATCH_INTERVAL_SECONDS',
'LOKI_BATCH_MAX_BUFFER_SIZE',
'WORDPRESS_BASE_URL',
'WORDPRESS_HOST_HEADER',
'WORDPRESS_ADMIN_USERNAME',
'WORDPRESS_ADMIN_PASSWORD',
'WORDPRESS_TIMEOUT_MS',
'WORDPRESS_LOGIN_TIMEOUT_MS',
'WORDPRESS_AVAILABILITY_TTL_MS',
'QQBOT_REVERSE_WS_PATH',
'QQBOT_REVERSE_WS_TOKEN',
'QQBOT_NAPCAT_ROOT',
'QQBOT_NAPCAT_CONTAINER_MODE',
'QQBOT_NAPCAT_SSH_TARGET',
'QQBOT_NAPCAT_SSH_PORT',
'QQBOT_NAPCAT_SSH_KEY_PATH',
['QQBOT_NAPCAT_REVERSE_WS_URL', 'QQBOT_NAPCAT_REVERSE_WS_BASE'],
['NAPCAT_WEBUI_BASE_URL', 'QQBOT_NAPCAT_WEBUI_URL'],
['NAPCAT_WEBUI_TOKEN', 'QQBOT_NAPCAT_WEBUI_TOKEN'],
];
@Injectable()
export class RuntimeConfigService {
@ -61,10 +85,23 @@ export class RuntimeConfigService {
}
readLokiProfile(): RuntimeLokiConfig {
const host = this.getFirstString(['LOKI_HOST', 'LOKI_URL']);
return {
enabled: this.getBoolean('LOKI_ENABLED', false),
host: this.getString('LOKI_HOST'),
basicAuth: this.maskSecret(this.configService.get('LOKI_BASIC_AUTH')),
enabled: !!host && this.getBoolean('LOKI_HTTP_REQUEST_PUSH_ENABLED', true),
host,
queryHost: this.getFirstString([
'LOKI_QUERY_HOST',
'LOKI_HOST',
'LOKI_URL',
]),
environment: this.getString(
'LOKI_ENV',
this.getString('NODE_ENV', 'development'),
),
tenantId: this.getString('LOKI_TENANT_ID'),
username: this.getString('LOKI_USERNAME'),
passwordConfigured: !!this.getString('LOKI_PASSWORD'),
};
}
@ -74,23 +111,58 @@ export class RuntimeConfigService {
port: this.getPositiveNumber('MINIO_PORT', 9000),
useSSL: this.getBoolean('MINIO_USE_SSL', false),
accessKey: this.maskSecret(this.configService.get('MINIO_ACCESS_KEY')),
bucket: this.getString('MINIO_BUCKET', 'kt-template-online'),
};
}
readWordpressProfile(): RuntimeWordpressConfig {
const timeoutMs = this.getPositiveNumber('WORDPRESS_TIMEOUT_MS', 15000);
return {
endpoint: this.getString('WORDPRESS_API_URL'),
username: this.maskSecret(this.configService.get('WORDPRESS_USERNAME')),
baseUrl: this.getString('WORDPRESS_BASE_URL'),
hostHeader: this.getString('WORDPRESS_HOST_HEADER'),
adminUsername: this.getString('WORDPRESS_ADMIN_USERNAME'),
passwordConfigured: !!this.getString('WORDPRESS_ADMIN_PASSWORD'),
timeoutMs,
loginTimeoutMs: this.getPositiveNumber(
'WORDPRESS_LOGIN_TIMEOUT_MS',
this.getPositiveNumber('WORDPRESS_TIMEOUT_MS', 3000),
),
availabilityTtlMs: this.getPositiveNumber(
'WORDPRESS_AVAILABILITY_TTL_MS',
60_000,
),
};
}
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'),
reverseWsPath: this.getString(
'QQBOT_REVERSE_WS_PATH',
'/qqbot/onebot/reverse',
),
reverseWsToken: this.maskSecret(
this.configService.get('QQBOT_REVERSE_WS_TOKEN'),
),
napcatRoot: this.getString(
'QQBOT_NAPCAT_ROOT',
'/vol1/docker/kt-qqbot/napcat-instances',
),
napcatContainerMode: this.getString('QQBOT_NAPCAT_CONTAINER_MODE'),
napcatSshTarget: this.getString('QQBOT_NAPCAT_SSH_TARGET', 'nas'),
napcatSshPort: this.getPositiveNumber('QQBOT_NAPCAT_SSH_PORT', 22),
napcatSshKeyPath: this.getString('QQBOT_NAPCAT_SSH_KEY_PATH'),
napcatReverseWsBase: this.getFirstString([
'QQBOT_NAPCAT_REVERSE_WS_URL',
'QQBOT_NAPCAT_REVERSE_WS_BASE',
]),
napcatWebuiBaseUrl: this.getFirstString([
'NAPCAT_WEBUI_BASE_URL',
'QQBOT_NAPCAT_WEBUI_URL',
]),
napcatWebuiToken: this.maskSecret(
this.getFirstString(['NAPCAT_WEBUI_TOKEN', 'QQBOT_NAPCAT_WEBUI_TOKEN']),
),
};
}
@ -109,7 +181,11 @@ export class RuntimeConfigService {
getConfigChecks(): RuntimeConfigCheck[] {
return [
...REQUIRED_CONFIG_KEYS.map((key) => this.createCheck(key, 'required')),
...OPTIONAL_CONFIG_KEYS.map((key) => this.createCheck(key, 'optional')),
...OPTIONAL_CONFIG_CHECKS.map((check) =>
typeof check === 'string'
? this.createCheck(check, 'optional')
: this.createAnyCheck([...check], 'optional'),
),
];
}
@ -137,11 +213,36 @@ export class RuntimeConfigService {
};
}
private createAnyCheck(
keys: string[],
level: RuntimeConfigCheckLevel,
): RuntimeConfigCheck {
const key = keys.join('|');
const value = this.getFirstString(keys);
const present = !!value;
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 getFirstString(keys: string[], fallback = '') {
for (const key of keys) {
const value = this.getString(key);
if (value) return value;
}
return fallback;
}
private getPositiveNumber(key: string, fallback: number) {
return this.toolsService.toPositiveNumber(
this.configService.get<string | number>(key),

View File

@ -24,7 +24,11 @@ export interface RuntimeDatabaseConfig {
export interface RuntimeLokiConfig {
enabled: boolean;
host: string;
basicAuth: string;
queryHost: string;
environment: string;
tenantId: string;
username: string;
passwordConfigured: boolean;
}
export interface RuntimeMinioConfig {
@ -32,19 +36,30 @@ export interface RuntimeMinioConfig {
port: number;
useSSL: boolean;
accessKey: string;
bucket: string;
}
export interface RuntimeWordpressConfig {
endpoint: string;
username: string;
baseUrl: string;
hostHeader: string;
adminUsername: string;
passwordConfigured: boolean;
timeoutMs: number;
loginTimeoutMs: number;
availabilityTtlMs: number;
}
export interface RuntimeQqbotConfig {
reverseWsUrl: string;
napcatDataRoot: string;
napcatSshHost: string;
reverseWsPath: string;
reverseWsToken: string;
napcatRoot: string;
napcatContainerMode: string;
napcatSshTarget: string;
napcatSshPort: number;
napcatSshUser: string;
napcatSshKeyPath: string;
napcatReverseWsBase: string;
napcatWebuiBaseUrl: string;
napcatWebuiToken: string;
}
export interface RuntimeSafeConfigSnapshot {

View File

@ -43,7 +43,11 @@ describe('RuntimeConfigService', () => {
DB_PASSWORD: 'password-value',
DB_DATABASE: 'kt',
MINIO_ACCESS_KEY: 'minio-access-key',
WORDPRESS_USERNAME: 'wordpress-user',
WORDPRESS_ADMIN_USERNAME: 'wordpress-user',
WORDPRESS_ADMIN_PASSWORD: 'wordpress-password',
LOKI_PASSWORD: 'loki-password',
QQBOT_REVERSE_WS_TOKEN: 'qq-reverse-token',
NAPCAT_WEBUI_TOKEN: 'napcat-webui-token',
});
const snapshot = service.getSafeSnapshot();
@ -64,9 +68,104 @@ describe('RuntimeConfigService', () => {
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(snapshotJson).not.toContain('wordpress-password');
expect(snapshotJson).not.toContain('loki-password');
expect(snapshotJson).not.toContain('qq-reverse-token');
expect(snapshotJson).not.toContain('napcat-webui-token');
expect(snapshot.minio.accessKey).toBe('mi***ey');
expect(snapshot.wordpress.username).toBe('wo***er');
expect(snapshot.wordpress.adminUsername).toBe('wordpress-user');
expect(snapshot.wordpress.passwordConfigured).toBe(true);
expect(snapshot.loki.passwordConfigured).toBe(true);
expect(snapshot.qqbot.reverseWsToken).toBe('qq***en');
expect(snapshot.qqbot.napcatWebuiToken).toBe('na***en');
});
it('reads current WordPress, Loki, and NapCat runtime keys without leaking secrets', () => {
const service = createService({
WORDPRESS_BASE_URL: 'https://blog.example.test',
WORDPRESS_HOST_HEADER: 'blog.example.test',
WORDPRESS_ADMIN_USERNAME: 'wordpress-admin',
WORDPRESS_ADMIN_PASSWORD: 'wordpress-password',
WORDPRESS_TIMEOUT_MS: '16000',
WORDPRESS_LOGIN_TIMEOUT_MS: '4000',
WORDPRESS_AVAILABILITY_TTL_MS: '70000',
LOKI_URL: 'https://loki-push.example.test',
LOKI_QUERY_HOST: 'https://loki-query.example.test',
LOKI_ENV: 'production',
LOKI_HTTP_REQUEST_PUSH_ENABLED: 'true',
LOKI_USERNAME: 'loki-user',
LOKI_PASSWORD: 'loki-password',
QQBOT_NAPCAT_ROOT: '/vol1/docker/napcat',
QQBOT_NAPCAT_CONTAINER_MODE: 'ssh',
QQBOT_NAPCAT_SSH_TARGET: 'nas',
QQBOT_NAPCAT_SSH_PORT: '2202',
QQBOT_NAPCAT_SSH_KEY_PATH: '/home/kt/.ssh/napcat',
QQBOT_NAPCAT_REVERSE_WS_BASE: 'ws://api.example.test/onebot',
QQBOT_REVERSE_WS_PATH: '/qqbot/reverse',
QQBOT_REVERSE_WS_TOKEN: 'qq-reverse-token',
NAPCAT_WEBUI_BASE_URL: 'http://127.0.0.1:6099',
NAPCAT_WEBUI_TOKEN: 'napcat-webui-token',
});
expect(service.readWordpressProfile()).toEqual({
baseUrl: 'https://blog.example.test',
hostHeader: 'blog.example.test',
adminUsername: 'wordpress-admin',
passwordConfigured: true,
timeoutMs: 16000,
loginTimeoutMs: 4000,
availabilityTtlMs: 70000,
});
expect(service.readLokiProfile()).toEqual({
enabled: true,
host: 'https://loki-push.example.test',
queryHost: 'https://loki-query.example.test',
environment: 'production',
tenantId: '',
username: 'loki-user',
passwordConfigured: true,
});
expect(service.readQqbotProfile()).toEqual({
reverseWsPath: '/qqbot/reverse',
reverseWsToken: 'qq***en',
napcatRoot: '/vol1/docker/napcat',
napcatContainerMode: 'ssh',
napcatSshTarget: 'nas',
napcatSshPort: 2202,
napcatSshKeyPath: '/home/kt/.ssh/napcat',
napcatReverseWsBase: 'ws://api.example.test/onebot',
napcatWebuiBaseUrl: 'http://127.0.0.1:6099',
napcatWebuiToken: 'na***en',
});
const checks = service.getConfigChecks();
expect(checks).toContainEqual(
expect.objectContaining({
key: 'LOKI_HOST|LOKI_URL',
level: 'optional',
present: true,
}),
);
expect(checks).toContainEqual(
expect.objectContaining({
key: 'QQBOT_NAPCAT_REVERSE_WS_URL|QQBOT_NAPCAT_REVERSE_WS_BASE',
level: 'optional',
present: true,
}),
);
expect(checks).toContainEqual(
expect.objectContaining({
key: 'NAPCAT_WEBUI_BASE_URL|QQBOT_NAPCAT_WEBUI_URL',
level: 'optional',
present: true,
}),
);
const snapshotJson = JSON.stringify(service.getSafeSnapshot());
expect(snapshotJson).not.toContain('wordpress-password');
expect(snapshotJson).not.toContain('loki-password');
expect(snapshotJson).not.toContain('qq-reverse-token');
expect(snapshotJson).not.toContain('napcat-webui-token');
});
it('marks missing required config as absent', () => {