mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-28 07:40:27 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
285d352bc8 | ||
|
|
a3b3836b8a | ||
|
|
b9f61cc0ee | ||
|
|
9998207346 | ||
|
|
4f47af233f | ||
|
|
6aadc2402d | ||
|
|
eb937b29e4 | ||
|
|
f44aca9a2f | ||
|
|
c34812bc9c | ||
|
|
d93b430034 | ||
|
|
c91e1378cf | ||
|
|
cad567dc3f | ||
|
|
82c8de00d0 | ||
|
|
f17abccfdc | ||
|
|
35af50bb73 | ||
|
|
5c72f771c3 | ||
|
|
62c9246368 | ||
|
|
d622178b25 | ||
|
|
9887eb8565 | ||
|
|
2f8569f30c |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -10,7 +10,7 @@ permissions: write-all
|
||||
|
||||
env:
|
||||
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
|
||||
OPENROUTER_MODEL: "gemini-3-flash-preview"
|
||||
OPENROUTER_MODEL: "deepseek-v3.2-chat"
|
||||
RELEASE_NAME: "NapCat"
|
||||
|
||||
jobs:
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
--arg system "$SYSTEM_PROMPT" \
|
||||
--arg user "$USER_CONTENT" \
|
||||
--arg model "$OPENROUTER_MODEL" \
|
||||
'{model: $model, messages:[{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:1500}')
|
||||
'{model: $model, messages:[{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:5000}')
|
||||
|
||||
echo "=== OpenRouter request body ==="
|
||||
echo "$BODY" | jq .
|
||||
|
||||
24
packages/napcat-core/external/napcat.json
vendored
24
packages/napcat-core/external/napcat.json
vendored
@@ -1,9 +1,17 @@
|
||||
{
|
||||
"fileLog": false,
|
||||
"consoleLog": true,
|
||||
"fileLogLevel": "debug",
|
||||
"consoleLogLevel": "info",
|
||||
"packetBackend": "auto",
|
||||
"packetServer": "",
|
||||
"o3HookMode": 1
|
||||
}
|
||||
"fileLog": false,
|
||||
"consoleLog": true,
|
||||
"fileLogLevel": "debug",
|
||||
"consoleLogLevel": "info",
|
||||
"packetBackend": "auto",
|
||||
"packetServer": "",
|
||||
"o3HookMode": 1,
|
||||
"bypass": {
|
||||
"hook": true,
|
||||
"window": true,
|
||||
"module": true,
|
||||
"process": true,
|
||||
"container": true,
|
||||
"js": true
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,15 @@ import { NapCatCore } from '@/napcat-core/index';
|
||||
import { Type, Static } from '@sinclair/typebox';
|
||||
import { AnySchema } from 'ajv';
|
||||
|
||||
export const BypassOptionsSchema = Type.Object({
|
||||
hook: Type.Boolean({ default: true }),
|
||||
window: Type.Boolean({ default: true }),
|
||||
module: Type.Boolean({ default: true }),
|
||||
process: Type.Boolean({ default: true }),
|
||||
container: Type.Boolean({ default: true }),
|
||||
js: Type.Boolean({ default: true }),
|
||||
});
|
||||
|
||||
export const NapcatConfigSchema = Type.Object({
|
||||
fileLog: Type.Boolean({ default: false }),
|
||||
consoleLog: Type.Boolean({ default: true }),
|
||||
@@ -11,6 +20,7 @@ export const NapcatConfigSchema = Type.Object({
|
||||
packetBackend: Type.String({ default: 'auto' }),
|
||||
packetServer: Type.String({ default: '' }),
|
||||
o3HookMode: Type.Number({ default: 0 }),
|
||||
bypass: Type.Optional(BypassOptionsSchema),
|
||||
});
|
||||
|
||||
export type NapcatConfig = Static<typeof NapcatConfigSchema>;
|
||||
|
||||
@@ -33,6 +33,7 @@ import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-cor
|
||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||
import { NTQQPacketApi } from './apis/packet';
|
||||
import { NativePacketHandler } from './packet/handler/client';
|
||||
import { Napi2NativeLoader } from './packet/handler/napi2nativeLoader';
|
||||
import { container, ReceiverServiceRegistry } from './packet/handler/serviceRegister';
|
||||
import { appEvent } from './packet/handler/eventList';
|
||||
import { TypedEventEmitter } from './packet/handler/typeEvent';
|
||||
@@ -314,6 +315,7 @@ export interface InstanceContext {
|
||||
readonly basicInfoWrapper: QQBasicInfoWrapper;
|
||||
readonly pathWrapper: NapCatPathWrapper;
|
||||
readonly packetHandler: NativePacketHandler;
|
||||
readonly napi2nativeLoader: Napi2NativeLoader;
|
||||
}
|
||||
|
||||
export interface StableNTApiWrapper {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import { constants } from 'node:os';
|
||||
import { LogStack } from '@/napcat-core/packet/context/clientContext';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
||||
import { OidbPacket, PacketBuf } from '@/napcat-core/packet/transformer/base';
|
||||
import { Napi2NativeLoader } from '@/napcat-core/packet/handler/napi2nativeLoader';
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
data: RecvPacketData;
|
||||
@@ -17,48 +14,36 @@ export interface RecvPacketData {
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
// 0 send 1 recv
|
||||
export interface NativePacketExportType {
|
||||
initHook?: (send: string, recv: string) => boolean;
|
||||
}
|
||||
|
||||
export class NativePacketClient {
|
||||
protected readonly napcore: NapCoreContext;
|
||||
protected readonly logger: PacketLogger;
|
||||
protected readonly cb = new Map<string, (json: RecvPacketData) => Promise<any> | any>(); // hash-type callback
|
||||
protected readonly napi2nativeLoader: Napi2NativeLoader;
|
||||
logStack: LogStack;
|
||||
available: boolean = false;
|
||||
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64'];
|
||||
private readonly MoeHooExport: { exports: NativePacketExportType; } = { exports: {} };
|
||||
|
||||
constructor (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
constructor (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack, napi2nativeLoader: Napi2NativeLoader) {
|
||||
this.napcore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = logStack;
|
||||
this.napi2nativeLoader = napi2nativeLoader;
|
||||
}
|
||||
|
||||
check (): boolean {
|
||||
const platform = process.platform + '.' + process.arch;
|
||||
if (!this.supportedPlatforms.includes(platform)) {
|
||||
this.logStack.pushLogWarn(`NativePacketClient: 不支持的平台: ${platform}`);
|
||||
return false;
|
||||
}
|
||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/napi2native/napi2native.' + platform + '.node');
|
||||
if (!fs.existsSync(moehoo_path)) {
|
||||
this.logStack.pushLogWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`);
|
||||
if (!this.napi2nativeLoader.loaded) {
|
||||
this.logStack.pushLogWarn('NativePacketClient: Napi2NativeLoader 未成功加载');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async init (_pid: number, recv: string, send: string): Promise<void> {
|
||||
const platform = process.platform + '.' + process.arch;
|
||||
const isNewQQ = this.napcore.basicInfo.requireMinNTQQBuild('40824');
|
||||
if (isNewQQ) {
|
||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/napi2native/napi2native.' + platform + '.node');
|
||||
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
|
||||
this.MoeHooExport?.exports.initHook?.(send, recv);
|
||||
this.available = true;
|
||||
const success = this.napi2nativeLoader.initHook(send, recv);
|
||||
if (success) {
|
||||
this.available = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NativePacketClient } from '@/napcat-core/packet/client/nativeClient';
|
||||
import { OidbPacket } from '@/napcat-core/packet/transformer/base';
|
||||
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
||||
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
||||
import { Napi2NativeLoader } from '@/napcat-core/packet/handler/napi2nativeLoader';
|
||||
|
||||
export class LogStack {
|
||||
private stack: string[] = [];
|
||||
@@ -43,12 +44,14 @@ export class PacketClientContext {
|
||||
private readonly napCore: NapCoreContext;
|
||||
private readonly logger: PacketLogger;
|
||||
private readonly logStack: LogStack;
|
||||
private readonly napi2nativeLoader: Napi2NativeLoader;
|
||||
private readonly _client: NativePacketClient;
|
||||
|
||||
constructor (napCore: NapCoreContext, logger: PacketLogger) {
|
||||
constructor (napCore: NapCoreContext, logger: PacketLogger, napi2nativeLoader: Napi2NativeLoader) {
|
||||
this.napCore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = new LogStack(logger);
|
||||
this.napi2nativeLoader = napi2nativeLoader;
|
||||
this._client = this.newClient();
|
||||
}
|
||||
|
||||
@@ -64,14 +67,14 @@ export class PacketClientContext {
|
||||
await this._client.init(pid, recv, send);
|
||||
}
|
||||
|
||||
async sendOidbPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T, timeout?: number): Promise<T extends true ? Buffer : void> {
|
||||
async sendOidbPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T, timeout?: number): Promise<T extends true ? Buffer : void> {
|
||||
const raw = await this._client.sendOidbPacket(pkt, rsp, timeout);
|
||||
return raw.data as T extends true ? Buffer : void;
|
||||
}
|
||||
|
||||
private newClient (): NativePacketClient {
|
||||
this.logger.info('使用 NativePacketClient 作为后端');
|
||||
const client = new NativePacketClient(this.napCore, this.logger, this.logStack);
|
||||
const client = new NativePacketClient(this.napCore, this.logger, this.logStack, this.napi2nativeLoader);
|
||||
if (!client.check()) {
|
||||
throw new Error('[Core] [Packet] NativePacketClient 不可用,NapCat.Packet将不会加载!');
|
||||
}
|
||||
|
||||
@@ -34,5 +34,9 @@ export class NapCoreContext {
|
||||
return this.core.configLoader.configData;
|
||||
}
|
||||
|
||||
get napi2nativeLoader () {
|
||||
return this.core.context.napi2nativeLoader;
|
||||
}
|
||||
|
||||
sendSsoCmdReqByContend = (cmd: string, data: Buffer) => this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, data);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class PacketContext {
|
||||
this.msgConverter = new PacketMsgConverter();
|
||||
this.napcore = new NapCoreContext(core);
|
||||
this.logger = new PacketLogger(this.napcore);
|
||||
this.client = new PacketClientContext(this.napcore, this.logger);
|
||||
this.client = new PacketClientContext(this.napcore, this.logger, this.napcore.napi2nativeLoader);
|
||||
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
|
||||
this.operation = new PacketOperationContext(this);
|
||||
}
|
||||
|
||||
90
packages/napcat-core/packet/handler/napi2nativeLoader.ts
Normal file
90
packages/napcat-core/packet/handler/napi2nativeLoader.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import { constants } from 'node:os';
|
||||
import { LogWrapper } from '../../helper/log';
|
||||
|
||||
export interface BypassOptions {
|
||||
hook?: boolean;
|
||||
window?: boolean;
|
||||
module?: boolean;
|
||||
process?: boolean;
|
||||
container?: boolean;
|
||||
js?: boolean;
|
||||
}
|
||||
|
||||
export interface Napi2NativeExportType {
|
||||
initHook?: (send: string, recv: string) => boolean;
|
||||
setVerbose?: (verbose: boolean) => void; // 默认关闭日志
|
||||
enableAllBypasses?: (options?: BypassOptions) => boolean;
|
||||
}
|
||||
|
||||
export class Napi2NativeLoader {
|
||||
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64'];
|
||||
private readonly exports: { exports: Napi2NativeExportType; } = { exports: {} };
|
||||
protected readonly logger: LogWrapper;
|
||||
private _loaded: boolean = false;
|
||||
|
||||
constructor ({ logger }: { logger: LogWrapper; }) {
|
||||
this.logger = logger;
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load (): void {
|
||||
const platform = process.platform + '.' + process.arch;
|
||||
|
||||
if (!this.supportedPlatforms.includes(platform)) {
|
||||
this.logger.logWarn(`Napi2NativeLoader: 不支持的平台: ${platform}`);
|
||||
this._loaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeModulePath = path.join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
'./native/napi2native/napi2native.' + platform + '.node'
|
||||
);
|
||||
|
||||
if (!fs.existsSync(nativeModulePath)) {
|
||||
this.logger.logWarn(`Napi2NativeLoader: 缺失运行时文件: ${nativeModulePath}`);
|
||||
this._loaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
process.dlopen(this.exports, nativeModulePath, constants.dlopen.RTLD_LAZY);
|
||||
this._loaded = true;
|
||||
this.logger.log('[Napi2NativeLoader] 加载成功');
|
||||
} catch (error) {
|
||||
this.logger.logError('Napi2NativeLoader 加载出错:', error);
|
||||
this._loaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
get loaded (): boolean {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
get nativeExports (): Napi2NativeExportType {
|
||||
return this.exports.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Hook
|
||||
* @param send send 偏移地址
|
||||
* @param recv recv 偏移地址
|
||||
* @returns 是否初始化成功
|
||||
*/
|
||||
initHook (send: string, recv: string): boolean {
|
||||
if (!this._loaded) {
|
||||
this.logger.logWarn('Napi2NativeLoader 未成功加载,无法初始化 Hook');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this.nativeExports.initHook?.(send, recv) ?? false;
|
||||
} catch (error) {
|
||||
this.logger.logError('Napi2NativeLoader initHook 出错:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
packages/napcat-dpapi/LICENSE
Normal file
21
packages/napcat-dpapi/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Xavier Monin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
packages/napcat-dpapi/README.md
Normal file
4
packages/napcat-dpapi/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# @primno/dpapi
|
||||
|
||||
## 协议与说明
|
||||
全部遵守原仓库要求
|
||||
65
packages/napcat-dpapi/index.ts
Normal file
65
packages/napcat-dpapi/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* napcat-dpapi - Windows DPAPI wrapper
|
||||
*
|
||||
* Loads the native @primno+dpapi.node addon from the runtime
|
||||
* native/dpapi/ directory using process.dlopen, consistent
|
||||
* with how other native modules (ffmpeg, packet, pty) are loaded.
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path, { dirname } from 'node:path';
|
||||
|
||||
export type DataProtectionScope = 'CurrentUser' | 'LocalMachine';
|
||||
|
||||
export interface DpapiBindings {
|
||||
protectData (dataToEncrypt: Uint8Array, optionalEntropy: Uint8Array | null, scope: DataProtectionScope): Uint8Array;
|
||||
unprotectData (encryptData: Uint8Array, optionalEntropy: Uint8Array | null, scope: DataProtectionScope): Uint8Array;
|
||||
}
|
||||
|
||||
let dpapiBindings: DpapiBindings | null = null;
|
||||
let loadError: Error | null = null;
|
||||
|
||||
function getAddonPath (): string {
|
||||
// At runtime, import.meta.url resolves to dist/ directory.
|
||||
// Native files are at dist/native/dpapi/{platform}-{arch}/@primno+dpapi.node
|
||||
const importDir = dirname(fileURLToPath(import.meta.url));
|
||||
const platform = process.platform; // 'win32'
|
||||
const arch = process.arch; // 'x64' or 'arm64'
|
||||
return path.join(importDir, 'native', 'dpapi', `${platform}-${arch}`, '@primno+dpapi.node');
|
||||
}
|
||||
|
||||
function loadDpapi (): DpapiBindings {
|
||||
if (dpapiBindings) {
|
||||
return dpapiBindings;
|
||||
}
|
||||
if (loadError) {
|
||||
throw loadError;
|
||||
}
|
||||
try {
|
||||
const addonPath = getAddonPath();
|
||||
const nativeModule: { exports: DpapiBindings } = { exports: {} as DpapiBindings };
|
||||
process.dlopen(nativeModule, addonPath);
|
||||
dpapiBindings = nativeModule.exports;
|
||||
return dpapiBindings;
|
||||
} catch (e) {
|
||||
loadError = e as Error;
|
||||
throw new Error(`Failed to load DPAPI native addon: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const isPlatformSupported = process.platform === 'win32';
|
||||
|
||||
export function protectData (data: Uint8Array, optionalEntropy: Uint8Array | null, scope: DataProtectionScope): Uint8Array {
|
||||
return loadDpapi().protectData(data, optionalEntropy, scope);
|
||||
}
|
||||
|
||||
export function unprotectData (data: Uint8Array, optionalEntropy: Uint8Array | null, scope: DataProtectionScope): Uint8Array {
|
||||
return loadDpapi().unprotectData(data, optionalEntropy, scope);
|
||||
}
|
||||
|
||||
export const Dpapi = {
|
||||
protectData,
|
||||
unprotectData,
|
||||
};
|
||||
|
||||
export default Dpapi;
|
||||
18
packages/napcat-dpapi/package.json
Normal file
18
packages/napcat-dpapi/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "napcat-dpapi",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
10
packages/napcat-dpapi/tsconfig.json
Normal file
10
packages/napcat-dpapi/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||
import { InitWebUi, WebUiConfig, webUiRuntimePort } from 'napcat-webui-backend/index';
|
||||
import { NapCatAdapterManager } from 'napcat-adapter';
|
||||
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
||||
import { Napi2NativeLoader, BypassOptions } from 'napcat-core/packet/handler/napi2nativeLoader';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import json5 from 'json5';
|
||||
import { FFmpegService } from 'napcat-core/helper/ffmpeg/ffmpeg';
|
||||
import { logSubscription, LogWrapper } from 'napcat-core/helper/log';
|
||||
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
|
||||
@@ -40,6 +44,35 @@ export async function NCoreInitFramework (
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion());
|
||||
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
||||
const napi2nativeLoader = new Napi2NativeLoader({ logger }); // 初始化 Napi2NativeLoader 用于后续使用
|
||||
//console.log('[NapCat] [Napi2NativeLoader]', napi2nativeLoader.nativeExports.enableAllBypasses?.());
|
||||
if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') {
|
||||
// 读取 napcat.json 配置
|
||||
let bypassOptions: BypassOptions = {
|
||||
hook: false,
|
||||
window: false,
|
||||
module: false,
|
||||
process: false,
|
||||
container: false,
|
||||
js: false,
|
||||
};
|
||||
try {
|
||||
const configFile = path.join(pathWrapper.configPath, 'napcat.json');
|
||||
if (fs.existsSync(configFile)) {
|
||||
const content = fs.readFileSync(configFile, 'utf-8');
|
||||
const config = json5.parse(content);
|
||||
if (config.bypass && typeof config.bypass === 'object') {
|
||||
bypassOptions = { ...bypassOptions, ...config.bypass };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.logWarn('[NapCat] 读取 napcat.json bypass 配置失败,已全部禁用:', e);
|
||||
}
|
||||
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
|
||||
logger.log('[NapCat] Napi2NativeLoader: Framework模式Bypass配置:', bypassOptions);
|
||||
} else {
|
||||
logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用');
|
||||
}
|
||||
// nativePacketHandler.onAll((packet) => {
|
||||
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
|
||||
// });
|
||||
@@ -73,11 +106,12 @@ export async function NCoreInitFramework (
|
||||
// 过早进入会导致addKernelMsgListener等Listener添加失败
|
||||
// await sleep(2500);
|
||||
// 初始化 NapCatFramework
|
||||
const loaderObject = new NapCatFramework(wrapper, session, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler);
|
||||
const loaderObject = new NapCatFramework(wrapper, session, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler, napi2nativeLoader);
|
||||
await loaderObject.core.initCore();
|
||||
|
||||
// 启动WebUi
|
||||
WebUiDataRuntime.setWorkingEnv(NapCatCoreWorkingEnv.Framework);
|
||||
WebUiDataRuntime.setQQDataPath(loaderObject.core.dataPath);
|
||||
InitWebUi(logger, pathWrapper, logSubscription, statusHelperSubscription).then().catch(e => logger.logError(e));
|
||||
// 使用 NapCatAdapterManager 统一管理协议适配器
|
||||
const adapterManager = new NapCatAdapterManager(loaderObject.core, loaderObject.context, pathWrapper);
|
||||
@@ -100,10 +134,12 @@ export class NapCatFramework {
|
||||
selfInfo: SelfInfo,
|
||||
basicInfoWrapper: QQBasicInfoWrapper,
|
||||
pathWrapper: NapCatPathWrapper,
|
||||
packetHandler: NativePacketHandler
|
||||
packetHandler: NativePacketHandler,
|
||||
napi2nativeLoader: Napi2NativeLoader
|
||||
) {
|
||||
this.context = {
|
||||
packetHandler,
|
||||
napi2nativeLoader,
|
||||
workingEnv: NapCatCoreWorkingEnv.Framework,
|
||||
wrapper,
|
||||
session,
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"napcat-adapter": "workspace:*",
|
||||
"napcat-webui-backend": "workspace:*",
|
||||
"napcat-vite": "workspace:*",
|
||||
"napcat-qrcode": "workspace:*"
|
||||
"napcat-qrcode": "workspace:*",
|
||||
"json5": "^2.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
|
||||
BIN
packages/napcat-native/dpapi/win32-arm64/@primno+dpapi.node
Normal file
BIN
packages/napcat-native/dpapi/win32-arm64/@primno+dpapi.node
Normal file
Binary file not shown.
BIN
packages/napcat-native/dpapi/win32-x64/@primno+dpapi.node
Normal file
BIN
packages/napcat-native/dpapi/win32-x64/@primno+dpapi.node
Normal file
Binary file not shown.
BIN
packages/napcat-native/napi2native/ffmpeg.dll
Normal file
BIN
packages/napcat-native/napi2native/ffmpeg.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -196,9 +196,14 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
||||
* 创建插件上下文
|
||||
*/
|
||||
private createPluginContext (entry: PluginEntry): NapCatPluginContext {
|
||||
const dataPath = path.join(entry.pluginPath, 'data');
|
||||
const dataPath = path.join(this.core.context.pathWrapper.configPath, 'plugins', entry.id);
|
||||
const configPath = path.join(dataPath, 'config.json');
|
||||
|
||||
// 确保插件配置目录存在
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.mkdirSync(dataPath, { recursive: true });
|
||||
}
|
||||
|
||||
// 创建插件专用日志器
|
||||
const pluginPrefix = `[Plugin: ${entry.id}]`;
|
||||
const coreLogger = this.logger;
|
||||
@@ -358,7 +363,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
||||
}
|
||||
|
||||
const pluginPath = entry.pluginPath;
|
||||
const dataPath = path.join(pluginPath, 'data');
|
||||
const dataPath = path.join(this.core.context.pathWrapper.configPath, 'plugins', pluginId);
|
||||
|
||||
if (entry.loaded) {
|
||||
await this.unloadPlugin(entry);
|
||||
@@ -372,7 +377,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
||||
fs.rmSync(pluginPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 清理数据
|
||||
// 清理插件配置数据
|
||||
if (cleanData && fs.existsSync(dataPath)) {
|
||||
fs.rmSync(dataPath, { recursive: true, force: true });
|
||||
}
|
||||
@@ -440,11 +445,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> i
|
||||
* 获取插件数据目录路径
|
||||
*/
|
||||
public getPluginDataPath (pluginId: string): string {
|
||||
const entry = this.plugins.get(pluginId);
|
||||
if (!entry) {
|
||||
throw new Error(`Plugin ${pluginId} not found`);
|
||||
}
|
||||
return path.join(entry.pluginPath, 'data');
|
||||
return path.join(this.core.context.pathWrapper.configPath, 'plugins', pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -173,9 +173,14 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
* 创建插件上下文
|
||||
*/
|
||||
private createPluginContext (entry: PluginEntry): NapCatPluginContext {
|
||||
const dataPath = path.join(entry.pluginPath, 'data');
|
||||
const dataPath = path.join(this.core.context.pathWrapper.configPath, 'plugins', entry.id);
|
||||
const configPath = path.join(dataPath, 'config.json');
|
||||
|
||||
// 确保插件配置目录存在
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.mkdirSync(dataPath, { recursive: true });
|
||||
}
|
||||
|
||||
// 创建插件专用日志器
|
||||
const pluginPrefix = `[Plugin: ${entry.id}]`;
|
||||
const coreLogger = this.logger;
|
||||
@@ -323,7 +328,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
}
|
||||
|
||||
const pluginPath = entry.pluginPath;
|
||||
const dataPath = path.join(pluginPath, 'data');
|
||||
const dataPath = path.join(this.core.context.pathWrapper.configPath, 'plugins', pluginId);
|
||||
|
||||
// 先卸载插件
|
||||
await this.unloadPlugin(entry);
|
||||
@@ -336,7 +341,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
fs.rmSync(pluginPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 清理数据
|
||||
// 清理插件配置数据
|
||||
if (cleanData && fs.existsSync(dataPath)) {
|
||||
fs.rmSync(dataPath, { recursive: true, force: true });
|
||||
}
|
||||
@@ -404,11 +409,7 @@ export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> impleme
|
||||
* 获取插件数据目录路径
|
||||
*/
|
||||
public getPluginDataPath (pluginId: string): string {
|
||||
const entry = this.plugins.get(pluginId);
|
||||
if (!entry) {
|
||||
throw new Error(`Plugin ${pluginId} not found`);
|
||||
}
|
||||
return path.join(entry.pluginPath, 'data');
|
||||
return path.join(this.core.context.pathWrapper.configPath, 'plugins', pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ import { hostname, systemVersion } from 'napcat-common/src/system';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import json5 from 'json5';
|
||||
import { LoginListItem, NodeIKernelLoginService } from 'napcat-core/services';
|
||||
import qrcode from 'napcat-qrcode/lib/main';
|
||||
import { NapCatAdapterManager } from 'napcat-adapter';
|
||||
@@ -30,12 +31,68 @@ import { NodeIO3MiscListener } from 'napcat-core/listeners/NodeIO3MiscListener';
|
||||
import { sleep } from 'napcat-common/src/helper';
|
||||
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
||||
import { NativePacketHandler } from 'napcat-core/packet/handler/client';
|
||||
import { Napi2NativeLoader, BypassOptions } from 'napcat-core/packet/handler/napi2nativeLoader';
|
||||
import { logSubscription, LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||
import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info';
|
||||
import { statusHelperSubscription } from '@/napcat-core/helper/status';
|
||||
import { applyPendingUpdates } from '@/napcat-webui-backend/src/api/UpdateNapCat';
|
||||
import { connectToNamedPipe } from './pipe';
|
||||
|
||||
/**
|
||||
* 读取 napcat.json 配置中的 bypass 选项,并根据分步禁用级别覆盖
|
||||
*
|
||||
* 分步禁用级别 (NAPCAT_BYPASS_DISABLE_LEVEL):
|
||||
* 0: 使用配置文件原始值(全部启用或用户自定义)
|
||||
* 1: 强制禁用 hook
|
||||
* 2: 强制禁用 hook + module
|
||||
* 3: 强制禁用全部 bypass
|
||||
*/
|
||||
function loadBypassConfig (configPath: string, logger: LogWrapper): BypassOptions {
|
||||
const defaultOptions: BypassOptions = {
|
||||
hook: true,
|
||||
window: true,
|
||||
module: true,
|
||||
process: true,
|
||||
container: true,
|
||||
js: true,
|
||||
};
|
||||
|
||||
let options = { ...defaultOptions };
|
||||
try {
|
||||
const configFile = path.join(configPath, 'napcat.json');
|
||||
if (fs.existsSync(configFile)) {
|
||||
const content = fs.readFileSync(configFile, 'utf-8');
|
||||
const config = json5.parse(content);
|
||||
if (config.bypass && typeof config.bypass === 'object') {
|
||||
options = { ...defaultOptions, ...config.bypass };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.logWarn('[NapCat] 读取 bypass 配置失败,使用默认值:', e);
|
||||
}
|
||||
// 根据分步禁用级别覆盖配置
|
||||
const disableLevel = parseInt(process.env['NAPCAT_BYPASS_DISABLE_LEVEL'] || '0', 10);
|
||||
if (disableLevel > 0) {
|
||||
const levelDescriptions = ['全部启用', '禁用 hook', '禁用 hook + module', '全部禁用 bypass'];
|
||||
logger.logWarn(`[NapCat] 崩溃恢复:当前 bypass 禁用级别 ${disableLevel} (${levelDescriptions[disableLevel] ?? '未知'})`);
|
||||
if (disableLevel >= 1) {
|
||||
options.hook = false;
|
||||
}
|
||||
if (disableLevel >= 2) {
|
||||
options.module = false;
|
||||
}
|
||||
if (disableLevel >= 3) {
|
||||
options.hook = false;
|
||||
options.window = false;
|
||||
options.module = false;
|
||||
options.process = false;
|
||||
options.container = false;
|
||||
options.js = false;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
// NapCat Shell App ES 入口文件
|
||||
async function handleUncaughtExceptions (logger: LogWrapper) {
|
||||
process.on('uncaughtException', (err) => {
|
||||
@@ -387,20 +444,33 @@ export async function NCoreInitShell () {
|
||||
handleUncaughtExceptions(logger);
|
||||
await applyPendingUpdates(pathWrapper, logger);
|
||||
|
||||
// 提前初始化 Native 模块(在登录前加载)
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const nativePacketHandler = new NativePacketHandler({ logger });
|
||||
const napi2nativeLoader = new Napi2NativeLoader({ logger });
|
||||
await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion());
|
||||
|
||||
// 初始化 FFmpeg 服务
|
||||
await FFmpegService.init(pathWrapper.binaryPath, logger);
|
||||
|
||||
if (!(process.env['NAPCAT_DISABLE_PIPE'] === '1' || process.env['NAPCAT_WORKER_PROCESS'] === '1')) {
|
||||
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
|
||||
}
|
||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||
const wrapper = loadQQWrapper(basicInfoWrapper.QQMainPath, basicInfoWrapper.getFullQQVersion());
|
||||
const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用
|
||||
|
||||
// nativePacketHandler.onAll((packet) => {
|
||||
// console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data);
|
||||
// });
|
||||
await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion());
|
||||
if (process.env['NAPCAT_ENABLE_VERBOSE_LOG'] === '1') {
|
||||
napi2nativeLoader.nativeExports.setVerbose?.(true);
|
||||
}
|
||||
// wrapper.node 加载后立刻启用 Bypass(可通过环境变量禁用)
|
||||
if (process.env['NAPCAT_DISABLE_BYPASS'] !== '1') {
|
||||
const bypassOptions = loadBypassConfig(pathWrapper.configPath, logger);
|
||||
logger.logDebug('[NapCat] Bypass 配置:', bypassOptions);
|
||||
const bypassEnabled = napi2nativeLoader.nativeExports.enableAllBypasses?.(bypassOptions);
|
||||
if (bypassEnabled) {
|
||||
logger.log('[NapCat] Napi2NativeLoader: 已启用Bypass');
|
||||
}
|
||||
} else {
|
||||
logger.log('[NapCat] Napi2NativeLoader: Bypass已通过环境变量禁用');
|
||||
}
|
||||
|
||||
const o3Service = wrapper.NodeIO3MiscService.get();
|
||||
o3Service.addO3MiscListener(new NodeIO3MiscListener());
|
||||
@@ -425,6 +495,7 @@ export async function NCoreInitShell () {
|
||||
}
|
||||
}
|
||||
const [dataPath, dataPathGlobal] = getDataPaths(wrapper);
|
||||
WebUiDataRuntime.setQQDataPath(dataPath);
|
||||
const systemPlatform = getPlatformType();
|
||||
|
||||
if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined');
|
||||
@@ -450,6 +521,13 @@ export async function NCoreInitShell () {
|
||||
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
|
||||
|
||||
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
|
||||
|
||||
// 登录成功后通知 Master 进程(用于切换崩溃重试策略)
|
||||
if (typeof process.send === 'function') {
|
||||
process.send({ type: 'login-success' });
|
||||
logger.log('[NapCat] 已通知主进程登录成功');
|
||||
}
|
||||
|
||||
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
|
||||
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));
|
||||
|
||||
@@ -487,7 +565,8 @@ export async function NCoreInitShell () {
|
||||
selfInfo,
|
||||
basicInfoWrapper,
|
||||
pathWrapper,
|
||||
nativePacketHandler
|
||||
nativePacketHandler,
|
||||
napi2nativeLoader
|
||||
).InitNapCat();
|
||||
}
|
||||
|
||||
@@ -502,10 +581,12 @@ export class NapCatShell {
|
||||
selfInfo: SelfInfo,
|
||||
basicInfoWrapper: QQBasicInfoWrapper,
|
||||
pathWrapper: NapCatPathWrapper,
|
||||
packetHandler: NativePacketHandler
|
||||
packetHandler: NativePacketHandler,
|
||||
napi2nativeLoader: Napi2NativeLoader
|
||||
) {
|
||||
this.context = {
|
||||
packetHandler,
|
||||
napi2nativeLoader,
|
||||
workingEnv: NapCatCoreWorkingEnv.Shell,
|
||||
wrapper,
|
||||
session,
|
||||
|
||||
@@ -45,7 +45,7 @@ const ENV = {
|
||||
|
||||
// Worker 消息类型
|
||||
interface WorkerMessage {
|
||||
type: 'restart' | 'restart-prepare' | 'shutdown';
|
||||
type: 'restart' | 'restart-prepare' | 'shutdown' | 'login-success';
|
||||
secretKey?: string;
|
||||
port?: number;
|
||||
}
|
||||
@@ -65,6 +65,16 @@ const recentCrashTimestamps: number[] = [];
|
||||
const CRASH_TIME_WINDOW = 10000; // 10秒时间窗口
|
||||
const MAX_CRASHES_IN_WINDOW = 3; // 最大崩溃次数
|
||||
|
||||
// 分步禁用策略:记录当前禁用级别 (0-3)
|
||||
// 0: 全部启用
|
||||
// 1: 禁用 hook
|
||||
// 2: 禁用 hook + module
|
||||
// 3: 全部禁用
|
||||
let bypassDisableLevel = 0;
|
||||
|
||||
// 是否已登录成功(登录后不再使用分步禁用策略)
|
||||
let isLoggedIn = false;
|
||||
|
||||
/**
|
||||
* 获取进程类型名称(用于日志)
|
||||
*/
|
||||
@@ -113,11 +123,49 @@ function forceKillProcess (pid: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理进程树中的残留子进程(Electron 模式专用)
|
||||
* 排除当前主进程和新 worker 进程
|
||||
*/
|
||||
async function cleanupOrphanedProcesses (excludePids: number[]): Promise<void> {
|
||||
if (!isElectron) return;
|
||||
|
||||
try {
|
||||
// 使用 Electron 的 app.getAppMetrics() 获取所有相关进程
|
||||
// @ts-ignore - electron 运行时存在但类型声明可能缺失
|
||||
const electron = await import('electron');
|
||||
if (electron.app && typeof electron.app.getAppMetrics === 'function') {
|
||||
const metrics = electron.app.getAppMetrics();
|
||||
const mainPid = process.pid;
|
||||
|
||||
for (const metric of metrics) {
|
||||
const pid = metric.pid;
|
||||
// 排除主进程、新 worker 进程和明确排除的 PID
|
||||
if (pid === mainPid || excludePids.includes(pid)) {
|
||||
continue;
|
||||
}
|
||||
// 尝试终止残留进程
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
logger.log(`[NapCat] [Process] 已清理残留进程: PID ${pid} (${metric.type})`);
|
||||
} catch {
|
||||
// 进程可能已经不存在,忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Electron API 不可用或出错,静默忽略
|
||||
logger.logDebug?.('[NapCat] [Process] 清理残留进程时出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启 Worker 进程
|
||||
*/
|
||||
export async function restartWorker (secretKey?: string, port?: number): Promise<void> {
|
||||
isRestarting = true;
|
||||
isLoggedIn = false;
|
||||
bypassDisableLevel = 0;
|
||||
|
||||
if (!currentWorker) {
|
||||
logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程');
|
||||
@@ -166,6 +214,13 @@ export async function restartWorker (secretKey?: string, port?: number): Promise
|
||||
|
||||
// 5. 启动新进程(重启模式不传递快速登录参数,传递密钥和端口)
|
||||
await startWorker(false, secretKey, port);
|
||||
|
||||
// 6. Electron 模式下清理可能残留的子进程
|
||||
if (isElectron && currentWorker?.pid) {
|
||||
const excludePids = [process.pid, currentWorker.pid];
|
||||
await cleanupOrphanedProcesses(excludePids);
|
||||
}
|
||||
|
||||
isRestarting = false;
|
||||
}
|
||||
|
||||
@@ -203,6 +258,7 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string,
|
||||
NAPCAT_WORKER_PROCESS: '1',
|
||||
...(secretKey ? { NAPCAT_WEBUI_JWT_SECRET_KEY: secretKey } : {}),
|
||||
...(preferredPort ? { NAPCAT_WEBUI_PREFERRED_PORT: String(preferredPort) } : {}),
|
||||
...(bypassDisableLevel > 0 ? { NAPCAT_BYPASS_DISABLE_LEVEL: String(bypassDisableLevel) } : {}),
|
||||
},
|
||||
stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'],
|
||||
});
|
||||
@@ -232,6 +288,9 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string,
|
||||
restartWorker(message.secretKey, message.port).catch(e => {
|
||||
logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e);
|
||||
});
|
||||
} else if (message.type === 'login-success') {
|
||||
isLoggedIn = true;
|
||||
logger.log(`[NapCat] [${processType}] Worker进程已登录成功,切换到正常重试策略`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -254,13 +313,34 @@ async function startWorker (passQuickLogin: boolean = true, secretKey?: string,
|
||||
// 记录本次崩溃
|
||||
recentCrashTimestamps.push(now);
|
||||
|
||||
// 检查是否超过崩溃阈值
|
||||
if (recentCrashTimestamps.length >= MAX_CRASHES_IN_WINDOW) {
|
||||
logger.logError(`[NapCat] [${processType}] Worker进程在 ${CRASH_TIME_WINDOW / 1000} 秒内异常退出 ${MAX_CRASHES_IN_WINDOW} 次,主进程退出`);
|
||||
process.exit(1);
|
||||
// 登录前:使用分步禁用策略
|
||||
if (!isLoggedIn) {
|
||||
// 每次崩溃提升禁用级别
|
||||
bypassDisableLevel = Math.min(bypassDisableLevel + 1, 3);
|
||||
|
||||
const levelDescriptions = [
|
||||
'全部启用',
|
||||
'禁用 hook',
|
||||
'禁用 hook + module',
|
||||
'全部禁用 bypass'
|
||||
];
|
||||
|
||||
if (bypassDisableLevel >= 3 && recentCrashTimestamps.length >= MAX_CRASHES_IN_WINDOW) {
|
||||
logger.logError(`[NapCat] [${processType}] Worker进程在 ${CRASH_TIME_WINDOW / 1000} 秒内异常退出 ${MAX_CRASHES_IN_WINDOW} 次,已尝试全部禁用策略,主进程退出`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出 (${recentCrashTimestamps.length}/${MAX_CRASHES_IN_WINDOW}),切换到禁用级别 ${bypassDisableLevel}: ${levelDescriptions[bypassDisableLevel]},正在尝试重新拉起...`);
|
||||
} else {
|
||||
// 登录后:使用正常重试策略
|
||||
if (recentCrashTimestamps.length >= MAX_CRASHES_IN_WINDOW) {
|
||||
logger.logError(`[NapCat] [${processType}] Worker进程在 ${CRASH_TIME_WINDOW / 1000} 秒内异常退出 ${MAX_CRASHES_IN_WINDOW} 次,主进程退出`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出 (${recentCrashTimestamps.length}/${MAX_CRASHES_IN_WINDOW}),正在尝试重新拉起...`);
|
||||
}
|
||||
|
||||
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出 (${recentCrashTimestamps.length}/${MAX_CRASHES_IN_WINDOW}),正在尝试重新拉起...`);
|
||||
startWorker(true).catch(e => {
|
||||
logger.logError(`[NapCat] [${processType}] 重新拉起Worker进程失败:`, e);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"napcat-qrcode": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"json5": "^2.2.3",
|
||||
"@types/node": "^22.0.1",
|
||||
"napcat-vite": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ const ShellBaseConfig = (source_map: boolean = false) =>
|
||||
'@/napcat-common': resolve(__dirname, '../napcat-common'),
|
||||
'@/napcat-onebot': resolve(__dirname, '../napcat-onebot'),
|
||||
'@/napcat-pty': resolve(__dirname, '../napcat-pty'),
|
||||
'@/napcat-dpapi': resolve(__dirname, '../napcat-dpapi'),
|
||||
'@/napcat-webui-backend': resolve(__dirname, '../napcat-webui-backend'),
|
||||
'@/napcat-image-size': resolve(__dirname, '../napcat-image-size'),
|
||||
'@/napcat-protocol': resolve(__dirname, '../napcat-protocol'),
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"multer": "^2.0.1",
|
||||
"napcat-common": "workspace:*",
|
||||
"napcat-dpapi": "workspace:*",
|
||||
"napcat-pty": "workspace:*",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
|
||||
97
packages/napcat-webui-backend/src/api/NapCatConfig.ts
Normal file
97
packages/napcat-webui-backend/src/api/NapCatConfig.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||
import json5 from 'json5';
|
||||
|
||||
// NapCat 配置默认值
|
||||
const defaultNapcatConfig = {
|
||||
fileLog: false,
|
||||
consoleLog: true,
|
||||
fileLogLevel: 'debug',
|
||||
consoleLogLevel: 'info',
|
||||
packetBackend: 'auto',
|
||||
packetServer: '',
|
||||
o3HookMode: 1,
|
||||
bypass: {
|
||||
hook: true,
|
||||
window: true,
|
||||
module: true,
|
||||
process: true,
|
||||
container: true,
|
||||
js: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 napcat 配置文件路径
|
||||
*/
|
||||
function getNapcatConfigPath (): string {
|
||||
return resolve(webUiPathWrapper.configPath, './napcat.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 napcat 配置
|
||||
*/
|
||||
function readNapcatConfig (): Record<string, unknown> {
|
||||
const configPath = getNapcatConfigPath();
|
||||
try {
|
||||
if (existsSync(configPath)) {
|
||||
const content = readFileSync(configPath, 'utf-8');
|
||||
return { ...defaultNapcatConfig, ...json5.parse(content) };
|
||||
}
|
||||
} catch (_e) {
|
||||
// 读取失败,使用默认值
|
||||
}
|
||||
return { ...defaultNapcatConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 napcat 配置
|
||||
*/
|
||||
function writeNapcatConfig (config: Record<string, unknown>): void {
|
||||
const configPath = resolve(webUiPathWrapper.configPath, './napcat.json');
|
||||
mkdirSync(webUiPathWrapper.configPath, { recursive: true });
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
// 获取 NapCat 配置
|
||||
export const NapCatGetConfigHandler: RequestHandler = (_, res) => {
|
||||
try {
|
||||
const config = readNapcatConfig();
|
||||
return sendSuccess(res, config);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Config Get Error: ' + (e as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置 NapCat 配置
|
||||
export const NapCatSetConfigHandler: RequestHandler = (req, res) => {
|
||||
try {
|
||||
const newConfig = req.body;
|
||||
if (!newConfig || typeof newConfig !== 'object') {
|
||||
return sendError(res, 'config is empty or invalid');
|
||||
}
|
||||
|
||||
// 读取当前配置并合并
|
||||
const currentConfig = readNapcatConfig();
|
||||
const mergedConfig = { ...currentConfig, ...newConfig };
|
||||
|
||||
// 验证 bypass 字段
|
||||
if (mergedConfig.bypass && typeof mergedConfig.bypass === 'object') {
|
||||
const bypass = mergedConfig.bypass as Record<string, unknown>;
|
||||
const validKeys = ['hook', 'window', 'module', 'process', 'container', 'js'];
|
||||
for (const key of validKeys) {
|
||||
if (key in bypass && typeof bypass[key] !== 'boolean') {
|
||||
return sendError(res, `bypass.${key} must be boolean`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeNapcatConfig(mergedConfig);
|
||||
return sendSuccess(res, null);
|
||||
} catch (e) {
|
||||
return sendError(res, 'Config Set Error: ' + (e as Error).message);
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,36 @@ import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||
import { WebUiConfig } from '@/napcat-webui-backend/index';
|
||||
import { isEmpty } from '@/napcat-webui-backend/src/utils/check';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||
import { Registry20Utils, MachineInfoUtils } from '@/napcat-webui-backend/src/utils/guid';
|
||||
import os from 'node:os';
|
||||
|
||||
// 获取 Registry20 路径的辅助函数
|
||||
const getRegistryPath = () => {
|
||||
// 优先从 WebUiDataRuntime 获取早期设置的 dataPath
|
||||
let dataPath = WebUiDataRuntime.getQQDataPath();
|
||||
if (!dataPath) {
|
||||
// 回退: 从 OneBotContext 获取
|
||||
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||
dataPath = oneBotContext?.core?.dataPath;
|
||||
}
|
||||
if (!dataPath) {
|
||||
throw new Error('QQ data path not available yet');
|
||||
}
|
||||
return Registry20Utils.getRegistryPath(dataPath);
|
||||
};
|
||||
|
||||
// 获取 machine-info 路径的辅助函数 (Linux)
|
||||
const getMachineInfoPath = () => {
|
||||
let dataPath = WebUiDataRuntime.getQQDataPath();
|
||||
if (!dataPath) {
|
||||
const oneBotContext = WebUiDataRuntime.getOneBotContext();
|
||||
dataPath = oneBotContext?.core?.dataPath;
|
||||
}
|
||||
if (!dataPath) {
|
||||
throw new Error('QQ data path not available yet');
|
||||
}
|
||||
return MachineInfoUtils.getMachineInfoPath(dataPath);
|
||||
};
|
||||
|
||||
// 获取QQ登录二维码
|
||||
export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
|
||||
@@ -147,3 +177,239 @@ export const QQPasswordLoginHandler: RequestHandler = async (req, res) => {
|
||||
}
|
||||
return sendSuccess(res, null);
|
||||
};
|
||||
|
||||
// 重置设备信息
|
||||
export const QQResetDeviceIDHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const registryPath = getRegistryPath();
|
||||
// 自动备份
|
||||
try {
|
||||
await Registry20Utils.backup(registryPath);
|
||||
} catch (e) {
|
||||
// 忽略备份错误(例如文件不存在)
|
||||
}
|
||||
|
||||
await Registry20Utils.delete(registryPath);
|
||||
return sendSuccess(res, { message: 'Device ID reset successfully (Registry20 deleted)' });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to reset Device ID: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取设备 GUID
|
||||
export const QQGetDeviceGUIDHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const registryPath = getRegistryPath();
|
||||
const guid = await Registry20Utils.readGuid(registryPath);
|
||||
return sendSuccess(res, { guid });
|
||||
} catch (e) {
|
||||
// 可能是文件不存在,或者非 Windows 平台,或者解密失败
|
||||
return sendError(res, `Failed to get GUID: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置设备 GUID
|
||||
export const QQSetDeviceGUIDHandler: RequestHandler = async (req, res) => {
|
||||
const { guid } = req.body;
|
||||
if (!guid || typeof guid !== 'string' || guid.length !== 32) {
|
||||
return sendError(res, 'Invalid GUID format, must be 32 hex characters');
|
||||
}
|
||||
try {
|
||||
const registryPath = getRegistryPath();
|
||||
// 自动备份
|
||||
try {
|
||||
await Registry20Utils.backup(registryPath);
|
||||
} catch { }
|
||||
|
||||
await Registry20Utils.writeGuid(registryPath, guid);
|
||||
return sendSuccess(res, { message: 'GUID set successfully' });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to set GUID: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取备份列表
|
||||
export const QQGetGUIDBackupsHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const registryPath = getRegistryPath();
|
||||
const backups = Registry20Utils.getBackups(registryPath);
|
||||
return sendSuccess(res, backups);
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to get backups: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复备份
|
||||
export const QQRestoreGUIDBackupHandler: RequestHandler = async (req, res) => {
|
||||
const { backupName } = req.body;
|
||||
if (!backupName) {
|
||||
return sendError(res, 'Backup name is required');
|
||||
}
|
||||
try {
|
||||
const registryPath = getRegistryPath();
|
||||
await Registry20Utils.restore(registryPath, backupName);
|
||||
return sendSuccess(res, { message: 'Restored successfully' });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to restore: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建备份
|
||||
export const QQCreateGUIDBackupHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const registryPath = getRegistryPath();
|
||||
const backupPath = await Registry20Utils.backup(registryPath);
|
||||
return sendSuccess(res, { message: 'Backup created', path: backupPath });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to backup: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 重启NapCat
|
||||
export const QQRestartNapCatHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const result = await WebUiDataRuntime.requestRestartProcess();
|
||||
if (result.result) {
|
||||
return sendSuccess(res, { message: result.message || 'Restart initiated' });
|
||||
} else {
|
||||
return sendError(res, result.message || 'Restart failed');
|
||||
}
|
||||
} catch (e) {
|
||||
return sendError(res, `Restart error: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 平台信息 & Linux GUID 管理
|
||||
// ============================================================
|
||||
|
||||
// 获取平台信息
|
||||
export const QQGetPlatformInfoHandler: RequestHandler = async (_, res) => {
|
||||
return sendSuccess(res, { platform: os.platform() });
|
||||
};
|
||||
|
||||
// 获取 Linux MAC 地址 (从 machine-info 文件读取)
|
||||
export const QQGetLinuxMACHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
const mac = MachineInfoUtils.readMac(machineInfoPath);
|
||||
return sendSuccess(res, { mac });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to get MAC: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置 Linux MAC 地址 (写入 machine-info 文件)
|
||||
export const QQSetLinuxMACHandler: RequestHandler = async (req, res) => {
|
||||
const { mac } = req.body;
|
||||
if (!mac || typeof mac !== 'string') {
|
||||
return sendError(res, 'MAC address is required');
|
||||
}
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
// 自动备份
|
||||
try {
|
||||
MachineInfoUtils.backup(machineInfoPath);
|
||||
} catch { }
|
||||
|
||||
MachineInfoUtils.writeMac(machineInfoPath, mac);
|
||||
return sendSuccess(res, { message: 'MAC set successfully' });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to set MAC: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取 Linux machine-id
|
||||
export const QQGetLinuxMachineIdHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const machineId = MachineInfoUtils.readMachineId();
|
||||
return sendSuccess(res, { machineId });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to read machine-id: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 计算 Linux GUID (用于前端实时预览)
|
||||
export const QQComputeLinuxGUIDHandler: RequestHandler = async (req, res) => {
|
||||
const { mac, machineId } = req.body;
|
||||
try {
|
||||
// 如果没传 machineId,从 /etc/machine-id 读取
|
||||
let mid = machineId;
|
||||
if (!mid || typeof mid !== 'string') {
|
||||
try {
|
||||
mid = MachineInfoUtils.readMachineId();
|
||||
} catch {
|
||||
mid = '';
|
||||
}
|
||||
}
|
||||
// 如果没传 mac,从 machine-info 文件读取
|
||||
let macStr = mac;
|
||||
if (!macStr || typeof macStr !== 'string') {
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
macStr = MachineInfoUtils.readMac(machineInfoPath);
|
||||
} catch {
|
||||
macStr = '';
|
||||
}
|
||||
}
|
||||
const guid = MachineInfoUtils.computeGuid(mid, macStr);
|
||||
return sendSuccess(res, { guid, machineId: mid, mac: macStr });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to compute GUID: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取 Linux machine-info 备份列表
|
||||
export const QQGetLinuxMachineInfoBackupsHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
const backups = MachineInfoUtils.getBackups(machineInfoPath);
|
||||
return sendSuccess(res, backups);
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to get backups: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建 Linux machine-info 备份
|
||||
export const QQCreateLinuxMachineInfoBackupHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
const backupPath = MachineInfoUtils.backup(machineInfoPath);
|
||||
return sendSuccess(res, { message: 'Backup created', path: backupPath });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to backup: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复 Linux machine-info 备份
|
||||
export const QQRestoreLinuxMachineInfoBackupHandler: RequestHandler = async (req, res) => {
|
||||
const { backupName } = req.body;
|
||||
if (!backupName) {
|
||||
return sendError(res, 'Backup name is required');
|
||||
}
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
MachineInfoUtils.restore(machineInfoPath, backupName);
|
||||
return sendSuccess(res, { message: 'Restored successfully' });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to restore: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置 Linux 设备信息 (删除 machine-info)
|
||||
export const QQResetLinuxDeviceIDHandler: RequestHandler = async (_, res) => {
|
||||
try {
|
||||
const machineInfoPath = getMachineInfoPath();
|
||||
// 自动备份
|
||||
try {
|
||||
MachineInfoUtils.backup(machineInfoPath);
|
||||
} catch { }
|
||||
|
||||
MachineInfoUtils.delete(machineInfoPath);
|
||||
return sendSuccess(res, { message: 'Device ID reset successfully (machine-info deleted)' });
|
||||
} catch (e) {
|
||||
return sendError(res, `Failed to reset Device ID: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ const LoginRuntime: LoginRuntimeType = {
|
||||
},
|
||||
QQLoginError: '',
|
||||
QQVersion: 'unknown',
|
||||
QQDataPath: '',
|
||||
OneBotContext: null,
|
||||
onQQLoginStatusChange: async (status: boolean) => {
|
||||
LoginRuntime.QQLoginStatus = status;
|
||||
@@ -167,6 +168,14 @@ export const WebUiDataRuntime = {
|
||||
return LoginRuntime.QQVersion;
|
||||
},
|
||||
|
||||
setQQDataPath (dataPath: string) {
|
||||
LoginRuntime.QQDataPath = dataPath;
|
||||
},
|
||||
|
||||
getQQDataPath (): string {
|
||||
return LoginRuntime.QQDataPath;
|
||||
},
|
||||
|
||||
setWebUiConfigQuickFunction (func: LoginRuntimeType['WebUiConfigQuickFunction']): void {
|
||||
LoginRuntime.WebUiConfigQuickFunction = func;
|
||||
},
|
||||
|
||||
12
packages/napcat-webui-backend/src/router/NapCatConfig.ts
Normal file
12
packages/napcat-webui-backend/src/router/NapCatConfig.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
import { NapCatGetConfigHandler, NapCatSetConfigHandler } from '@/napcat-webui-backend/src/api/NapCatConfig';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
// router:获取 NapCat 配置
|
||||
router.get('/GetConfig', NapCatGetConfigHandler);
|
||||
// router:设置 NapCat 配置
|
||||
router.post('/SetConfig', NapCatSetConfigHandler);
|
||||
|
||||
export { router as NapCatConfigRouter };
|
||||
@@ -11,6 +11,22 @@ import {
|
||||
setAutoLoginAccountHandler,
|
||||
QQRefreshQRcodeHandler,
|
||||
QQPasswordLoginHandler,
|
||||
QQResetDeviceIDHandler,
|
||||
QQRestartNapCatHandler,
|
||||
QQGetDeviceGUIDHandler,
|
||||
QQSetDeviceGUIDHandler,
|
||||
QQGetGUIDBackupsHandler,
|
||||
QQRestoreGUIDBackupHandler,
|
||||
QQCreateGUIDBackupHandler,
|
||||
QQGetPlatformInfoHandler,
|
||||
QQGetLinuxMACHandler,
|
||||
QQSetLinuxMACHandler,
|
||||
QQGetLinuxMachineIdHandler,
|
||||
QQComputeLinuxGUIDHandler,
|
||||
QQGetLinuxMachineInfoBackupsHandler,
|
||||
QQCreateLinuxMachineInfoBackupHandler,
|
||||
QQRestoreLinuxMachineInfoBackupHandler,
|
||||
QQResetLinuxDeviceIDHandler,
|
||||
} from '@/napcat-webui-backend/src/api/QQLogin';
|
||||
|
||||
const router: Router = Router();
|
||||
@@ -34,5 +50,41 @@ router.post('/SetQuickLoginQQ', setAutoLoginAccountHandler);
|
||||
router.post('/RefreshQRcode', QQRefreshQRcodeHandler);
|
||||
// router:密码登录
|
||||
router.post('/PasswordLogin', QQPasswordLoginHandler);
|
||||
// router:重置设备信息
|
||||
router.post('/ResetDeviceID', QQResetDeviceIDHandler);
|
||||
// router:重启NapCat
|
||||
router.post('/RestartNapCat', QQRestartNapCatHandler);
|
||||
// router:获取设备GUID
|
||||
router.post('/GetDeviceGUID', QQGetDeviceGUIDHandler);
|
||||
// router:设置设备GUID
|
||||
router.post('/SetDeviceGUID', QQSetDeviceGUIDHandler);
|
||||
// router:获取GUID备份列表
|
||||
router.post('/GetGUIDBackups', QQGetGUIDBackupsHandler);
|
||||
// router:恢复GUID备份
|
||||
router.post('/RestoreGUIDBackup', QQRestoreGUIDBackupHandler);
|
||||
// router:创建GUID备份
|
||||
router.post('/CreateGUIDBackup', QQCreateGUIDBackupHandler);
|
||||
|
||||
// ============================================================
|
||||
// 平台信息 & Linux GUID 管理
|
||||
// ============================================================
|
||||
// router:获取平台信息
|
||||
router.post('/GetPlatformInfo', QQGetPlatformInfoHandler);
|
||||
// router:获取Linux MAC地址
|
||||
router.post('/GetLinuxMAC', QQGetLinuxMACHandler);
|
||||
// router:设置Linux MAC地址
|
||||
router.post('/SetLinuxMAC', QQSetLinuxMACHandler);
|
||||
// router:获取Linux machine-id
|
||||
router.post('/GetLinuxMachineId', QQGetLinuxMachineIdHandler);
|
||||
// router:计算Linux GUID
|
||||
router.post('/ComputeLinuxGUID', QQComputeLinuxGUIDHandler);
|
||||
// router:获取Linux machine-info备份列表
|
||||
router.post('/GetLinuxMachineInfoBackups', QQGetLinuxMachineInfoBackupsHandler);
|
||||
// router:创建Linux machine-info备份
|
||||
router.post('/CreateLinuxMachineInfoBackup', QQCreateLinuxMachineInfoBackupHandler);
|
||||
// router:恢复Linux machine-info备份
|
||||
router.post('/RestoreLinuxMachineInfoBackup', QQRestoreLinuxMachineInfoBackupHandler);
|
||||
// router:重置Linux设备信息
|
||||
router.post('/ResetLinuxDeviceID', QQResetLinuxDeviceIDHandler);
|
||||
|
||||
export { router as QQLoginRouter };
|
||||
|
||||
@@ -19,6 +19,7 @@ import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
|
||||
import { ProcessRouter } from './Process';
|
||||
import { PluginRouter } from './Plugin';
|
||||
import { MirrorRouter } from './Mirror';
|
||||
import { NapCatConfigRouter } from './NapCatConfig';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
@@ -53,5 +54,7 @@ router.use('/Process', ProcessRouter);
|
||||
router.use('/Plugin', PluginRouter);
|
||||
// router:镜像管理相关路由
|
||||
router.use('/Mirror', MirrorRouter);
|
||||
// router:NapCat配置相关路由
|
||||
router.use('/NapCatConfig', NapCatConfigRouter);
|
||||
|
||||
export { router as ALLRouter };
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface LoginRuntimeType {
|
||||
QQLoginInfo: SelfInfo;
|
||||
QQLoginError: string;
|
||||
QQVersion: string;
|
||||
QQDataPath: string;
|
||||
onQQLoginStatusChange: (status: boolean) => Promise<void>;
|
||||
onWebUiTokenChange: (token: string) => Promise<void>;
|
||||
onRefreshQRCode: () => Promise<void>;
|
||||
|
||||
275
packages/napcat-webui-backend/src/utils/guid.ts
Normal file
275
packages/napcat-webui-backend/src/utils/guid.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import crypto from 'node:crypto';
|
||||
import { protectData, unprotectData } from 'napcat-dpapi';
|
||||
|
||||
const GUID_HEADER = Buffer.from([0x00, 0x00, 0x00, 0x14]);
|
||||
const XOR_KEY = 0x10;
|
||||
|
||||
/**
|
||||
* Unprotects data using Windows DPAPI via napcat-dpapi.
|
||||
*/
|
||||
function dpapiUnprotect (filePath: string): Buffer {
|
||||
const encrypted = fs.readFileSync(filePath);
|
||||
return Buffer.from(unprotectData(encrypted, null, 'CurrentUser'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Protects data using Windows DPAPI and writes to file.
|
||||
*/
|
||||
function dpapiProtectAndWrite (filePath: string, data: Buffer): void {
|
||||
const encrypted = protectData(data, null, 'CurrentUser');
|
||||
fs.writeFileSync(filePath, Buffer.from(encrypted));
|
||||
}
|
||||
|
||||
export class Registry20Utils {
|
||||
static getRegistryPath (dataPath: string): string {
|
||||
return path.join(dataPath, 'nt_qq', 'global', 'nt_data', 'msf', 'Registry20');
|
||||
}
|
||||
|
||||
static readGuid (registryPath: string): string {
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
throw new Error('Registry20 file not found');
|
||||
}
|
||||
if (os.platform() !== 'win32') {
|
||||
throw new Error('Registry20 decryption is only supported on Windows');
|
||||
}
|
||||
|
||||
const decrypted = dpapiUnprotect(registryPath);
|
||||
|
||||
if (decrypted.length < 20) {
|
||||
throw new Error(`Decrypted data too short (got ${decrypted.length} bytes, need 20)`);
|
||||
}
|
||||
|
||||
// Decode payload: header(4) + obfuscated_guid(16)
|
||||
const payload = decrypted.subarray(4, 20);
|
||||
const guidBuf = Buffer.alloc(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const payloadByte = payload[i] ?? 0;
|
||||
guidBuf[i] = (~(payloadByte ^ XOR_KEY)) & 0xFF;
|
||||
}
|
||||
|
||||
return guidBuf.toString('hex');
|
||||
}
|
||||
|
||||
static writeGuid (registryPath: string, guidHex: string): void {
|
||||
if (guidHex.length !== 32) {
|
||||
throw new Error('Invalid GUID length, must be 32 hex chars');
|
||||
}
|
||||
if (os.platform() !== 'win32') {
|
||||
throw new Error('Registry20 encryption is only supported on Windows');
|
||||
}
|
||||
|
||||
const guidBytes = Buffer.from(guidHex, 'hex');
|
||||
const payload = Buffer.alloc(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const guidByte = guidBytes[i] ?? 0;
|
||||
payload[i] = XOR_KEY ^ (~guidByte & 0xFF);
|
||||
}
|
||||
|
||||
const data = Buffer.concat([GUID_HEADER, payload]);
|
||||
|
||||
// Create directory if not exists
|
||||
const dir = path.dirname(registryPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
dpapiProtectAndWrite(registryPath, data);
|
||||
}
|
||||
|
||||
static getBackups (registryPath: string): string[] {
|
||||
const dir = path.dirname(registryPath);
|
||||
const baseName = path.basename(registryPath);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
|
||||
return fs.readdirSync(dir)
|
||||
.filter(f => f.startsWith(`${baseName}.bak.`))
|
||||
.sort()
|
||||
.reverse();
|
||||
}
|
||||
|
||||
static backup (registryPath: string): string {
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
throw new Error('Registry20 does not exist');
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
||||
const backupPath = `${registryPath}.bak.${timestamp}`;
|
||||
fs.copyFileSync(registryPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
static restore (registryPath: string, backupFileName: string): void {
|
||||
const dir = path.dirname(registryPath);
|
||||
const backupPath = path.join(dir, backupFileName);
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
throw new Error('Backup file not found');
|
||||
}
|
||||
fs.copyFileSync(backupPath, registryPath);
|
||||
}
|
||||
|
||||
static delete (registryPath: string): void {
|
||||
if (fs.existsSync(registryPath)) {
|
||||
fs.unlinkSync(registryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Linux machine-info 工具类
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* ROT13 编解码 (自逆运算)
|
||||
* 字母偏移13位,数字和符号不变
|
||||
*/
|
||||
function rot13 (s: string): string {
|
||||
return s.replace(/[a-zA-Z]/g, (c) => {
|
||||
const base = c <= 'Z' ? 65 : 97;
|
||||
return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux 平台 machine-info 文件工具类
|
||||
*
|
||||
* 文件格式 (逆向自 machine_guid_util.cc):
|
||||
* [4字节 BE uint32 长度 N] [N字节 ROT13 编码的 MAC 字符串]
|
||||
* - MAC 格式: xx-xx-xx-xx-xx-xx (17 字符)
|
||||
* - ROT13: 字母偏移13位, 数字和 '-' 不变
|
||||
*
|
||||
* GUID 生成算法:
|
||||
* GUID = MD5( /etc/machine-id + MAC地址 )
|
||||
*/
|
||||
export class MachineInfoUtils {
|
||||
/**
|
||||
* 获取 machine-info 文件路径
|
||||
*/
|
||||
static getMachineInfoPath (dataPath: string): string {
|
||||
return path.join(dataPath, 'nt_qq', 'global', 'nt_data', 'msf', 'machine-info');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 machine-info 文件读取 MAC 地址
|
||||
*/
|
||||
static readMac (machineInfoPath: string): string {
|
||||
if (!fs.existsSync(machineInfoPath)) {
|
||||
throw new Error('machine-info file not found');
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(machineInfoPath);
|
||||
|
||||
if (data.length < 4) {
|
||||
throw new Error(`machine-info data too short: ${data.length} < 4 bytes`);
|
||||
}
|
||||
|
||||
const length = data.readUInt32BE(0);
|
||||
|
||||
if (length >= 18) {
|
||||
throw new Error(`MAC string length abnormal: ${length} >= 18`);
|
||||
}
|
||||
|
||||
if (data.length < 4 + length) {
|
||||
throw new Error(`machine-info data incomplete: need ${4 + length} bytes, got ${data.length}`);
|
||||
}
|
||||
|
||||
const rot13Str = data.subarray(4, 4 + length).toString('ascii');
|
||||
return rot13(rot13Str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MAC 地址写入 machine-info 文件
|
||||
*/
|
||||
static writeMac (machineInfoPath: string, mac: string): void {
|
||||
mac = mac.trim().toLowerCase();
|
||||
|
||||
// 验证 MAC 格式: xx-xx-xx-xx-xx-xx
|
||||
if (!/^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/.test(mac)) {
|
||||
throw new Error('Invalid MAC format, must be xx-xx-xx-xx-xx-xx');
|
||||
}
|
||||
|
||||
const encoded = rot13(mac);
|
||||
const length = encoded.length;
|
||||
const buf = Buffer.alloc(4 + length);
|
||||
buf.writeUInt32BE(length, 0);
|
||||
buf.write(encoded, 4, 'ascii');
|
||||
|
||||
// 确保目录存在
|
||||
const dir = path.dirname(machineInfoPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(machineInfoPath, buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 /etc/machine-id
|
||||
*/
|
||||
static readMachineId (): string {
|
||||
const machineIdPath = '/etc/machine-id';
|
||||
if (!fs.existsSync(machineIdPath)) {
|
||||
throw new Error('/etc/machine-id not found');
|
||||
}
|
||||
return fs.readFileSync(machineIdPath, 'utf-8').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Linux GUID = MD5(machine-id + MAC)
|
||||
*/
|
||||
static computeGuid (machineId: string, mac: string): string {
|
||||
const md5 = crypto.createHash('md5');
|
||||
md5.update(machineId, 'ascii');
|
||||
md5.update(mac, 'ascii');
|
||||
return md5.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份列表
|
||||
*/
|
||||
static getBackups (machineInfoPath: string): string[] {
|
||||
const dir = path.dirname(machineInfoPath);
|
||||
const baseName = path.basename(machineInfoPath);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
|
||||
return fs.readdirSync(dir)
|
||||
.filter(f => f.startsWith(`${baseName}.bak.`))
|
||||
.sort()
|
||||
.reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*/
|
||||
static backup (machineInfoPath: string): string {
|
||||
if (!fs.existsSync(machineInfoPath)) {
|
||||
throw new Error('machine-info file does not exist');
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
||||
const backupPath = `${machineInfoPath}.bak.${timestamp}`;
|
||||
fs.copyFileSync(machineInfoPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复备份
|
||||
*/
|
||||
static restore (machineInfoPath: string, backupFileName: string): void {
|
||||
const dir = path.dirname(machineInfoPath);
|
||||
const backupPath = path.join(dir, backupFileName);
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
throw new Error('Backup file not found');
|
||||
}
|
||||
fs.copyFileSync(backupPath, machineInfoPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 machine-info
|
||||
*/
|
||||
static delete (machineInfoPath: string): void {
|
||||
if (fs.existsSync(machineInfoPath)) {
|
||||
fs.unlinkSync(machineInfoPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
714
packages/napcat-webui-frontend/src/components/guid_manager.tsx
Normal file
714
packages/napcat-webui-frontend/src/components/guid_manager.tsx
Normal file
@@ -0,0 +1,714 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import { Divider } from '@heroui/divider';
|
||||
import { Chip } from '@heroui/chip';
|
||||
import { Listbox, ListboxItem } from '@heroui/listbox';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { MdContentCopy, MdDelete, MdRefresh, MdSave, MdRestorePage, MdBackup } from 'react-icons/md';
|
||||
import MD5 from 'crypto-js/md5';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
|
||||
interface GUIDManagerProps {
|
||||
/** 是否显示重启按钮 */
|
||||
showRestart?: boolean;
|
||||
/** 紧凑模式(用于弹窗场景) */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const GUIDManager: React.FC<GUIDManagerProps> = ({ showRestart = true, compact = false }) => {
|
||||
const dialog = useDialog();
|
||||
|
||||
// 平台检测
|
||||
const [platform, setPlatform] = useState<string>('');
|
||||
const isWindows = platform === 'win32';
|
||||
const isMac = platform === 'darwin';
|
||||
const isLinux = platform !== '' && platform !== 'win32' && platform !== 'darwin';
|
||||
const platformDetected = platform !== '';
|
||||
|
||||
// Windows 状态
|
||||
const [currentGUID, setCurrentGUID] = useState<string>('');
|
||||
const [inputGUID, setInputGUID] = useState<string>('');
|
||||
const [backups, setBackups] = useState<string[]>([]);
|
||||
|
||||
// Linux 状态
|
||||
const [currentMAC, setCurrentMAC] = useState<string>('');
|
||||
const [inputMAC, setInputMAC] = useState<string>('');
|
||||
const [machineId, setMachineId] = useState<string>('');
|
||||
const [linuxBackups, setLinuxBackups] = useState<string[]>([]);
|
||||
|
||||
// 通用状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
|
||||
const isValidGUID = (guid: string) => /^[0-9a-fA-F]{32}$/.test(guid);
|
||||
const isValidMAC = (mac: string) => /^[0-9a-fA-F]{2}(-[0-9a-fA-F]{2}){5}$/.test(mac.trim().toLowerCase());
|
||||
|
||||
// 前端实时计算 Linux GUID = MD5(machine-id + MAC)
|
||||
const computedLinuxGUID = useMemo(() => {
|
||||
if (!isLinux) return '';
|
||||
const mac = inputMAC.trim().toLowerCase();
|
||||
if (!machineId && !mac) return '';
|
||||
return MD5(machineId + mac).toString();
|
||||
}, [isLinux, machineId, inputMAC]);
|
||||
|
||||
// 当前生效的 GUID (基于已保存的 MAC)
|
||||
const currentLinuxGUID = useMemo(() => {
|
||||
if (!isLinux || !currentMAC) return '';
|
||||
return MD5(machineId + currentMAC).toString();
|
||||
}, [isLinux, machineId, currentMAC]);
|
||||
|
||||
// 检测平台
|
||||
const fetchPlatform = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getPlatformInfo();
|
||||
setPlatform(data.platform);
|
||||
} catch {
|
||||
// 如果获取失败,默认 win32 向后兼容
|
||||
setPlatform('win32');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Windows: 获取 GUID
|
||||
const fetchGUID = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await QQManager.getDeviceGUID();
|
||||
setCurrentGUID(data.guid);
|
||||
setInputGUID(data.guid);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
setCurrentGUID('');
|
||||
setInputGUID('');
|
||||
if (!msg.includes('not found')) {
|
||||
toast.error(`获取 GUID 失败: ${msg}`);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Windows: 获取备份
|
||||
const fetchBackups = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getGUIDBackups();
|
||||
setBackups(data);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Linux: 获取 MAC + machine-id
|
||||
const fetchLinuxInfo = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [macData, midData] = await Promise.all([
|
||||
QQManager.getLinuxMAC().catch(() => ({ mac: '' })),
|
||||
QQManager.getLinuxMachineId().catch(() => ({ machineId: '' })),
|
||||
]);
|
||||
setCurrentMAC(macData.mac);
|
||||
setInputMAC(macData.mac);
|
||||
setMachineId(midData.machineId);
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`获取设备信息失败: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Linux: 获取备份
|
||||
const fetchLinuxBackups = useCallback(async () => {
|
||||
try {
|
||||
const data = await QQManager.getLinuxMachineInfoBackups();
|
||||
setLinuxBackups(data);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPlatform();
|
||||
}, [fetchPlatform]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!platformDetected) return;
|
||||
if (isWindows) {
|
||||
fetchGUID();
|
||||
fetchBackups();
|
||||
} else {
|
||||
fetchLinuxInfo();
|
||||
fetchLinuxBackups();
|
||||
}
|
||||
}, [platformDetected, isWindows, fetchGUID, fetchBackups, fetchLinuxInfo, fetchLinuxBackups]);
|
||||
|
||||
// ========== Windows 操作 ==========
|
||||
|
||||
const handleCopy = () => {
|
||||
const guid = isLinux ? currentLinuxGUID : currentGUID;
|
||||
if (guid) {
|
||||
navigator.clipboard.writeText(guid);
|
||||
toast.success('已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isValidGUID(inputGUID)) {
|
||||
toast.error('GUID 格式无效,需要 32 位十六进制字符');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await QQManager.setDeviceGUID(inputGUID);
|
||||
setCurrentGUID(inputGUID);
|
||||
toast.success('GUID 已设置,重启后生效');
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`设置 GUID 失败: ${msg}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除 Registry20 后,QQ 将在下次启动时生成新的设备标识。确定要删除吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.resetDeviceID();
|
||||
setCurrentGUID('');
|
||||
setInputGUID('');
|
||||
toast.success('已删除,重启后生效');
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`删除失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBackup = async () => {
|
||||
try {
|
||||
await QQManager.createGUIDBackup();
|
||||
toast.success('备份已创建');
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`备份失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = (backupName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认恢复',
|
||||
content: `确定要从备份 "${backupName}" 恢复吗?当前的 Registry20 将被覆盖。`,
|
||||
confirmText: '恢复',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.restoreGUIDBackup(backupName);
|
||||
toast.success('已恢复,重启后生效');
|
||||
await fetchGUID();
|
||||
await fetchBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`恢复失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ========== Linux 操作 ==========
|
||||
|
||||
const handleLinuxSaveMAC = async () => {
|
||||
const mac = inputMAC.trim().toLowerCase();
|
||||
if (!isValidMAC(mac)) {
|
||||
toast.error('MAC 格式无效,需要 xx-xx-xx-xx-xx-xx 格式');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await QQManager.setLinuxMAC(mac);
|
||||
setCurrentMAC(mac);
|
||||
toast.success('MAC 已设置,重启后生效');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`设置 MAC 失败: ${msg}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxCopyMAC = () => {
|
||||
if (currentMAC) {
|
||||
navigator.clipboard.writeText(currentMAC);
|
||||
toast.success('MAC 已复制到剪贴板');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxDelete = () => {
|
||||
dialog.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除 machine-info 后,QQ 将在下次启动时生成新的设备标识。确定要删除吗?',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.resetLinuxDeviceID();
|
||||
setCurrentMAC('');
|
||||
setInputMAC('');
|
||||
toast.success('已删除,重启后生效');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`删除失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLinuxBackup = async () => {
|
||||
try {
|
||||
await QQManager.createLinuxMachineInfoBackup();
|
||||
toast.success('备份已创建');
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`备份失败: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinuxRestore = (backupName: string) => {
|
||||
dialog.confirm({
|
||||
title: '确认恢复',
|
||||
content: `确定要从备份 "${backupName}" 恢复吗?当前的 machine-info 将被覆盖。`,
|
||||
confirmText: '恢复',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await QQManager.restoreLinuxMachineInfoBackup(backupName);
|
||||
toast.success('已恢复,重启后生效');
|
||||
await fetchLinuxInfo();
|
||||
await fetchLinuxBackups();
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`恢复失败: ${msg}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 重启 ==========
|
||||
|
||||
const handleRestart = () => {
|
||||
dialog.confirm({
|
||||
title: '确认重启',
|
||||
content: '确定要重启 NapCat 吗?这将导致当前连接断开。',
|
||||
confirmText: '重启',
|
||||
cancelText: '取消',
|
||||
onConfirm: async () => {
|
||||
setRestarting(true);
|
||||
try {
|
||||
await QQManager.restartNapCat();
|
||||
toast.success('重启指令已发送');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`重启失败: ${msg}`);
|
||||
} finally {
|
||||
setRestarting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading || !platformDetected) {
|
||||
return (
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<Spinner label='加载中...' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== macOS 不支持 ==========
|
||||
if (isMac) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
<div className='flex flex-col items-center justify-center py-8 gap-2'>
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
macOS 平台暂不支持 GUID 管理
|
||||
</Chip>
|
||||
<div className='text-xs text-default-400'>
|
||||
该功能仅适用于 Windows 和 Linux 平台
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Linux 渲染 ==========
|
||||
if (isLinux) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
{/* 当前设备信息 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前设备 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentLinuxGUID ? (
|
||||
<Chip variant='flat' color='primary' className='font-mono text-xs max-w-full'>
|
||||
{currentLinuxGUID}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentLinuxGUID && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleCopy}
|
||||
aria-label='复制GUID'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={fetchLinuxInfo}
|
||||
aria-label='刷新'
|
||||
>
|
||||
<MdRefresh size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
GUID = MD5(machine-id + MAC),修改 MAC 即可改变 GUID
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* machine-id 显示 */}
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='text-sm font-medium text-default-700'>Machine ID</div>
|
||||
<Chip variant='flat' color='default' className='font-mono text-xs max-w-full'>
|
||||
{machineId || '未知'}
|
||||
</Chip>
|
||||
<div className='text-xs text-default-400'>
|
||||
来自 /etc/machine-id,不可修改
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 当前 MAC 显示 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前 MAC 地址</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentMAC ? (
|
||||
<Chip variant='flat' color='secondary' className='font-mono text-xs max-w-full'>
|
||||
{currentMAC}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentMAC && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleLinuxCopyMAC}
|
||||
aria-label='复制MAC'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 编辑 MAC 地址 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>设置 MAC 地址</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='bordered'
|
||||
placeholder='xx-xx-xx-xx-xx-xx'
|
||||
value={inputMAC}
|
||||
onValueChange={setInputMAC}
|
||||
isInvalid={inputMAC.length > 0 && !isValidMAC(inputMAC)}
|
||||
errorMessage={inputMAC.length > 0 && !isValidMAC(inputMAC) ? '格式: xx-xx-xx-xx-xx-xx' : undefined}
|
||||
classNames={{
|
||||
input: 'font-mono text-sm',
|
||||
}}
|
||||
maxLength={17}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 实时 GUID 预览 */}
|
||||
{inputMAC && isValidMAC(inputMAC) && (
|
||||
<div className='flex flex-col gap-1 p-2 rounded-lg bg-default-100'>
|
||||
<div className='text-xs font-medium text-default-500'>预览 GUID</div>
|
||||
<div className='font-mono text-xs text-primary break-all'>
|
||||
{computedLinuxGUID}
|
||||
</div>
|
||||
{computedLinuxGUID !== currentLinuxGUID && (
|
||||
<div className='text-xs text-warning-500'>
|
||||
与当前 GUID 不同,保存后重启生效
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
isLoading={saving}
|
||||
isDisabled={!isValidMAC(inputMAC) || inputMAC.trim().toLowerCase() === currentMAC}
|
||||
onPress={handleLinuxSaveMAC}
|
||||
startContent={<MdSave size={16} />}
|
||||
>
|
||||
保存 MAC
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='flat'
|
||||
isDisabled={!currentMAC}
|
||||
onPress={handleLinuxDelete}
|
||||
startContent={<MdDelete size={16} />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='secondary'
|
||||
variant='flat'
|
||||
isDisabled={!currentMAC}
|
||||
onPress={handleLinuxBackup}
|
||||
startContent={<MdBackup size={16} />}
|
||||
>
|
||||
手动备份
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
修改 MAC 后 GUID 将变化,需重启 NapCat 才能生效,操作前会自动备份
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备份恢复 */}
|
||||
{linuxBackups.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>
|
||||
备份列表
|
||||
<span className='text-xs text-default-400 ml-2'>(点击恢复)</span>
|
||||
</div>
|
||||
<div className='max-h-[160px] overflow-y-auto rounded-lg border border-default-200'>
|
||||
<Listbox
|
||||
aria-label='备份列表'
|
||||
selectionMode='none'
|
||||
onAction={(key) => handleLinuxRestore(key as string)}
|
||||
>
|
||||
{linuxBackups.map((name) => (
|
||||
<ListboxItem
|
||||
key={name}
|
||||
startContent={<MdRestorePage size={16} className='text-default-400' />}
|
||||
className='font-mono text-xs'
|
||||
>
|
||||
{name}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 重启 */}
|
||||
{showRestart && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
size='sm'
|
||||
color='warning'
|
||||
variant='flat'
|
||||
isLoading={restarting}
|
||||
onPress={handleRestart}
|
||||
startContent={<MdRefresh size={16} />}
|
||||
>
|
||||
重启 NapCat
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Windows 渲染 ==========
|
||||
return (
|
||||
<div className={`flex flex-col gap-${compact ? '3' : '4'}`}>
|
||||
{/* 当前 GUID 显示 */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>当前设备 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
{currentGUID ? (
|
||||
<Chip variant='flat' color='primary' className='font-mono text-xs max-w-full'>
|
||||
{currentGUID}
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip variant='flat' color='warning' className='text-xs'>
|
||||
未设置 / 不存在
|
||||
</Chip>
|
||||
)}
|
||||
{currentGUID && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={handleCopy}
|
||||
aria-label='复制GUID'
|
||||
>
|
||||
<MdContentCopy size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
size='sm'
|
||||
variant='light'
|
||||
onPress={fetchGUID}
|
||||
aria-label='刷新'
|
||||
>
|
||||
<MdRefresh size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 设置 GUID */}
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>设置 GUID</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='bordered'
|
||||
placeholder='输入32位十六进制 GUID'
|
||||
value={inputGUID}
|
||||
onValueChange={setInputGUID}
|
||||
isInvalid={inputGUID.length > 0 && !isValidGUID(inputGUID)}
|
||||
errorMessage={inputGUID.length > 0 && !isValidGUID(inputGUID) ? '需要32位十六进制字符' : undefined}
|
||||
classNames={{
|
||||
input: 'font-mono text-sm',
|
||||
}}
|
||||
maxLength={32}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
isLoading={saving}
|
||||
isDisabled={!isValidGUID(inputGUID) || inputGUID === currentGUID}
|
||||
onPress={handleSave}
|
||||
startContent={<MdSave size={16} />}
|
||||
>
|
||||
保存 GUID
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='flat'
|
||||
isDisabled={!currentGUID}
|
||||
onPress={handleDelete}
|
||||
startContent={<MdDelete size={16} />}
|
||||
>
|
||||
删除 GUID
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
color='secondary'
|
||||
variant='flat'
|
||||
isDisabled={!currentGUID}
|
||||
onPress={handleBackup}
|
||||
startContent={<MdBackup size={16} />}
|
||||
>
|
||||
手动备份
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-default-400'>
|
||||
修改或删除 GUID 后需重启 NapCat 才能生效,操作前会自动备份
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备份恢复 */}
|
||||
{backups.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='text-sm font-medium text-default-700'>
|
||||
备份列表
|
||||
<span className='text-xs text-default-400 ml-2'>(点击恢复)</span>
|
||||
</div>
|
||||
<div className='max-h-[160px] overflow-y-auto rounded-lg border border-default-200'>
|
||||
<Listbox
|
||||
aria-label='备份列表'
|
||||
selectionMode='none'
|
||||
onAction={(key) => handleRestore(key as string)}
|
||||
>
|
||||
{backups.map((name) => (
|
||||
<ListboxItem
|
||||
key={name}
|
||||
startContent={<MdRestorePage size={16} className='text-default-400' />}
|
||||
className='font-mono text-xs'
|
||||
>
|
||||
{name}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 重启 */}
|
||||
{showRestart && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
size='sm'
|
||||
color='warning'
|
||||
variant='flat'
|
||||
isLoading={restarting}
|
||||
onPress={handleRestart}
|
||||
startContent={<MdRefresh size={16} />}
|
||||
>
|
||||
重启 NapCat
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GUIDManager;
|
||||
@@ -101,4 +101,100 @@ export default class QQManager {
|
||||
passwordMd5,
|
||||
});
|
||||
}
|
||||
|
||||
public static async resetDeviceID () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/ResetDeviceID');
|
||||
}
|
||||
|
||||
public static async restartNapCat () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestartNapCat');
|
||||
}
|
||||
|
||||
public static async getDeviceGUID () {
|
||||
const data = await serverRequest.post<ServerResponse<{ guid: string; }>>('/QQLogin/GetDeviceGUID');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async setDeviceGUID (guid: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetDeviceGUID', { guid });
|
||||
}
|
||||
|
||||
public static async getGUIDBackups () {
|
||||
const data = await serverRequest.post<ServerResponse<string[]>>('/QQLogin/GetGUIDBackups');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async restoreGUIDBackup (backupName: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestoreGUIDBackup', { backupName });
|
||||
}
|
||||
|
||||
public static async createGUIDBackup () {
|
||||
const data = await serverRequest.post<ServerResponse<{ path: string; }>>('/QQLogin/CreateGUIDBackup');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 平台信息 & Linux GUID 管理
|
||||
// ============================================================
|
||||
|
||||
public static async getPlatformInfo () {
|
||||
const data = await serverRequest.post<ServerResponse<{ platform: string; }>>('/QQLogin/GetPlatformInfo');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async getLinuxMAC () {
|
||||
const data = await serverRequest.post<ServerResponse<{ mac: string; }>>('/QQLogin/GetLinuxMAC');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async setLinuxMAC (mac: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetLinuxMAC', { mac });
|
||||
}
|
||||
|
||||
public static async getLinuxMachineId () {
|
||||
const data = await serverRequest.post<ServerResponse<{ machineId: string; }>>('/QQLogin/GetLinuxMachineId');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async computeLinuxGUID (mac?: string, machineId?: string) {
|
||||
const data = await serverRequest.post<ServerResponse<{ guid: string; machineId: string; mac: string; }>>('/QQLogin/ComputeLinuxGUID', { mac, machineId });
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async getLinuxMachineInfoBackups () {
|
||||
const data = await serverRequest.post<ServerResponse<string[]>>('/QQLogin/GetLinuxMachineInfoBackups');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async createLinuxMachineInfoBackup () {
|
||||
const data = await serverRequest.post<ServerResponse<{ path: string; }>>('/QQLogin/CreateLinuxMachineInfoBackup');
|
||||
return data.data.data;
|
||||
}
|
||||
|
||||
public static async restoreLinuxMachineInfoBackup (backupName: string) {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/RestoreLinuxMachineInfoBackup', { backupName });
|
||||
}
|
||||
|
||||
public static async resetLinuxDeviceID () {
|
||||
await serverRequest.post<ServerResponse<null>>('/QQLogin/ResetLinuxDeviceID');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NapCat 配置管理
|
||||
// ============================================================
|
||||
|
||||
public static async getNapCatConfig () {
|
||||
const { data } = await serverRequest.get<ServerResponse<NapCatConfig>>(
|
||||
'/NapCatConfig/GetConfig'
|
||||
);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public static async setNapCatConfig (config: Partial<NapCatConfig>) {
|
||||
await serverRequest.post<ServerResponse<null>>(
|
||||
'/NapCatConfig/SetConfig',
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons';
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import SwitchCard from '@/components/switch_card';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
|
||||
interface BypassFormData {
|
||||
hook: boolean;
|
||||
window: boolean;
|
||||
module: boolean;
|
||||
process: boolean;
|
||||
container: boolean;
|
||||
js: boolean;
|
||||
}
|
||||
|
||||
const defaultBypass: BypassFormData = {
|
||||
hook: true,
|
||||
window: true,
|
||||
module: true,
|
||||
process: true,
|
||||
container: true,
|
||||
js: true,
|
||||
};
|
||||
|
||||
const BypassConfigCard = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
setValue,
|
||||
} = useForm<BypassFormData>({
|
||||
defaultValues: defaultBypass,
|
||||
});
|
||||
|
||||
const loadConfig = async (showTip = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const config = await QQManager.getNapCatConfig();
|
||||
const bypass = config.bypass ?? defaultBypass;
|
||||
setValue('hook', bypass.hook ?? true);
|
||||
setValue('window', bypass.window ?? true);
|
||||
setValue('module', bypass.module ?? true);
|
||||
setValue('process', bypass.process ?? true);
|
||||
setValue('container', bypass.container ?? true);
|
||||
setValue('js', bypass.js ?? true);
|
||||
if (showTip) toast.success('刷新成功');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`获取配置失败: ${msg}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await QQManager.setNapCatConfig({ bypass: data });
|
||||
toast.success('保存成功,重启后生效');
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message;
|
||||
toast.error(`保存失败: ${msg}`);
|
||||
}
|
||||
});
|
||||
|
||||
const onReset = () => {
|
||||
loadConfig();
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
await loadConfig(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
if (loading) return <PageLoading loading />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>反检测配置 - NapCat WebUI</title>
|
||||
<div className='flex flex-col gap-1 mb-2'>
|
||||
<h3 className='text-lg font-semibold text-default-700'>反检测开关配置</h3>
|
||||
<p className='text-sm text-default-500'>
|
||||
控制 Napi2Native 模块的各项反检测功能,修改后需重启生效。
|
||||
</p>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name='hook'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Hook'
|
||||
description='hook特征隐藏'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='window'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Window'
|
||||
description='窗口伪造'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='module'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Module'
|
||||
description='加载模块隐藏'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='process'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Process'
|
||||
description='进程反检测'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='container'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='Container'
|
||||
description='容器反检测'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='js'
|
||||
render={({ field }) => (
|
||||
<SwitchCard
|
||||
{...field}
|
||||
label='JS'
|
||||
description='JS反检测'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SaveButtons
|
||||
onSubmit={onSubmit}
|
||||
reset={onReset}
|
||||
isSubmitting={isSubmitting}
|
||||
refresh={onRefresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BypassConfigCard;
|
||||
@@ -14,6 +14,7 @@ import SSLConfigCard from './ssl';
|
||||
import ThemeConfigCard from './theme';
|
||||
import WebUIConfigCard from './webui';
|
||||
import BackupConfigCard from './backup';
|
||||
import BypassConfigCard from './bypass';
|
||||
|
||||
export interface ConfigPageProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -114,6 +115,11 @@ export default function ConfigPage () {
|
||||
<BackupConfigCard />
|
||||
</ConfigPageItem>
|
||||
</Tab>
|
||||
<Tab title='Bypass配置' key='bypass'>
|
||||
<ConfigPageItem>
|
||||
<BypassConfigCard />
|
||||
</ConfigPageItem>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Input } from '@heroui/input';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Divider } from '@heroui/divider';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons';
|
||||
import GUIDManager from '@/components/guid_manager';
|
||||
import PageLoading from '@/components/page_loading';
|
||||
|
||||
import QQManager from '@/controllers/qq_manager';
|
||||
@@ -131,6 +133,14 @@ const LoginConfigCard = () => {
|
||||
重启进程将关闭当前 Worker 进程,等待 3 秒后启动新进程
|
||||
</div>
|
||||
</div>
|
||||
<Divider className='mt-6' />
|
||||
<div className='flex-shrink-0 w-full mt-4'>
|
||||
<div className='mb-3 text-sm text-default-600'>设备 GUID 管理</div>
|
||||
<div className='text-xs text-default-400 mb-3'>
|
||||
GUID 是设备登录唯一识别码,存储在 Registry20 文件中。修改后需重启生效。
|
||||
</div>
|
||||
<GUIDManager showRestart={false} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,40 +66,49 @@ export default function PluginPage () {
|
||||
content: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>确定要卸载插件「{plugin.name}」吗? 此操作不可恢复。</p>
|
||||
<p className="text-small text-default-500">如果插件创建了数据文件,是否一并删除?</p>
|
||||
<p className="text-small text-default-500">如果插件创建了配置文件,是否一并删除?</p>
|
||||
</div>
|
||||
),
|
||||
// This 'dialog' utility might not support returning a value from UI interacting.
|
||||
// We might need to implement a custom confirmation flow if we want a checkbox.
|
||||
// Alternatively, use two buttons? "Uninstall & Clean", "Uninstall Only"?
|
||||
// Standard dialog usually has Confirm/Cancel.
|
||||
// Let's stick to a simpler "Uninstall" and then maybe a second prompt? Or just clean data?
|
||||
// User requested: "Uninstall prompts whether to clean data".
|
||||
// Let's use `window.confirm` for the second step or assume `dialog.confirm` is flexible enough?
|
||||
// I will implement a two-step confirmation or try to modify the dialog hook if visible (not visible here).
|
||||
// Let's use a standard `window.confirm` for the data cleanup question if the custom dialog doesn't support complex return.
|
||||
// Better: Inside onConfirm, ask again?
|
||||
onConfirm: async () => {
|
||||
// Ask for data cleanup
|
||||
// Since we are in an async callback, we can use another dialog or confirm.
|
||||
// Native confirm is ugly but works reliably for logic:
|
||||
const cleanData = window.confirm(`是否同时清理插件「${plugin.name}」的数据文件?\n点击“确定”清理数据,点击“取消”仅卸载插件。`);
|
||||
|
||||
const loadingToast = toast.loading('卸载中...');
|
||||
try {
|
||||
await PluginManager.uninstallPlugin(plugin.id, cleanData);
|
||||
toast.success('卸载成功', { id: loadingToast });
|
||||
loadPlugins();
|
||||
resolve();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message, { id: loadingToast });
|
||||
reject(e);
|
||||
}
|
||||
dialog.confirm({
|
||||
title: '删除配置',
|
||||
content: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>是否同时清理插件「{plugin.name}」的配置文件?</p>
|
||||
<div className="text-small text-default-500">
|
||||
<p>配置目录: config/plugins/{plugin.id}</p>
|
||||
<p>点击"确定"清理配置,点击"取消"仅卸载插件。</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
confirmText: '清理并卸载',
|
||||
cancelText: '仅卸载',
|
||||
onConfirm: async () => {
|
||||
await performUninstall(true);
|
||||
},
|
||||
onCancel: async () => {
|
||||
await performUninstall(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
const performUninstall = async (cleanData: boolean) => {
|
||||
const loadingToast = toast.loading('卸载中...');
|
||||
try {
|
||||
await PluginManager.uninstallPlugin(plugin.id, cleanData);
|
||||
toast.success('卸载成功', { id: loadingToast });
|
||||
loadPlugins();
|
||||
resolve();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message, { id: loadingToast });
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { MdSettings } from 'react-icons/md';
|
||||
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import GUIDManager from '@/components/guid_manager';
|
||||
import Modal from '@/components/modal';
|
||||
|
||||
import HoverEffectCard from '@/components/effect_card';
|
||||
import { title } from '@/components/primitives';
|
||||
@@ -174,6 +177,8 @@ export default function QQLoginPage () {
|
||||
}
|
||||
};
|
||||
|
||||
const [showGUIDManager, setShowGUIDManager] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
onUpdateQrCode();
|
||||
@@ -210,7 +215,12 @@ export default function QQLoginPage () {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ThemeSwitch className='absolute right-4 top-4' />
|
||||
<div className='absolute right-4 top-4 flex items-center gap-2'>
|
||||
<Button isIconOnly variant="light" aria-label="Settings" onPress={() => setShowGUIDManager(true)}>
|
||||
<MdSettings size={22} />
|
||||
</Button>
|
||||
<ThemeSwitch />
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className='flex gap-5 p-10 pt-0'>
|
||||
@@ -266,6 +276,15 @@ export default function QQLoginPage () {
|
||||
</HoverEffectCard>
|
||||
</motion.div>
|
||||
</PureLayout>
|
||||
{showGUIDManager && (
|
||||
<Modal
|
||||
title='设备 GUID 管理'
|
||||
content={<GUIDManager compact showRestart />}
|
||||
size='lg'
|
||||
hideFooter
|
||||
onClose={() => setShowGUIDManager(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
19
packages/napcat-webui-frontend/src/types/napcat_conf.d.ts
vendored
Normal file
19
packages/napcat-webui-frontend/src/types/napcat_conf.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
interface BypassOptions {
|
||||
hook: boolean;
|
||||
window: boolean;
|
||||
module: boolean;
|
||||
process: boolean;
|
||||
container: boolean;
|
||||
js: boolean;
|
||||
}
|
||||
|
||||
interface NapCatConfig {
|
||||
fileLog: boolean;
|
||||
consoleLog: boolean;
|
||||
fileLogLevel: string;
|
||||
consoleLogLevel: string;
|
||||
packetBackend: string;
|
||||
packetServer: string;
|
||||
o3HookMode: number;
|
||||
bypass?: BypassOptions;
|
||||
}
|
||||
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@@ -128,8 +128,17 @@ importers:
|
||||
specifier: ^22.0.1
|
||||
version: 22.19.1
|
||||
|
||||
packages/napcat-dpapi:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.1
|
||||
version: 22.19.1
|
||||
|
||||
packages/napcat-framework:
|
||||
dependencies:
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
napcat-adapter:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-adapter
|
||||
@@ -351,6 +360,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.0.1
|
||||
version: 22.19.1
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
napcat-vite:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-vite
|
||||
@@ -441,6 +453,9 @@ importers:
|
||||
napcat-common:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-common
|
||||
napcat-dpapi:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-dpapi
|
||||
napcat-pty:
|
||||
specifier: workspace:*
|
||||
version: link:../napcat-pty
|
||||
@@ -1898,89 +1913,105 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@@ -2731,56 +2762,67 @@ packages:
|
||||
resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.2':
|
||||
resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.2':
|
||||
resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.2':
|
||||
resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.2':
|
||||
resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.2':
|
||||
resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.2':
|
||||
resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.2':
|
||||
resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.2':
|
||||
resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.2':
|
||||
resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.2':
|
||||
resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.2':
|
||||
resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==}
|
||||
@@ -2855,24 +2897,28 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.15.1':
|
||||
resolution: {integrity: sha512-fKzP9mRQGbhc5QhJPIsqKNNX/jyWrZgBxmo3Nz1SPaepfCUc7RFmtcJQI5q8xAun3XabXjh90wqcY/OVyg2+Kg==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.15.1':
|
||||
resolution: {integrity: sha512-ZLjMi138uTJxb+1wzo4cB8mIbJbAsSLWRNeHc1g1pMvkERPWOGlem+LEYkkzaFzCNv1J8aKcL653Vtw8INHQeg==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.15.1':
|
||||
resolution: {integrity: sha512-jvSI1IdsIYey5kOITzyajjofXOOySVitmLxb45OPUjoNojql4sDojvlW5zoHXXFePdA6qAX4Y6KbzAOV3T3ctA==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.15.1':
|
||||
resolution: {integrity: sha512-X/FcDtNrDdY9r4FcXHt9QxUqC/2FbQdvZobCKHlHe8vTSKhUHOilWl5EBtkFVfsEs4D5/yAri9e3bJbwyBhhBw==}
|
||||
@@ -3237,41 +3283,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
|
||||
Reference in New Issue
Block a user