feat: 生成NapCat运行态与协议Profile
This commit is contained in:
parent
923a383543
commit
5559c318f2
@ -0,0 +1,141 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ToolsService } from '@/common';
|
||||
import { stableJsonHash } from '../../domain/runtime/napcat-config-hash';
|
||||
import type { NapcatConfigFile } from '../../domain/runtime/napcat-profile.types';
|
||||
|
||||
type OnebotReverseWsClientConfig = {
|
||||
debug: false;
|
||||
enable: true;
|
||||
heartInterval: 30000;
|
||||
messagePostFormat: 'array';
|
||||
name: 'kt-template-online-api-reverse';
|
||||
reconnectInterval: 5000;
|
||||
reportSelfMessage: false;
|
||||
token: '';
|
||||
url: string;
|
||||
};
|
||||
|
||||
type OnebotConfig = {
|
||||
enableLocalFile2Url: false;
|
||||
musicSignUrl: '';
|
||||
network: {
|
||||
httpClients: [];
|
||||
httpServers: [];
|
||||
websocketClients: OnebotReverseWsClientConfig[];
|
||||
websocketServers: [];
|
||||
};
|
||||
parseMultMsg: false;
|
||||
};
|
||||
|
||||
type NapcatConfig = {
|
||||
bypass: {
|
||||
container: false;
|
||||
hook: false;
|
||||
js: false;
|
||||
module: false;
|
||||
process: false;
|
||||
window: false;
|
||||
};
|
||||
o3HookMode: 1;
|
||||
packetBackend: 'auto';
|
||||
packetServer: '';
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class NapcatConfigWriterService {
|
||||
/**
|
||||
* Initializes the config writer with shared text helpers for sanitization.
|
||||
* @param toolsService - Shared helper used to trim account and URL values before writing config files.
|
||||
*/
|
||||
constructor(private readonly toolsService: ToolsService) {}
|
||||
|
||||
/**
|
||||
* Builds all NapCat and OneBot config files for one account container.
|
||||
* @param input - Account id, reverse WS URL, and WebUI token used to build runtime config files.
|
||||
* @returns Config file bundle plus sanitized hashes for protocol-profile evidence.
|
||||
*/
|
||||
buildConfigFiles(input: {
|
||||
account?: string;
|
||||
reverseWsUrl: string;
|
||||
token: string;
|
||||
}) {
|
||||
const account = this.toolsService.toTrimmedString(input.account);
|
||||
const webuiConfig = {
|
||||
host: '0.0.0.0',
|
||||
loginRate: 3,
|
||||
port: 6099,
|
||||
token: input.token,
|
||||
};
|
||||
const napcatConfig: NapcatConfig = {
|
||||
bypass: {
|
||||
container: false,
|
||||
hook: false,
|
||||
js: false,
|
||||
module: false,
|
||||
process: false,
|
||||
window: false,
|
||||
},
|
||||
o3HookMode: 1,
|
||||
packetBackend: 'auto',
|
||||
packetServer: '',
|
||||
};
|
||||
const onebotConfig: OnebotConfig = {
|
||||
enableLocalFile2Url: false,
|
||||
musicSignUrl: '',
|
||||
network: {
|
||||
httpClients: [],
|
||||
httpServers: [],
|
||||
websocketClients: [
|
||||
{
|
||||
debug: false,
|
||||
enable: true,
|
||||
heartInterval: 30000,
|
||||
messagePostFormat: 'array',
|
||||
name: 'kt-template-online-api-reverse',
|
||||
reconnectInterval: 5000,
|
||||
reportSelfMessage: false,
|
||||
token: '',
|
||||
url: input.reverseWsUrl,
|
||||
},
|
||||
],
|
||||
websocketServers: [],
|
||||
},
|
||||
parseMultMsg: false,
|
||||
};
|
||||
const files: NapcatConfigFile[] = [
|
||||
{ content: this.stringify(webuiConfig), path: 'webui.json' },
|
||||
{ content: this.stringify(napcatConfig), path: 'napcat.json' },
|
||||
{ content: this.stringify(onebotConfig), path: 'onebot11.json' },
|
||||
];
|
||||
|
||||
if (account) {
|
||||
files.push(
|
||||
{
|
||||
content: this.stringify(napcatConfig),
|
||||
path: `napcat_${account}.json`,
|
||||
},
|
||||
{
|
||||
content: this.stringify(onebotConfig),
|
||||
path: `onebot11_${account}.json`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
napcatConfig,
|
||||
napcatConfigHash: stableJsonHash(napcatConfig),
|
||||
onebotConfig,
|
||||
onebotConfigHash: stableJsonHash(onebotConfig),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes config JSON with stable indentation for script and hash tests.
|
||||
* @param value - Config object that will be written to `/app/napcat/config`.
|
||||
* @returns Pretty JSON content with trailing newline for here-doc output.
|
||||
*/
|
||||
private stringify(value: Record<string, unknown>) {
|
||||
return `${JSON.stringify(value, null, 2)}\n`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { NapcatRuntimeProfileSnapshot } from '../../domain/runtime/napcat-profile.types';
|
||||
|
||||
@Injectable()
|
||||
export class NapcatRuntimeProfileService {
|
||||
/**
|
||||
* Initializes the profile resolver used before Docker script generation.
|
||||
* @param configService - Nest config provider that supplies image ref, UID/GID, shm size, and profile version.
|
||||
*/
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* Resolves the runtime profile for an account-owned NapCat container.
|
||||
* @param input - Account, container, data directory, and device identity ids that tie generated profile evidence to persistence.
|
||||
* @returns Runtime profile snapshot used by Docker script generation and later persistence.
|
||||
*/
|
||||
resolveRuntimeProfile(input: {
|
||||
accountId: string;
|
||||
containerId?: string;
|
||||
dataDir: string;
|
||||
deviceIdentityId?: string;
|
||||
}): NapcatRuntimeProfileSnapshot {
|
||||
return {
|
||||
accountId: input.accountId,
|
||||
containerId: input.containerId,
|
||||
dataDir: input.dataDir,
|
||||
desktopProfileVersion: this.getString(
|
||||
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION',
|
||||
'desktop-cn-v1',
|
||||
),
|
||||
deviceIdentityId: input.deviceIdentityId,
|
||||
imageRef: this.getString('QQBOT_NAPCAT_IMAGE', ''),
|
||||
locale: 'zh_CN.UTF-8',
|
||||
persistCache: true,
|
||||
persistLocalShare: true,
|
||||
persistLogs: true,
|
||||
runtimeGid: this.getNumber('QQBOT_NAPCAT_RUNTIME_GID', 1101),
|
||||
runtimeUid: this.getNumber('QQBOT_NAPCAT_RUNTIME_UID', 1101),
|
||||
shmSize: this.getString('QQBOT_NAPCAT_SHM_SIZE', '512m'),
|
||||
timezone: 'Asia/Shanghai',
|
||||
xdgCacheHome: '/app/.cache',
|
||||
xdgConfigHome: '/app/.config',
|
||||
xdgDataHome: '/app/.local/share',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a trimmed string config value for profile generation.
|
||||
* @param key - Environment key that controls NapCat runtime profile generation.
|
||||
* @param defaultValue - Value used when the key is absent from runtime config.
|
||||
* @returns Trimmed string value consumed by Docker script generation.
|
||||
*/
|
||||
private getString(key: string, defaultValue: string) {
|
||||
return `${this.configService.get<string>(key) || defaultValue}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a positive numeric config value for UID/GID profile fields.
|
||||
* @param key - Environment key that should contain a numeric UID/GID value.
|
||||
* @param defaultValue - Safe non-root fallback for profile generation.
|
||||
* @returns Positive integer used as the container runtime UID/GID.
|
||||
*/
|
||||
private getNumber(key: string, defaultValue: number) {
|
||||
const value = Number(this.configService.get<string>(key) || defaultValue);
|
||||
return Number.isFinite(value) && value > 0 ? value : defaultValue;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
* Produces a deterministic JSON hash for NapCat and OneBot config snapshots.
|
||||
* @param value - Config value that may contain nested plain objects or arrays.
|
||||
* @returns SHA-256 digest of the stable JSON representation.
|
||||
*/
|
||||
export function stableJsonHash(value: unknown) {
|
||||
return createHash('sha256').update(stableStringify(value)).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes config objects with stable object-key ordering.
|
||||
* @param value - JSON-compatible value written to NapCat config evidence.
|
||||
* @returns JSON string whose object key order is deterministic.
|
||||
*/
|
||||
function stableStringify(value: unknown): string {
|
||||
return JSON.stringify(sortJsonValue(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sorts plain-object keys while preserving array order.
|
||||
* @param value - JSON-compatible value to normalize before hashing.
|
||||
* @returns Normalized value with deterministic object key order.
|
||||
*/
|
||||
function sortJsonValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sortJsonValue(item));
|
||||
}
|
||||
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return Object.keys(value as Record<string, unknown>)
|
||||
.sort()
|
||||
.reduce<Record<string, unknown>>((result, key) => {
|
||||
result[key] = sortJsonValue((value as Record<string, unknown>)[key]);
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
export type NapcatRuntimeProfileSnapshot = {
|
||||
accountId: string;
|
||||
containerId?: string;
|
||||
dataDir: string;
|
||||
desktopProfileVersion: string;
|
||||
deviceIdentityId?: string;
|
||||
imageRef: string;
|
||||
locale: 'zh_CN.UTF-8';
|
||||
persistCache: true;
|
||||
persistLocalShare: true;
|
||||
persistLogs: true;
|
||||
runtimeGid: number;
|
||||
runtimeUid: number;
|
||||
shmSize: string;
|
||||
timezone: 'Asia/Shanghai';
|
||||
xdgCacheHome: '/app/.cache';
|
||||
xdgConfigHome: '/app/.config';
|
||||
xdgDataHome: '/app/.local/share';
|
||||
};
|
||||
|
||||
export type NapcatProtocolProfileSnapshot = {
|
||||
o3HookGrayEnabled: boolean;
|
||||
o3HookMode: 0 | 1;
|
||||
onebotConfigHash: string;
|
||||
packetBackend: 'auto';
|
||||
packetServer: '';
|
||||
};
|
||||
|
||||
export type NapcatConfigFile = {
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
@ -3,6 +3,7 @@ import type { NapcatDeviceIdentity } from '../../persistence/napcat-device-ident
|
||||
export type NapcatDockerDeviceOptions = {
|
||||
dataDir: string;
|
||||
deviceEnvPath: string;
|
||||
deviceIdentityId?: string;
|
||||
hostname: string;
|
||||
machineIdPath: string;
|
||||
macAddress: string;
|
||||
@ -11,18 +12,19 @@ export type NapcatDockerDeviceOptions = {
|
||||
|
||||
/**
|
||||
* 执行 NapCat 登录运行态流程。
|
||||
* @param identity - identity 输入;使用 `dataDir`、`hostname`、`machineIdPath`、`macAddress` 字段生成结果。
|
||||
* @returns NapCat 登录运行态产出的 NapcatDockerDeviceOptions。
|
||||
* @param identity - Persisted device identity row that supplies stable directory, hostname, machine-id, and MAC values for Docker.
|
||||
* @returns Docker option bundle used by remote create scripts.
|
||||
*/
|
||||
export function toNapcatDockerDeviceOptions(
|
||||
identity: Pick<
|
||||
NapcatDeviceIdentity,
|
||||
'dataDir' | 'hostname' | 'machineIdPath' | 'macAddress'
|
||||
'dataDir' | 'hostname' | 'id' | 'machineIdPath' | 'macAddress'
|
||||
>,
|
||||
): NapcatDockerDeviceOptions {
|
||||
return {
|
||||
dataDir: identity.dataDir,
|
||||
deviceEnvPath: `${identity.dataDir}/device.env`,
|
||||
deviceIdentityId: identity.id,
|
||||
hostname: identity.hostname,
|
||||
machineIdPath: identity.machineIdPath,
|
||||
macAddress: identity.macAddress,
|
||||
|
||||
@ -7,6 +7,9 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { throwVbenError, ToolsService } from '@/common';
|
||||
import { NapcatConfigWriterService } from '../../../application/runtime/napcat-config-writer.service';
|
||||
import { NapcatRuntimeProfileService } from '../../../application/runtime/napcat-runtime-profile.service';
|
||||
import type { NapcatConfigFile } from '../../../domain/runtime/napcat-profile.types';
|
||||
import {
|
||||
toNapcatDockerDeviceOptions,
|
||||
type NapcatDockerDeviceOptions,
|
||||
@ -50,13 +53,19 @@ type NapcatAutoLoginResult = {
|
||||
|
||||
@Injectable()
|
||||
export class QqbotNapcatContainerService {
|
||||
private readonly configWriterService: NapcatConfigWriterService;
|
||||
|
||||
private readonly runtimeProfileService: NapcatRuntimeProfileService;
|
||||
|
||||
/**
|
||||
* 初始化 QqbotNapcatContainerService 实例。
|
||||
* @param configService - Nest ConfigService 依赖;影响 constructor 的返回值。
|
||||
* @param containerRepository - NapCat仓库依赖;影响 constructor 的返回值。
|
||||
* @param bindingRepository - NapCat仓库依赖;影响 constructor 的返回值。
|
||||
* @param toolsService - ToolsService 依赖;影响 constructor 的返回值。
|
||||
* @param deviceIdentityService - deviceIdentityService 服务依赖;影响 constructor 的返回值。
|
||||
* @param configService - Runtime configuration source for NAS SSH, Docker image, port pool, and profile defaults.
|
||||
* @param containerRepository - Persistence adapter for NapCat container rows created or updated by runtime actions.
|
||||
* @param bindingRepository - Persistence adapter that links QQBot accounts to their primary NapCat containers.
|
||||
* @param toolsService - Shared helper for error extraction, text truncation, and bounded sleeps.
|
||||
* @param deviceIdentityService - Device identity resolver that supplies stable hostname, MAC, data-dir, and machine-id values.
|
||||
* @param runtimeProfileService - Runtime profile resolver used to generate Docker env and mount settings.
|
||||
* @param configWriterService - Config writer used to generate NapCat and OneBot config files.
|
||||
*/
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@ -66,7 +75,14 @@ export class QqbotNapcatContainerService {
|
||||
private readonly bindingRepository: Repository<NapcatAccountBinding>,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly deviceIdentityService?: NapcatDeviceIdentityService,
|
||||
) {}
|
||||
runtimeProfileService?: NapcatRuntimeProfileService,
|
||||
configWriterService?: NapcatConfigWriterService,
|
||||
) {
|
||||
this.runtimeProfileService =
|
||||
runtimeProfileService || new NapcatRuntimeProfileService(configService);
|
||||
this.configWriterService =
|
||||
configWriterService || new NapcatConfigWriterService(toolsService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 NapCat 登录运行态流程。
|
||||
@ -1159,11 +1175,12 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 NapCat 登录运行态对象或配置。
|
||||
* @param input - input 输入;使用 `dataDir`、`image`、`name`、`reverseWsUrl` 字段生成结果。
|
||||
* Builds the remote shell script that creates or recreates a managed NapCat container.
|
||||
* @param input - Container image, account, data-dir, device identity, and reverse-WS values that become Docker flags and config files.
|
||||
*/
|
||||
private buildRemoteCreateScript(input: {
|
||||
account?: string;
|
||||
containerId?: string;
|
||||
dataDir: string;
|
||||
deviceIdentity?: NapcatDockerDeviceOptions;
|
||||
image: string;
|
||||
@ -1181,6 +1198,18 @@ docker logs --since "$SINCE" --tail 300 "$NAME" 2>&1 || true
|
||||
const token = this.sh(input.token);
|
||||
const account = `${input.account || ''}`.trim();
|
||||
const loginPassword = this.toolsService.toSecretText(input.loginPassword);
|
||||
const runtimeProfile = this.runtimeProfileService.resolveRuntimeProfile({
|
||||
accountId: account || input.name,
|
||||
containerId: input.containerId,
|
||||
dataDir: input.dataDir,
|
||||
deviceIdentityId: input.deviceIdentity?.deviceIdentityId,
|
||||
});
|
||||
const configBundle = this.configWriterService.buildConfigFiles({
|
||||
account,
|
||||
reverseWsUrl: '$REVERSE_WS_URL',
|
||||
token: '$WEBUI_TOKEN',
|
||||
});
|
||||
const configWriteScript = this.renderConfigFiles(configBundle.files);
|
||||
const accountHeader = account ? `ACCOUNT=${this.sh(account)}\n` : '';
|
||||
const accountRunFlag = account ? ' -e ACCOUNT="$ACCOUNT" \\\n' : '';
|
||||
const passwordHeader = loginPassword
|
||||
@ -1222,63 +1251,61 @@ NAME=${name}
|
||||
PORT=${input.port}
|
||||
REVERSE_WS_URL=${reverseWsUrl}
|
||||
WEBUI_TOKEN=${token}
|
||||
NAPCAT_UID=${this.sh(`${runtimeProfile.runtimeUid}`)}
|
||||
NAPCAT_GID=${this.sh(`${runtimeProfile.runtimeGid}`)}
|
||||
NAPCAT_SHM_SIZE=${this.sh(runtimeProfile.shmSize)}
|
||||
${accountHeader}
|
||||
${passwordHeader}
|
||||
${deviceHeader}
|
||||
mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs"
|
||||
mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs" "$DATA_DIR/cache" "$DATA_DIR/local-share"
|
||||
chmod 700 "$DATA_DIR"
|
||||
${devicePrepareScript}
|
||||
|
||||
cat > "$DATA_DIR/config/webui.json" <<EOF
|
||||
{
|
||||
"host": "0.0.0.0",
|
||||
"port": 6099,
|
||||
"token": "$WEBUI_TOKEN",
|
||||
"loginRate": 3
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$DATA_DIR/config/onebot11.json" <<EOF
|
||||
{
|
||||
"network": {
|
||||
"httpServers": [],
|
||||
"httpClients": [],
|
||||
"websocketServers": [],
|
||||
"websocketClients": [
|
||||
{
|
||||
"name": "kt-template-online-api-reverse",
|
||||
"enable": true,
|
||||
"url": "$REVERSE_WS_URL",
|
||||
"messagePostFormat": "array",
|
||||
"reportSelfMessage": false,
|
||||
"reconnectInterval": 5000,
|
||||
"token": "",
|
||||
"debug": false,
|
||||
"heartInterval": 30000
|
||||
}
|
||||
]
|
||||
},
|
||||
"musicSignUrl": "",
|
||||
"enableLocalFile2Url": false,
|
||||
"parseMultMsg": false
|
||||
}
|
||||
EOF
|
||||
${configWriteScript}
|
||||
|
||||
${pullCmd}docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||
docker run -d \\
|
||||
--name "$NAME" \\
|
||||
--restart unless-stopped \\
|
||||
-e NAPCAT_UID=0 \\
|
||||
-e NAPCAT_GID=0 \\
|
||||
--init \\
|
||||
--shm-size "$NAPCAT_SHM_SIZE" \\
|
||||
-e NAPCAT_UID="$NAPCAT_UID" \\
|
||||
-e NAPCAT_GID="$NAPCAT_GID" \\
|
||||
-e WEBUI_TOKEN="$WEBUI_TOKEN" \\
|
||||
-e LANG=${runtimeProfile.locale} \\
|
||||
-e LC_ALL=${runtimeProfile.locale} \\
|
||||
-e LANGUAGE=zh_CN:zh \\
|
||||
-e TZ=${runtimeProfile.timezone} \\
|
||||
-e HOME=/app \\
|
||||
-e XDG_CONFIG_HOME=${runtimeProfile.xdgConfigHome} \\
|
||||
-e XDG_CACHE_HOME=${runtimeProfile.xdgCacheHome} \\
|
||||
-e XDG_DATA_HOME=${runtimeProfile.xdgDataHome} \\
|
||||
-e XDG_RUNTIME_DIR=/tmp/runtime-napcat \\
|
||||
${accountRunFlag}${passwordRunFlag}${deviceRunFlags} -p "$PORT:6099" \\
|
||||
-v "$DATA_DIR/QQ:/app/.config/QQ" \\
|
||||
-v "$DATA_DIR/config:/app/napcat/config" \\
|
||||
-v "$DATA_DIR/plugins:/app/napcat/plugins" \\
|
||||
-v "$DATA_DIR/cache:/app/.cache" \\
|
||||
-v "$DATA_DIR/local-share:/app/.local/share" \\
|
||||
-v "$DATA_DIR/logs:/app/napcat/logs" \\
|
||||
"$IMAGE" >/dev/null
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders NapCat config files as shell here-doc writes under the account config directory.
|
||||
* @param files - Config files generated by `NapcatConfigWriterService` for this container.
|
||||
* @returns Shell fragment that writes each config file before Docker starts.
|
||||
*/
|
||||
private renderConfigFiles(files: NapcatConfigFile[]) {
|
||||
return files
|
||||
.map((file) => {
|
||||
return `cat > "$DATA_DIR/config/${file.path}" <<EOF
|
||||
${file.content}EOF`;
|
||||
})
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 NapCat 登录运行态流程。
|
||||
*/
|
||||
|
||||
@ -7,6 +7,8 @@ import { QqbotCoreModule } from '@/modules/qqbot/core/qqbot-core.module';
|
||||
import { QqbotNapcatAccountRuntimeService } from './application/account-runtime/qqbot-napcat-account-runtime.service';
|
||||
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 { NapcatRuntimeProfileService } from './application/runtime/napcat-runtime-profile.service';
|
||||
import { QqbotNapcatLoginController } from './contract/qqbot-napcat-login.controller';
|
||||
import { QqbotNapcatContainerService } from './infrastructure/integration/container/qqbot-napcat-container.service';
|
||||
import { NapcatDeviceIdentityService } from './infrastructure/integration/device/napcat-device-identity.service';
|
||||
@ -20,8 +22,10 @@ export const QQBOT_NAPCAT_ENTITIES = [...NAPCAT_RUNTIME_ENTITIES];
|
||||
export const QQBOT_NAPCAT_CONTROLLERS = [QqbotNapcatLoginController];
|
||||
|
||||
export const QQBOT_NAPCAT_PROVIDERS = [
|
||||
NapcatConfigWriterService,
|
||||
NapcatDeviceIdentityService,
|
||||
NapcatLoginStateStoreService,
|
||||
NapcatRuntimeProfileService,
|
||||
QqbotNapcatAccountRuntimeService,
|
||||
QqbotNapcatContainerService,
|
||||
QqbotNapcatLoginService,
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { getMetadataArgsStorage } from 'typeorm';
|
||||
import { ToolsService } from '@/common';
|
||||
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 {
|
||||
NapcatLoginEvent,
|
||||
NapcatProtocolProfile,
|
||||
@ -88,3 +91,79 @@ describe('NapCat runtime and protocol profile persistence', () => {
|
||||
expect(riskColumns.join(' ')).not.toMatch(/daily|hour|budget|quota/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NapCat runtime profile generation', () => {
|
||||
it('resolves Chinese Desktop Runtime defaults without C.UTF-8 fallback', () => {
|
||||
const service = new NapcatRuntimeProfileService({
|
||||
get: jest.fn((key: string, defaultValue?: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_IMAGE: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||
QQBOT_NAPCAT_RUNTIME_GID: '1101',
|
||||
QQBOT_NAPCAT_RUNTIME_UID: '1101',
|
||||
QQBOT_NAPCAT_SHM_SIZE: '512m',
|
||||
};
|
||||
return values[key] || defaultValue || '';
|
||||
}),
|
||||
} as any);
|
||||
|
||||
const profile = service.resolveRuntimeProfile({
|
||||
accountId: 'account-1',
|
||||
containerId: 'container-1',
|
||||
dataDir: '/vol1/docker/kt-qqbot/napcat-instances/linux-pc-a1b2',
|
||||
deviceIdentityId: 'identity-1',
|
||||
});
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||
locale: 'zh_CN.UTF-8',
|
||||
runtimeGid: 1101,
|
||||
runtimeUid: 1101,
|
||||
shmSize: '512m',
|
||||
xdgCacheHome: '/app/.cache',
|
||||
xdgConfigHome: '/app/.config',
|
||||
xdgDataHome: '/app/.local/share',
|
||||
});
|
||||
expect(profile.locale).not.toBe('C.UTF-8');
|
||||
});
|
||||
|
||||
it('writes account-level NapCat and OneBot configs with minimal reverse WS only', () => {
|
||||
const writer = new NapcatConfigWriterService(new ToolsService());
|
||||
const webuiAuthValue = 'KT_TEST_WEBUI_AUTH_VALUE';
|
||||
const result = writer.buildConfigFiles({
|
||||
account: '10001',
|
||||
reverseWsUrl: 'ws://127.0.0.1:48085/qqbot/onebot/reverse',
|
||||
token: webuiAuthValue,
|
||||
});
|
||||
|
||||
expect(result.files.map((file) => file.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'webui.json',
|
||||
'napcat.json',
|
||||
'napcat_10001.json',
|
||||
'onebot11.json',
|
||||
'onebot11_10001.json',
|
||||
]),
|
||||
);
|
||||
expect(result.onebotConfig.network.websocketClients).toHaveLength(1);
|
||||
expect(result.onebotConfig.network.httpServers).toEqual([]);
|
||||
expect(result.onebotConfig.network.websocketServers).toEqual([]);
|
||||
expect(result.onebotConfig.network.websocketClients[0]).toMatchObject({
|
||||
debug: false,
|
||||
enable: true,
|
||||
heartInterval: 30000,
|
||||
messagePostFormat: 'array',
|
||||
reconnectInterval: 5000,
|
||||
reportSelfMessage: false,
|
||||
});
|
||||
expect(
|
||||
result.files.find((file) => file.path === 'webui.json')?.content,
|
||||
).toContain(webuiAuthValue);
|
||||
expect(
|
||||
JSON.stringify({
|
||||
napcatConfigHash: result.napcatConfigHash,
|
||||
onebotConfig: result.onebotConfig,
|
||||
onebotConfigHash: result.onebotConfigHash,
|
||||
}),
|
||||
).not.toContain(webuiAuthValue);
|
||||
});
|
||||
});
|
||||
|
||||
@ -342,6 +342,62 @@ describe('QqbotNapcatContainerService', () => {
|
||||
).not.toContain('docker pull');
|
||||
});
|
||||
|
||||
it('generates Chinese desktop runtime flags and account config files', () => {
|
||||
const service = new QqbotNapcatContainerService(
|
||||
{
|
||||
get: jest.fn((key: string, defaultValue?: string) => {
|
||||
const values: Record<string, string> = {
|
||||
QQBOT_NAPCAT_IMAGE: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||
QQBOT_NAPCAT_RUNTIME_GID: '1101',
|
||||
QQBOT_NAPCAT_RUNTIME_UID: '1101',
|
||||
QQBOT_NAPCAT_SHM_SIZE: '512m',
|
||||
};
|
||||
return values[key] || defaultValue || '';
|
||||
}),
|
||||
} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
new ToolsService(),
|
||||
) as any;
|
||||
|
||||
const script = service.buildRemoteCreateScript({
|
||||
account: '10001',
|
||||
dataDir: '/vol1/docker/kt-qqbot/napcat-instances/linux-pc-a1b2',
|
||||
image: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||
name: 'linux-pc-a1b2',
|
||||
port: 6100,
|
||||
reverseWsUrl: 'ws://127.0.0.1:48085/qqbot/onebot/reverse',
|
||||
token: 'token-test',
|
||||
});
|
||||
|
||||
expect(script).toContain(
|
||||
'mkdir -p "$DATA_DIR/QQ" "$DATA_DIR/config" "$DATA_DIR/plugins" "$DATA_DIR/logs" "$DATA_DIR/cache" "$DATA_DIR/local-share"',
|
||||
);
|
||||
expect(script).toContain("NAPCAT_UID='1101'");
|
||||
expect(script).toContain("NAPCAT_GID='1101'");
|
||||
expect(script).toContain("NAPCAT_SHM_SIZE='512m'");
|
||||
expect(script).toContain('--init \\');
|
||||
expect(script).toContain('--shm-size "$NAPCAT_SHM_SIZE"');
|
||||
expect(script).toContain('-e NAPCAT_UID="$NAPCAT_UID"');
|
||||
expect(script).toContain('-e NAPCAT_GID="$NAPCAT_GID"');
|
||||
expect(script).toContain('-e LANG=zh_CN.UTF-8');
|
||||
expect(script).toContain('-e LC_ALL=zh_CN.UTF-8');
|
||||
expect(script).toContain('-e LANGUAGE=zh_CN:zh');
|
||||
expect(script).toContain('-e TZ=Asia/Shanghai');
|
||||
expect(script).toContain('-e HOME=/app');
|
||||
expect(script).toContain('-e XDG_CONFIG_HOME=/app/.config');
|
||||
expect(script).toContain('-e XDG_CACHE_HOME=/app/.cache');
|
||||
expect(script).toContain('-e XDG_DATA_HOME=/app/.local/share');
|
||||
expect(script).toContain('-e XDG_RUNTIME_DIR=/tmp/runtime-napcat');
|
||||
expect(script).toContain('-v "$DATA_DIR/cache:/app/.cache"');
|
||||
expect(script).toContain(
|
||||
'-v "$DATA_DIR/local-share:/app/.local/share"',
|
||||
);
|
||||
expect(script).toContain('-v "$DATA_DIR/logs:/app/napcat/logs"');
|
||||
expect(script).toContain('cat > "$DATA_DIR/config/napcat_10001.json"');
|
||||
expect(script).toContain('cat > "$DATA_DIR/config/onebot11_10001.json"');
|
||||
});
|
||||
|
||||
it('skips quick-login recreate when the container already carries ACCOUNT', async () => {
|
||||
const containerRepository = {
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user