diff --git a/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md b/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md index ba7dc7c..b0ada7a 100644 --- a/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md +++ b/docs/superpowers/plans/2026-06-13-api-runtime-foundation.md @@ -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 = [ '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(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', }, diff --git a/src/runtime/config/runtime-config.service.ts b/src/runtime/config/runtime-config.service.ts index e2783da..e062aa6 100644 --- a/src/runtime/config/runtime-config.service.ts +++ b/src/runtime/config/runtime-config.service.ts @@ -22,19 +22,43 @@ const REQUIRED_CONFIG_KEYS = [ 'ADMIN_TOKEN_SECRET', ] as const; -const OPTIONAL_CONFIG_KEYS = [ +const OPTIONAL_CONFIG_CHECKS: ReadonlyArray = [ '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(key), diff --git a/src/runtime/config/runtime-config.types.ts b/src/runtime/config/runtime-config.types.ts index 4622505..4f8d565 100644 --- a/src/runtime/config/runtime-config.types.ts +++ b/src/runtime/config/runtime-config.types.ts @@ -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 { diff --git a/test/runtime/runtime-config.service.spec.ts b/test/runtime/runtime-config.service.spec.ts index 10a3f4e..d6368aa 100644 --- a/test/runtime/runtime-config.service.spec.ts +++ b/test/runtime/runtime-config.service.spec.ts @@ -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', () => {