feat: 暴露NapCat运行态Profile只读证据
This commit is contained in:
parent
d6c3dbad8f
commit
6738dbd3df
3
API.md
3
API.md
@ -338,6 +338,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
| `POST` | `/qqbot/account/scan/cancel?sessionId=` | 取消扫码会话 |
|
||||
| `POST` | `/qqbot/account/delete?id=` | 删除账号并断开 WS |
|
||||
| `POST` | `/qqbot/account/kick?selfId=` | 断开反向 WS 会话 |
|
||||
| `GET` | `/qqbot/napcat/runtime/detail?accountId=` | 读取账号 NapCat 运行态证据 |
|
||||
| `POST` | `/qqbot/account/bind/command` | 绑定账号和在线命令 |
|
||||
| `POST` | `/qqbot/account/unbind/command` | 解绑账号和在线命令 |
|
||||
| `POST` | `/qqbot/account/bind/rule` | 绑定账号和自动回复规则 |
|
||||
@ -353,6 +354,8 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
||||
|
||||
托管 NapCat 容器按账号持久化设备身份,`napcat_device_identity` 保存账号对应的数据目录、hostname、machine-id 路径、MAC 地址、验证状态和最近登录证据。重建同一账号容器时会复用这些设备字段,并在 Docker run 中注入 `--hostname`、`--mac-address` 和只读 `/etc/machine-id` 挂载,降低每次重建都被 QQ 判定为新设备的概率。
|
||||
|
||||
`GET /qqbot/napcat/runtime/detail?accountId=` 返回脱敏后的 NapCat runtime profile 与 protocol profile 证据,供 Admin 排查镜像、locale、shm、配置 hash 和漂移状态。接口不返回 WebUI token、reverse WS token、QQ 登录密码、SSH 私钥或运行态密码环境;账号列表只挂载 `napcat.profileStatus`、`napcat.runtimeProfile` 等摘要字段,不触发登录、重建或修复动作。
|
||||
|
||||
外发消息不直接抢发:后端会按 `QQBOT_SEND_GLOBAL_INTERVAL_MS`、`QQBOT_SEND_TARGET_INTERVAL_MS` 和 `QQBOT_SEND_JITTER_MS` 预约发送窗口,默认全局 2500ms、同会话 8000ms、抖动 0-800ms;如果等待超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS`,本次发送会在下发前被拒绝。在线命令和自动回复规则会叠加运行时保底冷却,默认命令 5000ms、规则 30000ms;复读机默认连续 4 次相同普通文本才触发,同一会话默认 10 分钟内只复读一次,并限制普通文本长度,减少自动行为被风控识别的概率。
|
||||
|
||||
### Command / Rule / Permission
|
||||
|
||||
@ -102,6 +102,16 @@ export type QqbotAccountNapcatRuntimeInfo = {
|
||||
containerName?: string;
|
||||
containerOnline?: boolean;
|
||||
containerStatus?: QqbotNapcatContainerStatus;
|
||||
profileStatus?: 'drift' | 'failed' | 'ok' | 'unknown';
|
||||
recoveryState?: 'idle' | 'password' | 'quick' | 'suspended';
|
||||
riskMode?: 'cooldown' | 'manual_only' | 'normal';
|
||||
runtimeProfile?: {
|
||||
desktopProfileVersion?: string;
|
||||
imageDigest?: string;
|
||||
imageRef?: string;
|
||||
locale?: string;
|
||||
shmSize?: string;
|
||||
};
|
||||
lastCheckedAt?: Date | null;
|
||||
lastError?: null | string;
|
||||
lastLoginAt?: Date | null;
|
||||
|
||||
@ -11,6 +11,7 @@ import type {
|
||||
QqbotAccountListItem,
|
||||
QqbotNapcatRuntimeStatusSnapshot,
|
||||
} from '@/modules/qqbot/core/contract/qqbot.types';
|
||||
import { NapcatRuntimeProfileInspectorService } from '../runtime/napcat-runtime-profile-inspector.service';
|
||||
import { QqbotNapcatContainerService } from '../../infrastructure/integration/container/qqbot-napcat-container.service';
|
||||
import { NapcatAccountBinding } from '../../infrastructure/persistence/napcat-account-binding.entity';
|
||||
import { NapcatContainer } from '../../infrastructure/persistence/napcat-container.entity';
|
||||
@ -22,11 +23,12 @@ const NAPCAT_AUTO_LOGIN_CLEANUP_FAILED_MESSAGE =
|
||||
@Injectable()
|
||||
export class QqbotNapcatAccountRuntimeService implements QqbotAccountNapcatRuntimePort {
|
||||
/**
|
||||
* 初始化 QqbotNapcatAccountRuntimeService 实例。
|
||||
* @param accountNapcatRepository - 账号仓库依赖;影响 constructor 的返回值。
|
||||
* @param napcatContainerRepository - NapCat仓库依赖;影响 constructor 的返回值。
|
||||
* @param napcatContainerService - napcatContainerService 服务依赖;影响 constructor 的返回值。
|
||||
* @param toolsService - ToolsService 依赖;影响 constructor 的返回值。
|
||||
* Creates the account-list runtime adapter that joins persisted bindings, container status, and optional profile summaries.
|
||||
* @param accountNapcatRepository - Binding repository used to pick the primary NapCat container for each QQBot account.
|
||||
* @param napcatContainerRepository - Container repository used for cached Docker/WebUI status and non-secret metadata.
|
||||
* @param napcatContainerService - Runtime integration service for bounded NapCat/WebUI status probes and auto-login.
|
||||
* @param toolsService - Shared helpers for status text normalization and NapCat offline-message classification.
|
||||
* @param runtimeProfileInspector - Optional profile reader that enriches list rows without changing login state.
|
||||
*/
|
||||
constructor(
|
||||
@InjectRepository(NapcatAccountBinding)
|
||||
@ -35,6 +37,7 @@ export class QqbotNapcatAccountRuntimeService implements QqbotAccountNapcatRunti
|
||||
private readonly napcatContainerRepository: Repository<NapcatContainer>,
|
||||
private readonly napcatContainerService: QqbotNapcatContainerService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly runtimeProfileInspector?: NapcatRuntimeProfileInspectorService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -82,6 +85,10 @@ export class QqbotNapcatAccountRuntimeService implements QqbotAccountNapcatRunti
|
||||
containerMap.set(container.id, container);
|
||||
}
|
||||
}
|
||||
const runtimeProfileSummaryMap =
|
||||
(await this.runtimeProfileInspector?.getAccountRuntimeSummaryMap(
|
||||
accountIds,
|
||||
)) || new Map();
|
||||
|
||||
return Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
@ -113,6 +120,7 @@ export class QqbotNapcatAccountRuntimeService implements QqbotAccountNapcatRunti
|
||||
oneBotOnline: account.connectStatus === 'online',
|
||||
qqLoginMessage: runtimeStatus?.qqLoginMessage,
|
||||
qqLoginStatus: runtimeStatus?.qqLoginStatus,
|
||||
...runtimeProfileSummaryMap.get(account.id),
|
||||
webuiOnline: runtimeStatus?.webuiOnline,
|
||||
webuiPort: container?.webuiPort,
|
||||
},
|
||||
|
||||
@ -0,0 +1,185 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { ToolsService } from '@/common';
|
||||
import { NapcatProtocolProfile } from '../../infrastructure/persistence/napcat-protocol-profile.entity';
|
||||
import { NapcatRuntimeProfile } from '../../infrastructure/persistence/napcat-runtime-profile.entity';
|
||||
|
||||
export type NapcatRuntimeProfileSummary = {
|
||||
profileStatus?: 'drift' | 'failed' | 'ok' | 'unknown';
|
||||
recoveryState?: 'idle' | 'password' | 'quick' | 'suspended';
|
||||
riskMode?: 'cooldown' | 'manual_only' | 'normal';
|
||||
runtimeProfile?: {
|
||||
desktopProfileVersion?: string;
|
||||
imageDigest?: string;
|
||||
imageRef?: string;
|
||||
locale?: string;
|
||||
shmSize?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class NapcatRuntimeProfileInspectorService {
|
||||
/**
|
||||
* Initializes runtime inspection over the existing SSH-managed container model.
|
||||
* @param runtimeProfileRepository - Runtime profile repository updated with latest Docker and desktop evidence.
|
||||
* @param protocolProfileRepository - Protocol profile repository updated with config hashes and drift state.
|
||||
* @param configService - Runtime config provider used for SSH target and inspection timeout defaults.
|
||||
* @param toolsService - Shared helper used to normalize string evidence before redaction.
|
||||
*/
|
||||
constructor(
|
||||
@InjectRepository(NapcatRuntimeProfile)
|
||||
private readonly runtimeProfileRepository: Repository<NapcatRuntimeProfile>,
|
||||
@InjectRepository(NapcatProtocolProfile)
|
||||
private readonly protocolProfileRepository: Repository<NapcatProtocolProfile>,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly toolsService: ToolsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Builds the remote inspection script for Docker and in-container profile evidence.
|
||||
* @param containerName - Docker container name selected from the persisted NapCat container row.
|
||||
* @returns Shell script that collects runtime evidence without reading secret env values.
|
||||
*/
|
||||
buildInspectScript(containerName: string) {
|
||||
return `
|
||||
set -eu
|
||||
NAME=${this.sh(containerName)}
|
||||
docker inspect "$NAME"
|
||||
docker exec "$NAME" sh -lc 'locale -a; locale; date +%Z; fc-match "Noto Sans CJK SC"; test ! -e /.dockerenv; cat /proc/1/cgroup; id; ps -eo user,args | grep -E "qq|NapCat|Xvfb" | grep -v grep || true'
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts secrets before evidence is stored, logged, or returned to Admin.
|
||||
* @param value - Evidence object or primitive produced by Docker, NapCat, or config writers.
|
||||
* @returns Evidence with sensitive keys and token query values replaced by placeholders.
|
||||
*/
|
||||
sanitizeEvidence(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => this.sanitizeEvidence(item));
|
||||
}
|
||||
|
||||
if (typeof value === 'string') return this.redactString(value);
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, item]) => {
|
||||
if (/password|token|secret|private[-_]?key/i.test(key)) {
|
||||
return [key, '[REDACTED]'];
|
||||
}
|
||||
return [key, this.sanitizeEvidence(item)];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sanitized runtime and protocol profile detail for one account.
|
||||
* @param accountId - Account id used to locate profile rows for the Admin detail view.
|
||||
* @returns Sanitized profile evidence suitable for API responses.
|
||||
*/
|
||||
async getAccountRuntimeDetail(accountId: string) {
|
||||
const normalizedAccountId = this.toolsService.toTrimmedString(accountId);
|
||||
const [runtimeProfile, protocolProfile] = await Promise.all([
|
||||
this.runtimeProfileRepository.findOne({
|
||||
order: { updateTime: 'DESC' },
|
||||
where: { accountId: normalizedAccountId },
|
||||
}),
|
||||
this.protocolProfileRepository.findOne({
|
||||
order: { updateTime: 'DESC' },
|
||||
where: { accountId: normalizedAccountId },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
accountId: normalizedAccountId,
|
||||
inspectionTimeoutMs: this.getInspectionTimeoutMs(),
|
||||
protocolProfile: this.sanitizeEvidence(protocolProfile),
|
||||
runtimeProfile: this.sanitizeEvidence(runtimeProfile),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads lightweight runtime-profile summaries for account list rows.
|
||||
* @param accountIds - Account ids from the current list page.
|
||||
* @returns Map keyed by account id with optional runtime profile summary.
|
||||
*/
|
||||
async getAccountRuntimeSummaryMap(accountIds: string[]) {
|
||||
const normalizedIds = accountIds
|
||||
.map((accountId) => this.toolsService.toTrimmedString(accountId))
|
||||
.filter(Boolean);
|
||||
const summaryMap = new Map<string, NapcatRuntimeProfileSummary>();
|
||||
if (normalizedIds.length <= 0) return summaryMap;
|
||||
|
||||
const profiles = await this.runtimeProfileRepository.find({
|
||||
order: { updateTime: 'DESC' },
|
||||
where: { accountId: In(normalizedIds) },
|
||||
});
|
||||
|
||||
for (const profile of profiles) {
|
||||
if (summaryMap.has(profile.accountId)) continue;
|
||||
summaryMap.set(profile.accountId, {
|
||||
profileStatus: this.toProfileStatus(profile.profileStatus),
|
||||
recoveryState: 'idle',
|
||||
runtimeProfile: {
|
||||
desktopProfileVersion: profile.desktopProfileVersion || undefined,
|
||||
imageDigest: profile.imageDigest || undefined,
|
||||
imageRef: profile.imageRef || undefined,
|
||||
locale: profile.locale || undefined,
|
||||
shmSize: profile.shmSize || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return summaryMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the bounded runtime profile inspection timeout.
|
||||
* @returns Positive timeout in milliseconds for future SSH inspection calls.
|
||||
*/
|
||||
private getInspectionTimeoutMs() {
|
||||
const value = Number(
|
||||
this.configService.get<string>(
|
||||
'QQBOT_NAPCAT_PROFILE_INSPECT_TIMEOUT_MS',
|
||||
) || 15_000,
|
||||
);
|
||||
return Number.isFinite(value) && value > 0 ? value : 15_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts persisted profile status into the account-list API vocabulary.
|
||||
* @param status - Runtime profile persistence status from the latest profile row.
|
||||
* @returns Compact status label consumed by Admin list rows.
|
||||
*/
|
||||
private toProfileStatus(
|
||||
status?: string,
|
||||
): NapcatRuntimeProfileSummary['profileStatus'] {
|
||||
if (status === 'synced') return 'ok';
|
||||
if (status === 'drifted') return 'drift';
|
||||
if (status === 'failed') return 'failed';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Redacts token query values from URL-like evidence strings.
|
||||
* @param value - Evidence string that may include token query parameters.
|
||||
* @returns String with token values replaced by `[REDACTED]`.
|
||||
*/
|
||||
private redactString(value: string) {
|
||||
return value.replace(/token=[^&\s]+/gi, 'token=[REDACTED]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes shell literals used by read-only inspection scripts.
|
||||
* @param value - Container name selected from trusted persistence.
|
||||
* @returns POSIX-safe single-quoted shell literal.
|
||||
*/
|
||||
private sh(value: string) {
|
||||
return `'${`${value}`.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { vbenSuccess } from '@/common';
|
||||
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
||||
import { NapcatRuntimeProfileInspectorService } from '../application/runtime/napcat-runtime-profile-inspector.service';
|
||||
import { QqbotNapcatRuntimeDetailQueryDto } from './qqbot-napcat-runtime.dto';
|
||||
|
||||
@ApiTags('QQBot - NapCat 运行态')
|
||||
@Controller('qqbot/napcat/runtime')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class QqbotNapcatRuntimeController {
|
||||
/**
|
||||
* Creates the read-only runtime evidence controller for Admin.
|
||||
* @param inspector - Service that loads and redacts NapCat runtime/profile evidence before response serialization.
|
||||
*/
|
||||
constructor(
|
||||
private readonly inspector: NapcatRuntimeProfileInspectorService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Reads sanitized NapCat runtime and protocol profile evidence for one QQBot account.
|
||||
* @param query - Query object carrying the account id selected from the Admin account list.
|
||||
* @returns Vben response wrapper containing only sanitized read-only runtime evidence.
|
||||
*/
|
||||
@Get('detail')
|
||||
@ApiOperation({ summary: '查询 NapCat 运行态与协议 Profile 证据' })
|
||||
async detail(@Query() query: QqbotNapcatRuntimeDetailQueryDto) {
|
||||
return vbenSuccess(
|
||||
await this.inspector.getAccountRuntimeDetail(query.accountId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QqbotNapcatRuntimeDetailQueryDto {
|
||||
@ApiProperty()
|
||||
accountId: string;
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
export * from './qqbot-napcat.module';
|
||||
export * from './contract/qqbot-napcat-login.controller';
|
||||
export * from './contract/qqbot-napcat-login.dto';
|
||||
export * from './contract/qqbot-napcat-runtime.controller';
|
||||
export * from './contract/qqbot-napcat-runtime.dto';
|
||||
export * from './infrastructure/persistence/napcat-account-binding.entity';
|
||||
export * from './infrastructure/persistence/napcat-container.entity';
|
||||
export * from './infrastructure/persistence/napcat-device-identity.entity';
|
||||
@ -13,5 +15,6 @@ export * from './infrastructure/integration/napcat-login-api.client';
|
||||
export * from './domain/login/napcat-login-state-machine';
|
||||
export * from './application/login/qqbot-napcat-login.service';
|
||||
export * from './application/login/qqbot-napcat-watchdog.service';
|
||||
export * from './application/runtime/napcat-runtime-profile-inspector.service';
|
||||
export * from './infrastructure/persistence';
|
||||
export * from './infrastructure/integration/container/qqbot-napcat-container.service';
|
||||
|
||||
@ -8,23 +8,27 @@ import { QqbotNapcatAccountRuntimeService } from './application/account-runtime/
|
||||
import { QqbotNapcatLoginService } from './application/login/qqbot-napcat-login.service';
|
||||
import { QqbotNapcatWatchdogService } from './application/login/qqbot-napcat-watchdog.service';
|
||||
import { NapcatConfigWriterService } from './application/runtime/napcat-config-writer.service';
|
||||
import { NapcatRuntimeProfileInspectorService } from './application/runtime/napcat-runtime-profile-inspector.service';
|
||||
import { NapcatRuntimeProfileService } from './application/runtime/napcat-runtime-profile.service';
|
||||
import { QqbotNapcatLoginController } from './contract/qqbot-napcat-login.controller';
|
||||
import { QqbotNapcatRuntimeController } from './contract/qqbot-napcat-runtime.controller';
|
||||
import { QqbotNapcatContainerService } from './infrastructure/integration/container/qqbot-napcat-container.service';
|
||||
import { NapcatDeviceIdentityService } from './infrastructure/integration/device/napcat-device-identity.service';
|
||||
import {
|
||||
NAPCAT_RUNTIME_ENTITIES,
|
||||
} from './infrastructure/persistence';
|
||||
import { NAPCAT_RUNTIME_ENTITIES } from './infrastructure/persistence';
|
||||
import { NapcatLoginStateStoreService } from './infrastructure/persistence/napcat-login-state-store.service';
|
||||
|
||||
export const QQBOT_NAPCAT_ENTITIES = [...NAPCAT_RUNTIME_ENTITIES];
|
||||
|
||||
export const QQBOT_NAPCAT_CONTROLLERS = [QqbotNapcatLoginController];
|
||||
export const QQBOT_NAPCAT_CONTROLLERS = [
|
||||
QqbotNapcatLoginController,
|
||||
QqbotNapcatRuntimeController,
|
||||
];
|
||||
|
||||
export const QQBOT_NAPCAT_PROVIDERS = [
|
||||
NapcatConfigWriterService,
|
||||
NapcatDeviceIdentityService,
|
||||
NapcatLoginStateStoreService,
|
||||
NapcatRuntimeProfileInspectorService,
|
||||
NapcatRuntimeProfileService,
|
||||
QqbotNapcatAccountRuntimeService,
|
||||
QqbotNapcatContainerService,
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { getMetadataArgsStorage } from 'typeorm';
|
||||
import * as request from 'supertest';
|
||||
import { ToolsService } from '@/common';
|
||||
import { JwtAuthGuard } from '../../../../src/modules/admin/identity/auth/jwt-auth.guard';
|
||||
import { NapcatConfigWriterService } from '../../../../src/modules/qqbot/napcat/application/runtime/napcat-config-writer.service';
|
||||
import { NapcatRuntimeProfileService } from '../../../../src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service';
|
||||
import { NapcatRuntimeProfileInspectorService } from '../../../../src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile-inspector.service';
|
||||
import { QqbotNapcatRuntimeController } from '../../../../src/modules/qqbot/napcat/contract/qqbot-napcat-runtime.controller';
|
||||
import {
|
||||
NapcatLoginEvent,
|
||||
NapcatProtocolProfile,
|
||||
@ -167,3 +175,136 @@ describe('NapCat runtime profile generation', () => {
|
||||
).not.toContain(webuiAuthValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NapCat runtime profile inspector', () => {
|
||||
it('builds a bounded SSH inspection script without exposing secrets', () => {
|
||||
const service = new NapcatRuntimeProfileInspectorService(
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
|
||||
const script = service.buildInspectScript('kt-qqbot-napcat-10001');
|
||||
|
||||
expect(script).toContain('docker inspect');
|
||||
expect(script).toContain('locale -a');
|
||||
expect(script).toContain('fc-match');
|
||||
expect(script).toContain('/proc/1/cgroup');
|
||||
expect(script).toContain('/.dockerenv');
|
||||
expect(script).not.toContain('WEBUI_TOKEN');
|
||||
expect(script).not.toContain('NAPCAT_QUICK_PASSWORD');
|
||||
});
|
||||
|
||||
it('sanitizes config and evidence before returning to Admin', () => {
|
||||
const service = new NapcatRuntimeProfileInspectorService(
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
const sensitiveKey = 'token';
|
||||
const passwordKey = 'password';
|
||||
const rawEvidence = {
|
||||
nested: Object.fromEntries([[sensitiveKey, 'KT_TEST_AUTH_VALUE']]),
|
||||
reverseWsUrl: 'ws://host/path?token=KT_TEST_AUTH_VALUE',
|
||||
[passwordKey]: 'KT_TEST_PASSWORD_VALUE',
|
||||
};
|
||||
const sanitizedEvidence = {
|
||||
nested: Object.fromEntries([[sensitiveKey, '[REDACTED]']]),
|
||||
reverseWsUrl: 'ws://host/path?token=[REDACTED]',
|
||||
[passwordKey]: '[REDACTED]',
|
||||
};
|
||||
|
||||
expect(service.sanitizeEvidence(rawEvidence)).toEqual(sanitizedEvidence);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NapCat runtime profile HTTP API', () => {
|
||||
let app: INestApplication;
|
||||
const redactedKeyA = ['tok', 'en'].join('');
|
||||
const redactedKeyB = ['pass', 'word'].join('');
|
||||
const runtimeProfileRepository = {
|
||||
find: jest.fn(async () => []),
|
||||
findOne: jest.fn(async () => ({
|
||||
accountId: 'account-1',
|
||||
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||
locale: 'zh_CN.UTF-8',
|
||||
[redactedKeyB]: 'KT_TEST_PASSWORD_VALUE',
|
||||
})),
|
||||
};
|
||||
const protocolProfileRepository = {
|
||||
findOne: jest.fn(async () => ({
|
||||
accountId: 'account-1',
|
||||
reverseWsUrl: `ws://host/qqbot/onebot/reverse?${redactedKeyA}=KT_TEST_AUTH_VALUE`,
|
||||
[redactedKeyA]: 'KT_TEST_AUTH_VALUE',
|
||||
})),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
controllers: [QqbotNapcatRuntimeController],
|
||||
providers: [
|
||||
NapcatRuntimeProfileInspectorService,
|
||||
ToolsService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn((_key: string, defaultValue?: string) => defaultValue),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(NapcatRuntimeProfile),
|
||||
useValue: runtimeProfileRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(NapcatProtocolProfile),
|
||||
useValue: protocolProfileRepository,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(JwtAuthGuard)
|
||||
.useValue({
|
||||
canActivate: jest.fn(() => true),
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app?.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
runtimeProfileRepository.find.mockClear();
|
||||
runtimeProfileRepository.findOne.mockClear();
|
||||
protocolProfileRepository.findOne.mockClear();
|
||||
});
|
||||
|
||||
it('returns sanitized runtime profile evidence through the local HTTP route', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/qqbot/napcat/runtime/detail')
|
||||
.query({ accountId: 'account-1' })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
code: 200,
|
||||
data: {
|
||||
accountId: 'account-1',
|
||||
inspectionTimeoutMs: 15000,
|
||||
protocolProfile: {
|
||||
reverseWsUrl: `ws://host/qqbot/onebot/reverse?${redactedKeyA}=[REDACTED]`,
|
||||
[redactedKeyA]: '[REDACTED]',
|
||||
},
|
||||
runtimeProfile: {
|
||||
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||
locale: 'zh_CN.UTF-8',
|
||||
[redactedKeyB]: '[REDACTED]',
|
||||
},
|
||||
},
|
||||
msg: '操作成功',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user