Merge branch 'refs/heads/main' into laana

# Conflicts:
#	package.json
#	src/core/index.ts
This commit is contained in:
Wesley F. Young 2024-10-13 10:26:36 +08:00
commit 9427cb4688
23 changed files with 344 additions and 14 deletions

View File

@ -34,7 +34,7 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl) [Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl)
## 猫猫朋友 ## 猫猫朋友
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot) 提供部分参考 感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持

View File

@ -4,7 +4,7 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "2.6.24", "version": "2.6.27",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "2.6.24", "version": "2.6.27",
"scripts": { "scripts": {
"build:framework": "vite build --mode framework", "build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell", "build:shell": "vite build --mode shell",

View File

@ -24,8 +24,10 @@ export class LRUCache<K, V> {
} else if (this.cache.size >= this.capacity) { } else if (this.cache.size >= this.capacity) {
// If the cache is full, remove the least recently used key (the first one in the map) // If the cache is full, remove the least recently used key (the first one in the map)
const firstKey = this.cache.keys().next().value; const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey); this.cache.delete(firstKey);
} }
}
this.cache.set(key, value); this.cache.set(key, value);
} }
} }

View File

@ -61,7 +61,7 @@ export class RequestUtil {
const options = { const options = {
hostname: option.hostname, hostname: option.hostname,
port: option.port, port: option.port,
path: option.href, path: option.pathname + option.search,
method: method, method: method,
headers: headers, headers: headers,
}; };

View File

@ -1 +1 @@
export const napCatVersion = '2.6.24'; export const napCatVersion = '2.6.27';

View File

@ -9,10 +9,22 @@ import {
MemberExtSourceType, MemberExtSourceType,
NapCatCore, NapCatCore,
} from '@/core'; } from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper'; import { isNumeric, sleep, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique'; import { LimitedHashTable } from '@/common/message-unique';
import { NTEventWrapper } from '@/common/event'; import { NTEventWrapper } from '@/common/event';
import { encodeGroupPoke } from '../proto/Poke';
import { randomUUID } from 'crypto';
import { RequestUtil } from '@/common/request';
interface recvPacket
{
type: string,//仅recv
trace_id_md5?: string,
data: {
seq: number,
hex_data: string,
cmd: string
}
}
export class NTQQGroupApi { export class NTQQGroupApi {
context: InstanceContext; context: InstanceContext;
core: NapCatCore; core: NapCatCore;
@ -20,6 +32,7 @@ export class NTQQGroupApi {
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>(); groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
groups: Group[] = []; groups: Group[] = [];
essenceLRU = new LimitedHashTable<number, string>(1000); essenceLRU = new LimitedHashTable<number, string>(1000);
session: any;
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
@ -33,6 +46,11 @@ export class NTQQGroupApi {
this.groupCache.set(group.groupCode, group); this.groupCache.set(group.groupCode, group);
} }
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`); this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
//console.log('pid', process.pid);
// this.session = await frida.attach(process.pid);
// setTimeout(async () => {
// this.sendPocketRkey();
// }, 10000);
} }
async getCoreAndBaseInfo(uids: string[]) { async getCoreAndBaseInfo(uids: string[]) {
return await this.core.eventWrapper.callNoListenerEvent( return await this.core.eventWrapper.callNoListenerEvent(
@ -41,6 +59,17 @@ export class NTQQGroupApi {
uids, uids,
); );
} }
async sendPocketRkey() {
let hex = '08E7A00210CA01221D0A130A05080110CA011206A80602B006011A0208022206080A081408022A006001';
let ret = await this.core.apis.PacketApi.sendPacket('OidbSvcTrpcTcp.0x9067_202', hex, true);
//console.log('ret: ', ret);
}
async sendPacketPoke(group: number, peer: number) {
let data = encodeGroupPoke(group, peer);
let hex = Buffer.from(data).toString('hex');
let retdata = await this.core.apis.PacketApi.sendPacket('OidbSvcTrpcTcp.0xed3_1', hex, false);
//console.log('sendPacketPoke', retdata);
}
async fetchGroupEssenceList(groupCode: string) { async fetchGroupEssenceList(groupCode: string) {
const pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; const pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().fetchGroupEssenceList({ return this.context.session.getGroupService().fetchGroupEssenceList({

68
src/core/apis/packet.ts Normal file
View File

@ -0,0 +1,68 @@
import { InstanceContext, NapCatCore } from '..';
import { RequestUtil } from '@/common/request';
import offset from '@/core/external/offset.json';
import * as crypto from 'crypto';
import { PacketClient } from '../helper/packet';
interface OffsetType {
[key: string]: {
recv: string;
send: string;
};
}
const typedOffset: OffsetType = offset;
export class NTQQPacketApi {
context: InstanceContext;
core: NapCatCore;
serverUrl: string | undefined;
qqversion: string | undefined;
isInit: boolean = false;
PacketClient: PacketClient | undefined;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
let config = this.core.configLoader.configData;
if (config && config.packetServer && config.packetServer.length > 0) {
let serverurl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086';
this.InitSendPacket(serverurl, this.context.basicInfoWrapper.getFullQQVesion())
.then()
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
}
}
async InitSendPacket(serverUrl: string, qqversion: string) {
this.serverUrl = serverUrl;
this.qqversion = qqversion;
let offsetTable: OffsetType = offset;
if (!offsetTable[qqversion]) return false;
let url = 'ws://' + this.serverUrl + '/ws';
this.PacketClient = new PacketClient(url, this.core.context.logger);
await this.PacketClient.connect();
await this.PacketClient.init(process.pid, offsetTable[qqversion].recv, offsetTable[qqversion].send);
this.isInit = true;
return this.isInit;
}
randText(len: number) {
let text = '';
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
async sendPacket(cmd: string, data: string, rsp = false) {
// wtfk tx
// 校验失败和异常 可能返回undefined
return new Promise((resolve, reject) => {
if (!this.isInit || !this.PacketClient?.isConnected) {
this.core.context.logger.logError('PacketClient is not init');
return undefined;
}
let md5 = crypto.createHash('md5').update(data).digest('hex');
let trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
this.PacketClient?.sendCommand(cmd, data, trace_id, rsp, 5000, async () => {
await this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
}).then((res) => resolve(res)).catch((e) => reject(e));
});
}
}

View File

@ -38,5 +38,9 @@
"6.9.56-28418": { "6.9.56-28418": {
"appid": 537249367, "appid": 537249367,
"qua": "V1_MAC_NQ_6.9.56_28418_GW_B" "qua": "V1_MAC_NQ_6.9.56_28418_GW_B"
},
"9.9.15-28498":{
"appid": 537249321,
"qua": "V1_WIN_NQ_9.9.15_28498_GW_B"
} }
} }

View File

@ -2,5 +2,6 @@
"fileLog": true, "fileLog": true,
"consoleLog": true, "consoleLog": true,
"fileLogLevel": "debug", "fileLogLevel": "debug",
"consoleLogLevel": "info" "consoleLogLevel": "info",
"packetServer": ""
} }

14
src/core/external/offset.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"3.2.12-28418": {
"recv": "A0723E0",
"send": "A06EAE0"
},
"9.9.15-28418": {
"recv": "37A9004",
"send": "37A4BD0"
},
"9.9.15-28498": {
"recv": "37A9004",
"send": "37A4BD0"
}
}

123
src/core/helper/packet.ts Normal file
View File

@ -0,0 +1,123 @@
import { LogWrapper } from "@/common/log";
import { LRUCache } from "@/common/lru-cache";
import WebSocket from "ws";
import { createHash } from "crypto";
export class PacketClient {
private websocket: WebSocket | undefined;
public isConnected: boolean = false;
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
//trace_id-type callback
private cb = new LRUCache<string, any>(500);
constructor(private url: string, public logger: LogWrapper) { }
connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.logger.log.bind(this.logger)(`Attempting to connect to ${this.url}`);
this.websocket = new WebSocket(this.url);
this.websocket.on('error', (err) => this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message));
this.websocket.onopen = () => {
this.isConnected = true;
this.reconnectAttempts = 0;
this.logger.log.bind(this.logger)(`Connected to ${this.url}`);
resolve();
};
this.websocket.onerror = (error) => {
this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
reject(error);
};
this.websocket.onmessage = (event) => {
// const message = JSON.parse(event.data.toString());
// console.log("Received message:", message);
this.handleMessage(event.data);
};
this.websocket.onclose = () => {
this.isConnected = false;
this.logger.logWarn.bind(this.logger)(`Disconnected from ${this.url}`);
this.attemptReconnect();
};
});
}
private attemptReconnect(): void {
try {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.logger.logError.bind(this.logger)(`Reconnecting attempt ${this.reconnectAttempts}`);
setTimeout(() => this.connect().then().catch(), 1000 * this.reconnectAttempts);
} else {
this.logger.logError.bind(this.logger)(`Max reconnect attempts reached. Could not reconnect to ${this.url}`);
}
} catch (error) {
this.logger.logError.bind(this.logger)(`Error attempting to reconnect: ${error}`);
}
}
async registerCallback(trace_id: string, type: string, callback: any): Promise<void> {
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
}
async init(pid: number, recv: string, send: string): Promise<void> {
if (!this.isConnected || !this.websocket) {
throw new Error("WebSocket is not connected");
}
const initMessage = {
action: 'init',
pid: pid,
recv: recv,
send: send
};
this.websocket.send(JSON.stringify(initMessage));
}
async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 5000, sendcb: any = () => { }): Promise<any> {
return new Promise<any>((resolve, reject) => {
if (!this.isConnected || !this.websocket) {
throw new Error("WebSocket is not connected");
}
const commandMessage = {
action: 'send',
cmd: cmd,
data: data,
trace_id: trace_id
};
this.websocket.send(JSON.stringify(commandMessage));
if (rsp) {
this.registerCallback(trace_id, 'recv', (json: any) => {
clearTimeout(timeoutHandle);
resolve(json);
});
}
this.registerCallback(trace_id, 'send', (json: any) => {
sendcb(json);
if (!rsp) {
clearTimeout(timeoutHandle);
resolve(json);
}
});
const timeoutHandle = setTimeout(() => {
reject(new Error(`sendCommand timed out after ${timeout} ms`));
}, timeout);
});
}
private async handleMessage(message: any): Promise<void> {
try {
let json = JSON.parse(message.toString());
let trace_id_md5 = json.trace_id_md5;
let action = json?.type ?? 'init';
let event = this.cb.get(trace_id_md5 + action);
if (event) {
await event(json.data);
}
//console.log("Received message:", json);
} catch (error) {
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
}
}
}

View File

@ -31,6 +31,7 @@ import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileLis
import { proxiedListenerOf } from '@/common/proxy-handler'; import { proxiedListenerOf } from '@/common/proxy-handler';
import { NapCatEventChannel } from '@/core/events'; import { NapCatEventChannel } from '@/core/events';
import { NTQQPacketApi } from './apis/packet';
export * from './wrapper'; export * from './wrapper';
export * from './entities'; export * from './entities';
export * from './services'; export * from './services';
@ -84,17 +85,18 @@ export class NapCatCore {
this.context = context; this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil; this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session); this.eventWrapper = new NTEventWrapper(context.session);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
this.apis = { this.apis = {
FileApi: new NTQQFileApi(this.context, this), FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this), SystemApi: new NTQQSystemApi(this.context, this),
CollectionApi: new NTQQCollectionApi(this.context, this), CollectionApi: new NTQQCollectionApi(this.context, this),
PacketApi: new NTQQPacketApi(this.context, this),
WebApi: new NTQQWebApi(this.context, this), WebApi: new NTQQWebApi(this.context, this),
FriendApi: new NTQQFriendApi(this.context, this), FriendApi: new NTQQFriendApi(this.context, this),
MsgApi: new NTQQMsgApi(this.context, this), MsgApi: new NTQQMsgApi(this.context, this),
UserApi: new NTQQUserApi(this.context, this), UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this), GroupApi: new NTQQGroupApi(this.context, this),
}; };
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
this.NapCatDataPath = path.join(this.dataPath, 'NapCat'); this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
fs.mkdirSync(this.NapCatDataPath, { recursive: true }); fs.mkdirSync(this.NapCatDataPath, { recursive: true });
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp'); this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
@ -328,6 +330,7 @@ export interface InstanceContext {
export interface StableNTApiWrapper { export interface StableNTApiWrapper {
FileApi: NTQQFileApi, FileApi: NTQQFileApi,
SystemApi: NTQQSystemApi, SystemApi: NTQQSystemApi,
PacketApi: NTQQPacketApi,
CollectionApi: NTQQCollectionApi, CollectionApi: NTQQCollectionApi,
WebApi: NTQQWebApi, WebApi: NTQQWebApi,
FriendApi: NTQQFriendApi, FriendApi: NTQQFriendApi,

View File

@ -0,0 +1,21 @@
import { MessageType, ScalarType } from "@protobuf-ts/runtime";
import { OidbSvcTrpcTcpBase } from "./Poke";
export const OidbSvcTrpcTcp0XFE1_2 = new MessageType("oidb_svc_trpctcp_0xfe1_2", [
{ no: 1, name: "uin", kind: "scalar", T: ScalarType.UINT32 },
{ no: 3, name: "key", kind: "scalar", T: ScalarType.BYTES, opt: true }
]);
export function encode_packet_0xfe1_2(PeerUin: string) {
let Body = OidbSvcTrpcTcp0XFE1_2.toBinary
({
uin: parseInt(PeerUin),
key: new Uint8Array([0x00, 0x00, 0x00, 0x00])
});
return OidbSvcTrpcTcpBase.toBinary
({
command: 0xfe1,
subcommand: 2,
body: Body,
isreserved: 1
});
}

31
src/core/proto/Poke.ts Normal file
View File

@ -0,0 +1,31 @@
import { MessageType, ScalarType, BinaryWriter } from '@protobuf-ts/runtime';
export const OidbSvcTrpcTcpBase = new MessageType("oidb_svc_trpctcp_base", [
{ no: 1, name: "command", kind: "scalar", T: ScalarType.UINT32 },
{ no: 2, name: "subcommand", kind: "scalar", T: ScalarType.UINT32, opt: true },
{ no: 4, name: "body", kind: "scalar", T: ScalarType.BYTES, opt: true },
{ no: 12, name: "isreserved", kind: "scalar", T: ScalarType.INT32, opt: true }
]);
export const OidbSvcTrpcTcp0XED3_1 = new MessageType("oidb_svc_trpctcp_0xed3_1", [
{ no: 1, name: "uin", kind: "scalar", T: ScalarType.UINT32 },
{ no: 2, name: "groupuin", kind: "scalar", T: ScalarType.UINT32, opt: true },
{ no: 5, name: "frienduin", kind: "scalar", T: ScalarType.UINT32, opt: true },
{ no: 6, name: "ext", kind: "scalar", T: ScalarType.UINT32 }
]);
export function encodeGroupPoke(groupUin: number, PeerUin: number) {
let Body = OidbSvcTrpcTcp0XED3_1.toBinary
({
uin: PeerUin,
groupuin: groupUin,
ext: 0
});
//console.log(Body)
return OidbSvcTrpcTcpBase.toBinary
({
command: 0xed3,
subcommand: 1,
body: Body
});
}

View File

@ -4,7 +4,7 @@ import { dlopen } from "process";
import fs from "fs"; import fs from "fs";
export class Native { export class Native {
platform: string; platform: string;
supportedPlatforms = ['win32']; supportedPlatforms = [''];
MoeHooExport: any = { exports: {} }; MoeHooExport: any = { exports: {} };
recallHookEnabled: boolean = false; recallHookEnabled: boolean = false;
inited = true; inited = true;

View File

@ -0,0 +1,26 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
// no_cache get时传字符串
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
},
required: ['group_id', 'user_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GroupPoke extends BaseAction<Payload, any> {
actionName = ActionName.GroupPoke;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
if (!this.core.apis.PacketApi.PacketClient?.isConnected) {
throw new Error('PacketClient is not init');
}
this.core.apis.GroupApi.sendPacketPoke(+payload.group_id, +payload.user_id);
}
}

View File

@ -84,6 +84,7 @@ import { GetGroupFileSystemInfo } from '@/onebot/action/go-cqhttp/GetGroupFileSy
import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles'; import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles';
import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder'; import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder';
import { GetGroupSystemMsg } from './system/GetSystemMsg'; import { GetGroupSystemMsg } from './system/GetSystemMsg';
import { GroupPoke } from './group/GroupPoke';
export type ActionMap = Map<string, BaseAction<any, any>>; export type ActionMap = Map<string, BaseAction<any, any>>;
@ -180,6 +181,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetGroupFilesByFolder(obContext, core), new GetGroupFilesByFolder(obContext, core),
new GetGroupSystemMsg(obContext, core), new GetGroupSystemMsg(obContext, core),
new FetchUserProfileLike(obContext, core), new FetchUserProfileLike(obContext, core),
new GroupPoke(obContext, core),
]; ];
const actionMap = new Map(); const actionMap = new Map();
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@ -16,6 +16,7 @@ export interface InvalidCheckResult {
export enum ActionName { export enum ActionName {
// 以下为扩展napcat扩展 // 以下为扩展napcat扩展
Unknown = 'unknown', Unknown = 'unknown',
GroupPoke = 'group_poke',
SharePeer = 'ArkSharePeer', SharePeer = 'ArkSharePeer',
ShareGroupEx = 'ArkShareGroup', ShareGroupEx = 'ArkShareGroup',
RebootNormal = 'reboot_normal',//无快速登录重新启动 RebootNormal = 'reboot_normal',//无快速登录重新启动

View File

@ -21,6 +21,7 @@ import { OB11GroupTitleEvent } from '@/onebot/event/notice/OB11GroupTitleEvent';
import { FileNapCatOneBotUUID } from '@/common/helper'; import { FileNapCatOneBotUUID } from '@/common/helper';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
export class OneBotGroupApi { export class OneBotGroupApi {
obContext: NapCatOneBot11Adapter; obContext: NapCatOneBot11Adapter;
core: NapCatCore; core: NapCatCore;

View File

@ -540,6 +540,10 @@ export class NapCatOneBot11Adapter {
if (isSelfMsg) { if (isSelfMsg) {
ob11Msg.target_id = parseInt(message.peerUin); ob11Msg.target_id = parseInt(message.peerUin);
} }
// if(ob11Msg.raw_message.startsWith('!poke')){
// console.log('poke',message.peerUin, message.senderUin);
// this.core.apis.GroupApi.sendPacketPoke(message.peerUin, message.senderUin);
// }
this.networkManager.emitEvent(ob11Msg); this.networkManager.emitEvent(ob11Msg);
}).catch(e => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e)); }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e));

View File

@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
undefined, undefined,
SettingButton('V2.6.24', 'napcat-update-button', 'secondary'), SettingButton('V2.6.27', 'napcat-update-button', 'secondary'),
), ),
]), ]),
SettingList([ SettingList([

View File

@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
void 0, void 0,
SettingButton("V2.6.24", "napcat-update-button", "secondary") SettingButton("V2.6.27", "napcat-update-button", "secondary")
) )
]), ]),
SettingList([ SettingList([