Compare commits

...

63 Commits

Author SHA1 Message Date
手瓜一十雪
8243abaf0c feat: remote 2025-06-17 12:49:47 +08:00
手瓜一十雪
25be976fc9 feat: mock Wrapper 2025-06-17 12:48:37 +08:00
手瓜一十雪
a8d8a94309 fix 2025-06-17 12:41:38 +08:00
手瓜一十雪
eb9209cffb fix 2025-06-17 12:41:08 +08:00
手瓜一十雪
06bc761dd3 feat: 本地pipe分离测试 2025-06-17 11:58:31 +08:00
手瓜一十雪
8994d3af14 fix 2025-06-16 18:51:01 +08:00
手瓜一十雪
5eefd3dbe8 feat: 支持client 多注册 2025-06-16 17:41:49 +08:00
手瓜一十雪
2be014a9f2 feat: remove superjson 2025-06-16 17:36:27 +08:00
手瓜一十雪
ee4c9e95ad fix 2025-06-16 16:59:44 +08:00
手瓜一十雪
3e25172450 fix 2025-06-16 16:57:50 +08:00
手瓜一十雪
ac51c50046 fix 2025-06-16 16:55:09 +08:00
手瓜一十雪
a2cae1734b fix: 初步解耦 session到远程 2025-06-16 16:53:12 +08:00
手瓜一十雪
88e9caddfa feat: test-rpc-service 2025-06-14 17:42:31 +08:00
手瓜一十雪
f576cd9417 fix: type 2025-06-13 16:58:05 +08:00
时瑾
9cfd224b74 fix: 优化get_group_ignored_notifies接口返回值 2025-06-12 20:14:11 +08:00
时瑾
c12f8de8b4 feat: get_collection_list 2025-06-12 13:28:31 +08:00
时瑾
ed9a7c52e2 feat: get_group_ignore_add_request 2025-06-12 13:23:22 +08:00
Mlikiowa
38fcaaa28b release: v4.7.78 2025-06-12 04:30:05 +00:00
手瓜一十雪
5317a1c1a9 fix: 35951 2025-06-12 12:29:14 +08:00
时瑾
4bc5933ea2 fix: 修正部分接口的参数、返回值,提高兼容性 (#1072)
* fix: 修正`get_group_system_msg` `get_group_honor_info`接口返回值 提升兼容性

* fix: `create_group_file_folder` 接口兼容性提升
2025-06-11 12:37:29 +08:00
Mlikiowa
6a6bd33fe5 release: v4.7.77 2025-06-10 06:28:20 +00:00
手瓜一十雪
8256942a3d fix: #1051 2025-06-10 14:27:50 +08:00
手瓜一十雪
697632eee8 feat: 35951 2025-06-10 13:19:19 +08:00
手瓜一十雪
6bbf5b254d fix: #1049 2025-06-10 13:05:36 +08:00
手瓜一十雪
5831898c4a fix: #1058 2025-06-10 12:54:29 +08:00
dependabot[bot]
2cc413bec1 build(deps-dev): bump multer from 1.4.5-lts.2 to 2.0.1 (#1070)
Bumps [multer](https://github.com/expressjs/multer) from 1.4.5-lts.2 to 2.0.1.
- [Release notes](https://github.com/expressjs/multer/releases)
- [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/expressjs/multer/compare/v1.4.5-lts.2...v2.0.1)

---
updated-dependencies:
- dependency-name: multer
  dependency-version: 2.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 12:48:39 +08:00
时瑾
0af36e89d9 fix: 转发消息接口返回值兼容gocq (#1066) 2025-06-09 10:02:27 +08:00
837951602
b2c0f5d2e5 /get_group_system_msg description (#1064) 2025-06-08 10:38:32 +08:00
手瓜一十雪
80b74c7da9 Merge pull request #1054 from NapNeko/dependabot/npm_and_yarn/file-type-21.0.0
build(deps-dev): bump file-type from 20.5.0 to 21.0.0
2025-06-06 11:53:31 +08:00
手瓜一十雪
f14f13b158 build(deps-dev): bump esbuild from 0.25.4 to 0.25.5 (#1056)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.4 to 0.25.5.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 11:53:06 +08:00
lzw
9dda00b6fa chore: add host in listen log
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-06-05 12:59:01 +08:00
Lan Zongwei
a29debb738 fix: fix missing host in onebot http-server listen 2025-06-05 12:59:01 +08:00
dependabot[bot]
b990fc43df build(deps-dev): bump esbuild from 0.25.4 to 0.25.5
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.4 to 0.25.5.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.4...v0.25.5)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 09:14:28 +00:00
dependabot[bot]
915e9552ee build(deps-dev): bump file-type from 20.5.0 to 21.0.0
Bumps [file-type](https://github.com/sindresorhus/file-type) from 20.5.0 to 21.0.0.
- [Release notes](https://github.com/sindresorhus/file-type/releases)
- [Commits](https://github.com/sindresorhus/file-type/compare/v20.5.0...v21.0.0)

---
updated-dependencies:
- dependency-name: file-type
  dependency-version: 21.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 09:09:48 +00:00
Mlikiowa
c522e0a386 release: v4.7.76 2025-05-29 13:58:02 +00:00
手瓜一十雪
c9cc08a9ba fix: #1048 2025-05-29 21:15:07 +08:00
手瓜一十雪
66e1b1662f fix: 支持registerCallback 2025-05-29 20:45:35 +08:00
手瓜一十雪
9372e83bd8 feat: nativeLoader功能预备 2025-05-29 14:39:09 +08:00
Mlikiowa
b38a240dbb release: v4.7.75 2025-05-26 12:19:44 +00:00
手瓜一十雪
76b9506395 fix: #1043 2025-05-26 19:58:50 +08:00
手瓜一十雪
f1cf636aa2 Merge pull request #1041 from Neboer/main
允许使用环境变量指定napcat工作路径。
2025-05-26 14:41:01 +08:00
Mlikiowa
312dcd0e13 release: v4.7.74 2025-05-26 05:57:44 +00:00
手瓜一十雪
42c2419613 Revert "fix: #1038"
This reverts commit 4e7c96634c.
2025-05-26 13:56:48 +08:00
手瓜一十雪
8f7f748e82 Revert "fix: #1039"
This reverts commit 1eda3f2e33.
2025-05-26 13:56:14 +08:00
Neboer
7ad3bad1be 修改环境变量名字NAPCAT_WRITEPATH为NAPCAT_WORKDIR 2025-05-26 05:36:07 +00:00
Neboer
5cd682e69f 允许使用NAPCAT_WRITEPATH环境变量指定napcat工作路径。 2025-05-26 04:59:20 +00:00
Mlikiowa
5d57780e84 release: v4.7.73 2025-05-26 03:52:01 +00:00
手瓜一十雪
f399955204 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-05-26 11:51:35 +08:00
手瓜一十雪
770652fe6b fix: remove debug 2025-05-26 11:51:25 +08:00
Mlikiowa
9ed5fa8c67 release: v4.7.72 2025-05-26 03:51:12 +00:00
手瓜一十雪
5a4ad29727 fix: #1040 2025-05-26 11:50:45 +08:00
手瓜一十雪
1eda3f2e33 fix: #1039 2025-05-26 10:58:01 +08:00
Mlikiowa
95cb95ef96 release: v4.7.70 2025-05-25 08:56:20 +00:00
手瓜一十雪
4e7c96634c fix: #1038 2025-05-25 16:55:32 +08:00
手瓜一十雪
58587b8aea fix 2025-05-25 16:30:15 +08:00
手瓜一十雪
3fbf6239db fix: #1031 2025-05-25 16:18:50 +08:00
手瓜一十雪
faec53d497 feat: #1031 2025-05-25 16:09:06 +08:00
手瓜一十雪
482dcc534e feat: kill-update 2025-05-24 10:33:14 +08:00
手瓜一十雪
854f61dda6 feat: createGrayTip 2025-05-23 17:22:07 +08:00
Mlikiowa
fca38713a1 release: v4.7.68 2025-05-22 03:48:00 +00:00
手瓜一十雪
5dd3bade53 fix: #1029 2025-05-22 11:47:31 +08:00
手瓜一十雪
665360f48d fix: #1027 2025-05-22 11:33:23 +08:00
Mlikiowa
65719cb56a release: v4.7.67 2025-05-21 04:59:16 +00:00
52 changed files with 3183 additions and 158 deletions

Binary file not shown.

View File

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

View File

@@ -26,7 +26,7 @@ const itemVariants = {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 20 }
transition: { type: 'spring' as const, stiffness: 300, damping: 20 }
}
}

View File

@@ -24,9 +24,7 @@ const oneBotHttpApiGroup = {
},
'/get_group_system_msg': {
description: '获取群系统消息',
request: z.object({
group_id: z.union([z.string(), z.number()]).describe('群号')
}),
request: z.object({}),
response: baseResponseSchema.extend({
data: z.object({
InvitedRequest: z
@@ -37,6 +35,7 @@ const oneBotHttpApiGroup = {
invitor_uin: z.string().describe('邀请人 QQ 号'),
invitor_nick: z.string().describe('邀请人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
@@ -50,6 +49,7 @@ const oneBotHttpApiGroup = {
requester_uin: z.string().describe('请求人 QQ 号'),
requester_nick: z.string().describe('请求人昵称'),
group_id: z.string().describe('群号'),
message: z.string().describe('入群回答'),
group_name: z.string().describe('群名称'),
checked: z.boolean().describe('是否已处理'),
actor: z.string().describe('处理人 QQ 号')
@@ -604,7 +604,7 @@ const oneBotHttpApiGroup = {
response: baseResponseSchema.extend({
data: z
.object({
group_id: z.string().describe('群号'),
group_id: z.number().describe('群号'),
current_talkative: z
.object({
user_id: z.number().describe('QQ 号'),

View File

@@ -56,9 +56,9 @@ export default function TerminalPage() {
setTabs((prev) => [...prev, newTab])
setSelectedTab(id)
} catch (error) {
} catch (error: unknown) {
console.error('Failed to create terminal:', error)
toast.error('创建终端失败')
toast.error((error as Error).message)
}
}

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.7.66",
"version": "4.7.78",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -41,29 +41,29 @@
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"esbuild": "0.25.4",
"esbuild": "0.25.5",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.29.1",
"express-rate-limit": "^7.5.0",
"fast-xml-parser": "^4.3.6",
"file-type": "^20.0.0",
"file-type": "^21.0.0",
"globals": "^16.0.0",
"json5": "^2.2.3",
"multer": "^1.4.5-lts.1",
"multer": "^2.0.1",
"napcat.protobuf": "^1.1.4",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",
"vite-plugin-cp": "^6.0.0",
"vite-tsconfig-paths": "^5.1.0",
"napcat.protobuf": "^1.1.4",
"winston": "^3.17.0",
"compressing": "^1.10.1"
"winston": "^3.17.0"
},
"dependencies": {
"express": "^5.0.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}
}
}

View File

@@ -10,9 +10,9 @@ interface InternalMapKey {
checker: ((...args: any[]) => boolean) | undefined;
}
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
export type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
type FuncKeys<T> = Extract<
export type FuncKeys<T> = Extract<
{
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
}[keyof T],

View File

@@ -13,11 +13,15 @@ export class NapCatPathWrapper {
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath;
let writePath: string;
if (os.platform() === 'darwin') {
if (process.env['NAPCAT_WORKDIR']) {
writePath = process.env['NAPCAT_WORKDIR'];
} else if (os.platform() === 'darwin') {
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else {
writePath = this.binaryPath;
}
this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config');
this.cachePath = path.join(writePath, 'cache');

View File

@@ -20,3 +20,23 @@ export function proxyHandlerOf(logger: LogWrapper) {
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
return new Proxy<T>(listener, proxyHandlerOf(logger));
}
export function proxyHandlerOfWithoutLogger() {
return {
get(target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (..._args: unknown[]) => {
console.log(`${target.constructor.name} has no method ${prop}`);
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
};
}
export function proxiedListenerOfWithoutLogger<T extends object>(listener: T) {
return new Proxy<T>(listener, proxyHandlerOfWithoutLogger());
}

View File

@@ -1 +1 @@
export const napCatVersion = '4.7.66';
export const napCatVersion = '4.7.78';

View File

@@ -10,11 +10,14 @@ import {
GroupNotify,
GroupInfoSource,
ShutUpGroupMember,
Peer,
ChatType,
} from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique';
import { NTEventWrapper } from '@/common/event';
import { CancelableTask, TaskExecutor } from '@/common/cancel-task';
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
export class NTQQGroupApi {
context: InstanceContext;
@@ -47,6 +50,22 @@ export class NTQQGroupApi {
this.initCache().then().catch(e => this.context.logger.logError(e));
}
async createGrayTip(groupCode: string, tip: string) {
return this.context.session.getMsgService().addLocalJsonGrayTipMsg(
{
chatType: ChatType.KCHATTYPEGROUP,
peerUid: groupCode,
} as Peer,
{
busiId: 2201,
jsonStr: JSON.stringify({ "align": "center", "items": [{ "txt": tip, "type": "nor" }] }),
recentAbstract: tip,
isServer: false
},
true,
true
)
}
async initCache() {
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
@@ -95,6 +114,58 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().setHeader(groupCode, filePath);
}
// 0 0 无需管理员审核
// 0 2 需要管理员审核
// 1 2 禁止Bot入群( 最好只传一个1 )
async setGroupRobotAddOption(groupCode: string, robotMemberSwitch?: number, robotMemberExamine?: number) {
let extInfo = createGroupExtInfo(groupCode);
let groupExtFilter = createGroupExtFilter();
if (robotMemberSwitch !== undefined) {
extInfo.extInfo.inviteRobotMemberSwitch = robotMemberSwitch;
groupExtFilter.inviteRobotMemberSwitch = 1;
}
if (robotMemberExamine !== undefined) {
extInfo.extInfo.inviteRobotMemberExamine = robotMemberExamine;
groupExtFilter.inviteRobotMemberExamine = 1;
}
return this.context.session.getGroupService().modifyGroupExtInfoV2(extInfo, groupExtFilter);
}
async setGroupAddOption(groupCode: string, option: {
addOption: number;
groupQuestion?: string;
groupAnswer?: string;
}) {
let param = createGroupDetailInfoV2Param(groupCode);
// 设置要修改的目标
param.filter.addOption = 1;
if (option.addOption == 4 || option.addOption == 5) {
// 4 问题进入答案 5 问题管理员批准
param.filter.groupQuestion = 1;
param.filter.groupAnswer = option.addOption == 4 ? 1 : 0;
param.modifyInfo.groupQuestion = option.groupQuestion || '';
param.modifyInfo.groupAnswer = option.addOption == 4 ? option.groupAnswer || '' : '';
}
param.modifyInfo.addOption = option.addOption;
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
}
async setGroupSearch(groupCode: string, option: {
noCodeFingerOpenFlag?: number;
noFingerOpenFlag?: number;
}) {
let param = createGroupDetailInfoV2Param(groupCode);
if (option.noCodeFingerOpenFlag) {
param.filter.noCodeFingerOpenFlag = 1;
param.modifyInfo.noCodeFingerOpenFlag = option.noCodeFingerOpenFlag;
}
if (option.noFingerOpenFlag) {
param.filter.noFingerOpenFlag = 1;
param.modifyInfo.noFingerOpenFlag = option.noFingerOpenFlag;
}
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
}
async getGroups(forced: boolean = false) {
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupList',

View File

@@ -142,7 +142,6 @@ export class NTQQMsgApi {
}
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],

View File

@@ -264,7 +264,7 @@ export class NTQQWebApi {
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
let HonorInfo = {
group_id: groupCode,
group_id: Number(groupCode),
current_talkative: {},
talkative_list: [],
performer_list: [],

245
src/core/data/group.ts Normal file
View File

@@ -0,0 +1,245 @@
import { GroupDetailInfoV2Param, GroupExtInfo, GroupExtFilter } from "../types";
export function createGroupDetailInfoV2Param(group_code: string): GroupDetailInfoV2Param {
return {
groupCode: group_code,
filter:
{
noCodeFingerOpenFlag: 0,
noFingerOpenFlag: 0,
groupName: 0,
classExt: 0,
classText: 0,
fingerMemo: 0,
richFingerMemo: 0,
tagRecord: 0,
groupGeoInfo:
{
ownerUid: 0,
setTime: 0,
cityId: 0,
longitude: 0,
latitude: 0,
geoContent: 0,
poiId: 0
},
groupExtAdminNum: 0,
flag: 0,
groupMemo: 0,
groupAioSkinUrl: 0,
groupBoardSkinUrl: 0,
groupCoverSkinUrl: 0,
groupGrade: 0,
activeMemberNum: 0,
certificationType: 0,
certificationText: 0,
groupNewGuideLines:
{
enabled: 0,
content: 0
},
groupFace: 0,
addOption: 0,
shutUpTime: 0,
groupTypeFlag: 0,
appPrivilegeFlag: 0,
appPrivilegeMask: 0,
groupExtOnly:
{
tribeId: 0,
moneyForAddGroup: 0
}, groupSecLevel: 0,
groupSecLevelInfo: 0,
subscriptionUin: 0,
subscriptionUid: "",
allowMemberInvite: 0,
groupQuestion: 0,
groupAnswer: 0,
groupFlagExt3: 0,
groupFlagExt3Mask: 0,
groupOpenAppid: 0,
rootId: 0,
msgLimitFrequency: 0,
hlGuildAppid: 0,
hlGuildSubType: 0,
hlGuildOrgId: 0,
groupFlagExt4: 0,
groupFlagExt4Mask: 0,
groupSchoolInfo: {
location: 0,
grade: 0,
school: 0
},
groupCardPrefix:
{
introduction: 0,
rptPrefix: 0
}, allianceId: 0,
groupFlagPro1: 0,
groupFlagPro1Mask: 0
},
modifyInfo: {
noCodeFingerOpenFlag: 0,
noFingerOpenFlag: 0,
groupName: "",
classExt: 0,
classText: "",
fingerMemo: "",
richFingerMemo: "",
tagRecord: [],
groupGeoInfo: {
ownerUid: "",
SetTime: 0,
CityId: 0,
Longitude: "",
Latitude: "",
GeoContent: "",
poiId: ""
},
groupExtAdminNum: 0,
flag: 0,
groupMemo: "",
groupAioSkinUrl: "",
groupBoardSkinUrl: "",
groupCoverSkinUrl: "",
groupGrade: 0,
activeMemberNum: 0,
certificationType: 0,
certificationText: "",
groupNewGuideLines: {
enabled: false,
content: ""
}, groupFace: 0,
addOption: 0,
shutUpTime: 0,
groupTypeFlag: 0,
appPrivilegeFlag: 0,
appPrivilegeMask: 0,
groupExtOnly: {
tribeId: 0,
moneyForAddGroup: 0
},
groupSecLevel: 0,
groupSecLevelInfo: 0,
subscriptionUin: "",
subscriptionUid: "",
allowMemberInvite: 0,
groupQuestion: "",
groupAnswer: "",
groupFlagExt3: 0,
groupFlagExt3Mask: 0,
groupOpenAppid: 0,
rootId: "",
msgLimitFrequency: 0,
hlGuildAppid: 0,
hlGuildSubType: 0,
hlGuildOrgId: 0,
groupFlagExt4: 0,
groupFlagExt4Mask: 0,
groupSchoolInfo: {
location: "",
grade: 0,
school: ""
},
groupCardPrefix:
{
introduction: "",
rptPrefix: []
},
allianceId: "",
groupFlagPro1: 0,
groupFlagPro1Mask: 0
}
}
}
export function createGroupExtInfo(group_code: string): GroupExtInfo {
return {
groupCode: group_code,
resultCode: 0,
extInfo: {
groupInfoExtSeq: 0,
reserve: 0,
luckyWordId: '',
lightCharNum: 0,
luckyWord: '',
starId: 0,
essentialMsgSwitch: 0,
todoSeq: 0,
blacklistExpireTime: 0,
isLimitGroupRtc: 0,
companyId: 0,
hasGroupCustomPortrait: 0,
bindGuildId: '',
groupOwnerId: {
memberUin: '',
memberUid: '',
memberQid: '',
},
essentialMsgPrivilege: 0,
msgEventSeq: '',
inviteRobotSwitch: 0,
gangUpId: '',
qqMusicMedalSwitch: 0,
showPlayTogetherSwitch: 0,
groupFlagPro1: '',
groupBindGuildIds: {
guildIds: [],
},
viewedMsgDisappearTime: '',
groupExtFlameData: {
switchState: 0,
state: 0,
dayNums: [],
version: 0,
updateTime: '',
isDisplayDayNum: false,
},
groupBindGuildSwitch: 0,
groupAioBindGuildId: '',
groupExcludeGuildIds: {
guildIds: [],
},
fullGroupExpansionSwitch: 0,
fullGroupExpansionSeq: '',
inviteRobotMemberSwitch: 0,
inviteRobotMemberExamine: 0,
groupSquareSwitch: 0,
}
}
}
export function createGroupExtFilter(): GroupExtFilter {
return {
groupInfoExtSeq: 0,
reserve: 0,
luckyWordId: 0,
lightCharNum: 0,
luckyWord: 0,
starId: 0,
essentialMsgSwitch: 0,
todoSeq: 0,
blacklistExpireTime: 0,
isLimitGroupRtc: 0,
companyId: 0,
hasGroupCustomPortrait: 0,
bindGuildId: 0,
groupOwnerId: 0,
essentialMsgPrivilege: 0,
msgEventSeq: 0,
inviteRobotSwitch: 0,
gangUpId: 0,
qqMusicMedalSwitch: 0,
showPlayTogetherSwitch: 0,
groupFlagPro1: 0,
groupBindGuildIds: 0,
viewedMsgDisappearTime: 0,
groupExtFlameData: 0,
groupBindGuildSwitch: 0,
groupAioBindGuildId: 0,
groupExcludeGuildIds: 0,
fullGroupExpansionSwitch: 0,
fullGroupExpansionSeq: 0,
inviteRobotMemberSwitch: 0,
inviteRobotMemberExamine: 0,
groupSquareSwitch: 0,
}
};

1
src/core/data/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from "./group";

View File

@@ -302,5 +302,17 @@
"9.9.19-35341": {
"appid": 537291347,
"qua": "V1_WIN_NQ_9.9.19_35341_GW_B"
},
"9.9.19-35469": {
"appid": 537291398,
"qua": "V1_WIN_NQ_9.9.19_35469_GW_B"
},
"3.2.18-35951": {
"appid": 537296013,
"qua": "V1_LNX_NQ_3.2.18_35951_GW_B"
},
"9.9.20-35951": {
"appid": 537295977,
"qua": "V1_WIN_NQ_9.9.20_35951_GW_B"
}
}

View File

@@ -380,8 +380,12 @@
"recv": "3BEA210"
},
"9.9.19-35341-x64": {
"send": "3BE5A10",
"recv": "3BEA210"
"send": "3BF1D50",
"recv": "3BF6550"
},
"9.9.19-35469-x64": {
"send": "3BF1D50",
"recv": "3BF6550"
},
"3.2.17-35341-x64": {
"send": "AE2F700",
@@ -390,5 +394,13 @@
"3.2.17-35341-arm64": {
"send": "778D840",
"recv": "7791170"
},
"9.9.20-35951-x64": {
"send": "3034BAC",
"recv": "3038354"
},
"3.2.18-35951-x64": {
"send": "AFBBB00",
"recv": "AFBF520"
}
}

View File

@@ -30,6 +30,10 @@ import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { handleServiceServerOnce, receiverServiceListener, ServiceMethodCommand } from '@/remote/service';
import { rpc_decode, rpc_encode } from '@/remote/serialize';
import { PipeClient, PipeServer } from '@/remote/pipe';
import { RemoteWrapperSession } from '@/remote/remoteSession';
export * from './wrapper';
export * from './types';
export * from './services';
@@ -97,9 +101,63 @@ export class NapCatCore {
constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath,NapcatConfigSchema);
// 管道服务端测试
let pipe_server = new PipeServer('//./pipe/napcat');
pipe_server.registerHandler(async (packet, helper) => {
if (packet.type !== 'event_request') {
return helper.error('Invalid packet type');
}
let event_rpc_data = rpc_decode<{ params: any[] }>(JSON.parse(packet.data));
let event_rpc_trace = packet.trace;
let event_rpc_command = packet.command as ServiceMethodCommand;
let event_rpc_result = await handleServiceServerOnce(event_rpc_command,
async (listenerCommand: string, ...args: any[]) => {
let listener_data = rpc_encode<{ params: any[] }>({ params: args });
helper.sendListenerCallback(listenerCommand, JSON.stringify(rpc_encode(listener_data)));
},
this.eventWrapper,
...event_rpc_data.params
);
return helper.sendEventResponse(event_rpc_trace, JSON.stringify(rpc_encode(event_rpc_result)));
});
pipe_server.start().then(() => {
this.context.logger.log('Pipe server started successfully');
let pipe_client = new PipeClient('//./pipe/napcat');
let trace_callback_map = new Map<string, (trace: string, data: any) => void>();
pipe_client.registerHandler(async (packet, _helper) => {
if (packet.type == 'event_response') {
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
trace_callback_map.get(packet.trace)?.(packet.trace, event_rpc_data);
} else if (packet.type == 'listener_callback') {
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
await receiverServiceListener(packet.command, ...event_rpc_data);
}
});
this.context.session = new RemoteWrapperSession(async (_serviceClient, serviceCommand, ...args) => {
let trace = crypto.randomUUID();
return await new Promise((resolve, _reject) => {
trace_callback_map.set(trace, (_trace, data) => {
//console.log('Received response for trace:', _trace, 'with data:', data);
resolve(data);
});
pipe_client.sendRequest(serviceCommand, JSON.stringify(rpc_encode({ params: args })), trace);
});
});
pipe_client.connect().then(() => {
this.context.logger.log('Pipe client connected successfully');
}).catch((e) => {
this.context.logger.logError('Pipe client connection failed: ' + e.message);
});
}).catch((e) => {
this.context.logger.logError('Pipe server start failed: ' + e.message);
});
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this),
@@ -251,13 +309,13 @@ export async function genSessionConfig(
}
export interface InstanceContext {
readonly workingEnv: NapCatCoreWorkingEnv;
readonly wrapper: WrapperNodeApi;
readonly session: NodeIQQNTWrapperSession;
readonly logger: LogWrapper;
readonly loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper;
session: NodeIQQNTWrapperSession;
workingEnv: NapCatCoreWorkingEnv;
wrapper: WrapperNodeApi;
logger: LogWrapper;
loginService: NodeIKernelLoginService;
basicInfoWrapper: QQBasicInfoWrapper;
pathWrapper: NapCatPathWrapper;
}
export interface StableNTApiWrapper {

View File

@@ -40,7 +40,6 @@ export class NodeIKernelBuddyListener {
}
onDelBatchBuddyInfos(_arg: unknown): any {
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
}
onDoubtBuddyReqChange(_arg:

View File

@@ -1,71 +1,71 @@
import { User, UserDetailInfoListenerArg } from '@/core/types';
import { SelfStatusInfo, User, UserDetailInfoListenerArg } from '@/core/types';
export class NodeIKernelProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
onUserDetailInfoChanged(_arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]): any {
onProfileSimpleChanged(..._args: unknown[]): any {
}
onProfileDetailInfoChanged(profile: User): any {
onProfileDetailInfoChanged(_profile: User): any {
}
onStatusUpdate(...args: unknown[]): any {
onStatusUpdate(..._args: unknown[]): any {
}
onSelfStatusChanged(...args: unknown[]): any {
onSelfStatusChanged(_info: SelfStatusInfo): any {
}
onStrangerRemarkChanged(...args: unknown[]): any {
onStrangerRemarkChanged(..._args: unknown[]): any {
}
onMemberListChange(...args: unknown[]): any {
onMemberListChange(..._args: unknown[]): any {
}
onMemberInfoChange(...args: unknown[]): any {
onMemberInfoChange(..._args: unknown[]): any {
}
onGroupListUpdate(...args: unknown[]): any {
onGroupListUpdate(..._args: unknown[]): any {
}
onGroupAllInfoChange(...args: unknown[]): any {
onGroupAllInfoChange(..._args: unknown[]): any {
}
onGroupDetailInfoChange(...args: unknown[]): any {
onGroupDetailInfoChange(..._args: unknown[]): any {
}
onGroupConfMemberChange(...args: unknown[]): any {
onGroupConfMemberChange(..._args: unknown[]): any {
}
onGroupExtListUpdate(...args: unknown[]): any {
onGroupExtListUpdate(..._args: unknown[]): any {
}
onGroupNotifiesUpdated(...args: unknown[]): any {
onGroupNotifiesUpdated(..._args: unknown[]): any {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
onGroupNotifiesUnreadCountUpdated(..._args: unknown[]): any {
}
onGroupMemberLevelInfoChange(...args: unknown[]): any {
onGroupMemberLevelInfoChange(..._args: unknown[]): any {
}
onGroupBulletinChange(...args: unknown[]): any {
onGroupBulletinChange(..._args: unknown[]): any {
}
}

View File

@@ -8,10 +8,22 @@ import {
GroupNotifyMsgType,
NTGroupRequestOperateTypes,
KickMemberV2Req,
GroupDetailInfoV2Param,
GroupExtInfo,
GroupExtFilter,
} from '@/core/types';
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService {
modifyGroupExtInfoV2(groupExtInfo: GroupExtInfo, groupExtFilter: GroupExtFilter): Promise<GeneralCallResult &
{
result: {
groupCode: string,
result: number
}
}>;
// --->
// 待启用 For Next Version 3.2.0
// isTroopMember ? 0 : 111
@@ -169,6 +181,9 @@ export interface NodeIKernelGroupService {
modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
// 第二个参数在大多数情况为0 设置群成员权限 例如上传群文件权限和群成员付费/加入邀请加入时为8
modifyGroupDetailInfoV2(param: GroupDetailInfoV2Param, arg: number): Promise<GeneralCallResult>;
setGroupMsgMask(groupCode: string, arg: unknown): void;
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void;

View File

@@ -16,6 +16,16 @@ export * from './NodeIKernelDbToolsService';
export * from './NodeIKernelTipOffService';
export * from './NodeIKernelSearchService';
export * from './NodeIKernelCollectionService';
export * from './NodeIKernelAlbumService';
export * from './NodeIKernelECDHService';
export * from './NodeIKernelNodeMiscService';
export * from './NodeIKernelMsgBackupService';
export * from './NodeIKernelTianShuService';
export * from './NodeIKernelUnitedConfigService';
export * from './NodeIkernelTestPerformanceService';
export * from './NodeIKernelUixConvertService';
export * from './NodeIKernelMSFService';
export * from './NodeIKernelRecentContactService';
import type {
NodeIKernelAvatarService,
@@ -36,8 +46,19 @@ import type {
NodeIKernelTicketService,
NodeIKernelTipOffService,
} from '.';
import { NodeIKernelAlbumService } from './NodeIKernelAlbumService';
import { NodeIKernelECDHService } from './NodeIKernelECDHService';
import { NodeIKernelNodeMiscService } from './NodeIKernelNodeMiscService';
import { NodeIKernelMsgBackupService } from './NodeIKernelMsgBackupService';
import { NodeIKernelTianShuService } from './NodeIKernelTianShuService';
import { NodeIKernelUnitedConfigService } from './NodeIKernelUnitedConfigService';
import { NodeIkernelTestPerformanceService } from './NodeIkernelTestPerformanceService';
import { NodeIKernelUixConvertService } from './NodeIKernelUixConvertService';
import { NodeIKernelMSFService } from './NodeIKernelMSFService';
import { NodeIKernelRecentContactService } from './NodeIKernelRecentContactService';
export type ServiceNamingMapping = {
NodeIKernelAlbumService: NodeIKernelAlbumService;
NodeIKernelAvatarService: NodeIKernelAvatarService;
NodeIKernelBuddyService: NodeIKernelBuddyService;
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
@@ -53,6 +74,15 @@ export type ServiceNamingMapping = {
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
NodeIKernelTipOffService: NodeIKernelTipOffService;
NodeIKernelSearchService: NodeIKernelSearchService,
NodeIKernelSearchService: NodeIKernelSearchService;
NodeIKernelCollectionService: NodeIKernelCollectionService;
NodeIKernelECDHService: NodeIKernelECDHService;
NodeIKernelNodeMiscService: NodeIKernelNodeMiscService;
NodeIKernelMsgBackupService: NodeIKernelMsgBackupService;
NodeIKernelTianShuService: NodeIKernelTianShuService;
NodeIKernelUnitedConfigService: NodeIKernelUnitedConfigService;
NodeIkernelTestPerformanceService: NodeIkernelTestPerformanceService;
NodeIKernelUixConvertService: NodeIKernelUixConvertService;
NodeIKernelMSFService: NodeIKernelMSFService;
NodeIKernelRecentContactService: NodeIKernelRecentContactService;
};

View File

@@ -58,6 +58,7 @@ export interface GrayTipRovokeElement {
operatorUid: string;
operatorNick: string;
operatorRemark: string;
isSelfOperate: boolean; // 是否是自己撤回的
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}

View File

@@ -1,4 +1,97 @@
import { QQLevel, NTSex } from './user';
export interface GroupExtInfo {
groupCode: string;
resultCode: number;
extInfo: EXTInfo;
}
export interface GroupExtFilter {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: number;
lightCharNum: number;
luckyWord: number;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: number;
groupOwnerId: number;
essentialMsgPrivilege: number;
msgEventSeq: number;
inviteRobotSwitch: number;
gangUpId: number;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: number;
groupBindGuildIds: number;
viewedMsgDisappearTime: number;
groupExtFlameData: number;
groupBindGuildSwitch: number;
groupAioBindGuildId: number;
groupExcludeGuildIds: number;
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: number;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
};
export interface EXTInfo {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: string;
lightCharNum: number;
luckyWord: string;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: string;
groupOwnerId: GroupOwnerID;
essentialMsgPrivilege: number;
msgEventSeq: string;
inviteRobotSwitch: number;
gangUpId: string;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: string;
groupBindGuildIds: GroupGuildIDS;
viewedMsgDisappearTime: string;
groupExtFlameData: GroupEXTFlameData;
groupBindGuildSwitch: number;
groupAioBindGuildId: string;
groupExcludeGuildIds: GroupGuildIDS;
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: string;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
}
export interface GroupGuildIDS {
guildIds: any[];
}
export interface GroupEXTFlameData {
switchState: number;
state: number;
dayNums: any[];
version: number;
updateTime: string;
isDisplayDayNum: boolean;
}
export interface GroupOwnerID {
memberUin: string;
memberUid: string;
memberQid: string;
}
export interface KickMemberInfo {
optFlag: number;
@@ -7,6 +100,185 @@ export interface KickMemberInfo {
optBytesMsg: string;
}
export interface GroupDetailInfoV2Param {
groupCode: string;
filter: Filter;
modifyInfo: ModifyInfo;
}
export interface Filter {
noCodeFingerOpenFlag: number;
noFingerOpenFlag: number;
groupName: number;
classExt: number;
classText: number;
fingerMemo: number;
richFingerMemo: number;
tagRecord: number;
groupGeoInfo: FilterGroupGeoInfo;
groupExtAdminNum: number;
flag: number;
groupMemo: number;
groupAioSkinUrl: number;
groupBoardSkinUrl: number;
groupCoverSkinUrl: number;
groupGrade: number;
activeMemberNum: number;
certificationType: number;
certificationText: number;
groupNewGuideLines: FilterGroupNewGuideLines;
groupFace: number;
addOption: number;
shutUpTime: number;
groupTypeFlag: number;
appPrivilegeFlag: number;
appPrivilegeMask: number;
groupExtOnly: GroupEXTOnly;
groupSecLevel: number;
groupSecLevelInfo: number;
subscriptionUin: number;
subscriptionUid: string;
allowMemberInvite: number;
groupQuestion: number;
groupAnswer: number;
groupFlagExt3: number;
groupFlagExt3Mask: number;
groupOpenAppid: number;
rootId: number;
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
hlGuildOrgId: number;
groupFlagExt4: number;
groupFlagExt4Mask: number;
groupSchoolInfo: FilterGroupSchoolInfo;
groupCardPrefix: FilterGroupCardPrefix;
allianceId: number;
groupFlagPro1: number;
groupFlagPro1Mask: number;
}
export interface FilterGroupCardPrefix {
introduction: number;
rptPrefix: number;
}
export interface GroupEXTOnly {
tribeId: number;
moneyForAddGroup: number;
}
export interface FilterGroupGeoInfo {
ownerUid: number;
setTime: number;
cityId: number;
longitude: number;
latitude: number;
geoContent: number;
poiId: number;
}
export interface FilterGroupNewGuideLines {
enabled: number;
content: number;
}
export interface FilterGroupSchoolInfo {
location: number;
grade: number;
school: number;
}
export interface ModifyInfo {
noCodeFingerOpenFlag: number;
noFingerOpenFlag: number;
groupName: string;
classExt: number;
classText: string;
fingerMemo: string;
richFingerMemo: string;
tagRecord: any[];
groupGeoInfo: ModifyInfoGroupGeoInfo;
groupExtAdminNum: number;
flag: number;
groupMemo: string;
groupAioSkinUrl: string;
groupBoardSkinUrl: string;
groupCoverSkinUrl: string;
groupGrade: number;
activeMemberNum: number;
certificationType: number;
certificationText: string;
groupNewGuideLines: ModifyInfoGroupNewGuideLines;
groupFace: number;
addOption: number;// 0 空设置 1 任何人都可以进入 2 需要管理员批准 3 不允许任何人入群 4 问题进入答案 5 问题管理员批准
shutUpTime: number;
groupTypeFlag: number;
appPrivilegeFlag: number;
// 需要管理员审核
// 0000 0000 0000 0000 0000 0000 0000
// 无需审核入群
// 0000 0001 0000 0000 0000 0000 0000
// 成员数100内无审核
// 0100 0000 0000 0000 0000 0000 0000
// 禁用 群成员邀请好友
// 0100 0000 0000 0000 0000 0000 0000
appPrivilegeMask: number;
// 0110 0001 0000 0000 0000 0000 0000
// 101711872
groupExtOnly: GroupEXTOnly;
groupSecLevel: number;
groupSecLevelInfo: number;
subscriptionUin: string;
subscriptionUid: string;
allowMemberInvite: number;
groupQuestion: string;
groupAnswer: string;
groupFlagExt3: number;
groupFlagExt3Mask: number;
groupOpenAppid: number;
rootId: string;
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
hlGuildOrgId: number;
groupFlagExt4: number;
groupFlagExt4Mask: number;
groupSchoolInfo: ModifyInfoGroupSchoolInfo;
groupCardPrefix: ModifyInfoGroupCardPrefix;
allianceId: string;
groupFlagPro1: number;
groupFlagPro1Mask: number;
}
export interface ModifyInfoGroupCardPrefix {
introduction: string;
rptPrefix: any[];
}
export interface ModifyInfoGroupGeoInfo {
ownerUid: string;
SetTime: number;
CityId: number;
Longitude: string;
Latitude: string;
GeoContent: string;
poiId: string;
}
export interface ModifyInfoGroupNewGuideLines {
enabled: boolean;
content: string;
}
export interface ModifyInfoGroupSchoolInfo {
location: string;
grade: number;
school: string;
}
// 获取群详细信息的来源类型
export enum GroupInfoSource {
KUNSPECIFIED,

View File

@@ -48,6 +48,12 @@ export async function NCoreInitFramework(
});
}
//直到登录成功后,执行下一步
// const selfInfo = {
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',
// uin: '3684714082',
// nick: '',
// online: true
// }
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener();
loginListener.onQRCodeLoginSucceed = async (loginResult) => {

View File

@@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');
async function initializeNapCat(session, loginService, registerCallback) {
//const logFile = path.join(currentPath, 'napcat.log');
console.log('[NapCat] [Info] 开始初始化NapCat');
//fs.writeFileSync(logFile, '', { flag: 'w' });
//fs.writeFileSync(logFile, '[NapCat] [Info] NapCat 初始化成功\n', { flag: 'a' });
try {
const currentPath = path.dirname(__filename);
const { NCoreInitFramework } = await import('file://' + path.join(currentPath, './napcat.mjs'));
await NCoreInitFramework(session, loginService, (callback) => { registerCallback(callback) });
} catch (error) {
console.log('[NapCat] [Error] 初始化NapCat', error);
//fs.writeFileSync(logFile, `[NapCat] [Error] 初始化NapCat失败: ${error.message}\n`, { flag: 'a' });
}
}
module.exports = {
initializeNapCat: initializeNapCat
};

View File

@@ -0,0 +1,28 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
add_type: Type.Number(),
group_question: Type.Optional(Type.String()),
group_answer: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupAddOption extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupAddOption;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupAddOption(payload.group_id, {
addOption: payload.add_type,
groupQuestion: payload.group_question,
groupAnswer: payload.group_answer,
});
if (ret.result != 0) {
throw new Error(`设置群添加选项失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
user_id: Type.Array(Type.String()),
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupKickMembers extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupKickMembers;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
const rejectReq = payload.reject_add_request?.toString() == 'true';
const uids: string[] = await Promise.all(payload.user_id.map(async uin => await this.core.apis.UserApi.getUidByUinV2(uin)));
await this.core.apis.GroupApi.kickMember(payload.group_id.toString(), uids.filter(uid => !!uid), rejectReq);
return null;
}
}

View File

@@ -0,0 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
robot_member_switch: Type.Optional(Type.Number()),
robot_member_examine: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupRobotAddOption extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupRobotAddOption;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupRobotAddOption(
payload.group_id,
payload.robot_member_switch,
payload.robot_member_examine,
);
if (ret.result != 0) {
throw new Error(`设置群机器人添加选项失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -0,0 +1,26 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
no_code_finger_open: Type.Optional(Type.Number()),
no_finger_open: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupSearch extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupSearch;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupSearch(payload.group_id, {
noCodeFingerOpenFlag: payload.no_code_finger_open,
noFingerOpenFlag: payload.no_finger_open,
});
if (ret.result != 0) {
throw new Error(`设置群搜索失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -4,7 +4,10 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
folder_name: Type.String(),
// 兼容gocq 与name二选一
folder_name: Type.Optional(Type.String()),
// 兼容gocq 与folder_name二选一
name: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
@@ -16,6 +19,7 @@ export class CreateGroupFileFolder extends OneBotAction<Payload, ResponseType>
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
const folderName = payload.folder_name || payload.name;
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), folderName!)).resultWithGroupItem;
}
}

View File

@@ -0,0 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GetGroupDetailInfo extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetGroupDetailInfo;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
return {
...data,
group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
group_remark: '',
group_id: +payload.group_id,
group_name: data.groupName,
member_count: data.memberNum,
max_member_count: data.maxMemberNum,
};
}
}

View File

@@ -4,6 +4,7 @@ import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types';
interface RetData {
invited_requests: Notify[];
InvitedRequest: Notify[];
join_requests: Notify[];
}
@@ -13,7 +14,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
async _handle(): Promise<RetData> {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
const retData: RetData = { InvitedRequest: [], join_requests: [] };
const retData: RetData = { invited_requests: [], InvitedRequest: [], join_requests: [] };
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
@@ -38,7 +39,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
});
await Promise.all(notifyPromises);
retData.invited_requests = retData.InvitedRequest;
return retData;
}
}

View File

@@ -8,10 +8,16 @@ interface GroupNotice {
notice_id: string;
message: {
text: string
// 保持一段时间兼容性 防止以往版本出现问题 后续版本可考虑移除
image: Array<{
height: string
width: string
id: string
}>,
images: Array<{
height: string
width: string
id: string
}>
};
}
@@ -40,15 +46,18 @@ export class GetGroupNotice extends OneBotAction<Payload, GroupNotice[]> {
continue;
}
const retApiNotice: WebApiGroupNoticeFeed = ret.feeds[key];
const image = retApiNotice.msg.pics?.map((pic) => {
return { id: pic.id, height: pic.h, width: pic.w };
}) || [];
const retNotice: GroupNotice = {
notice_id: retApiNotice.fid,
sender_id: retApiNotice.u,
publish_time: retApiNotice.pubt,
message: {
text: retApiNotice.msg.text,
image: retApiNotice.msg.pics?.map((pic) => {
return { id: pic.id, height: pic.h, width: pic.w };
}) || [],
image,
images: image,
},
};
retNotices.push(retNotice);

View File

@@ -116,10 +116,22 @@ import { CleanCache } from './system/CleanCache';
import SetFriendRemark from './user/SetFriendRemark';
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
import SetGroupAddOption from './extends/SetGroupAddOption';
import SetGroupSearch from './extends/SetGroupSearch';
import SetGroupRobotAddOption from './extends/SetGroupRobotAddOption';
import SetGroupKickMembers from './extends/SetGroupKickMembers';
import { GetGroupDetailInfo } from './group/GetGroupDetailInfo';
import GetGroupAddRequest from './extends/GetGroupAddRequest';
import { GetCollectionList } from './extends/GetCollectionList';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [
new GetGroupDetailInfo(obContext, core),
new SetGroupKickMembers(obContext, core),
new SetGroupAddOption(obContext, core),
new SetGroupRobotAddOption(obContext, core),
new SetGroupSearch(obContext, core),
new SetDoubtFriendsAddRequest(obContext, core),
new GetDoubtFriendsAddRequest(obContext, core),
new SetFriendRemark(obContext, core),
@@ -247,6 +259,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetPrivateFileUrl(obContext, core),
new GetUnidirectionalFriendList(obContext, core),
new CleanCache(obContext, core),
new GetGroupAddRequest(obContext, core),
new GetCollectionList(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];

View File

@@ -19,6 +19,7 @@ import { rawMsgWithSendMsg } from '@/core/packet/message/converter';
export interface ReturnDataType {
message_id: number;
res_id?: string;
forward_id?: string;
}
export enum ContextMode {
@@ -147,7 +148,10 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
peerUid: peer.peerUid,
chatType: peer.chatType,
}, (returnMsgAndResId.message).msgId);
return { message_id: msgShortId!, res_id: returnMsgAndResId.res_id! };
// 对gocq的forward_id进行兼容
const resId = returnMsgAndResId.res_id!;
return { message_id: msgShortId!, res_id: resId, forward_id: resId };
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
throw Error(`发送转发消息res_id${returnMsgAndResId.res_id} 失败`);
}

View File

@@ -10,6 +10,10 @@ export interface InvalidCheckResult {
}
export const ActionName = {
SetGroupKickMembers: 'set_group_kick_members',
SetGroupRobotAddOption: 'set_group_robot_add_option',
SetGroupAddOption: 'set_group_add_option',
SetGroupSearch: 'set_group_search',
// new extends 完全差异OneBot类别
GetDoubtFriendsAddRequest: 'get_doubt_friends_add_request',
SetDoubtFriendsAddRequest: 'set_doubt_friends_add_request',
@@ -59,7 +63,7 @@ export const ActionName = {
GetStatus: 'get_status',
GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart',
CleanCache : 'clean_cache',
CleanCache: 'clean_cache',
Exit: 'bot_exit',
// go-cqhttp
SetQQProfile: 'set_qq_profile',
@@ -128,6 +132,7 @@ export const ActionName = {
FetchEmojiLike: 'fetch_emoji_like',
SetInputStatus: 'set_input_status',
GetGroupInfoEx: 'get_group_info_ex',
GetGroupDetailInfo: 'get_group_detail_info',
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request',
DelGroupNotice: '_del_group_notice',
FriendPoke: 'friend_poke',

View File

@@ -4,6 +4,7 @@ import { ActionName } from '@/onebot/action/router';
import { Notify } from '@/onebot/types';
interface RetData {
invited_requests: Notify[];
InvitedRequest: Notify[];
join_requests: Notify[];
}
@@ -13,7 +14,7 @@ export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
async _handle(): Promise<RetData> {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50);
const retData: RetData = { InvitedRequest: [], join_requests: [] };
const retData: RetData = { invited_requests: [], InvitedRequest: [], join_requests: [] };
const notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
@@ -39,6 +40,7 @@ export class GetGroupSystemMsg extends OneBotAction<void, RetData> {
await Promise.all(notifyPromises);
retData.invited_requests = retData.InvitedRequest;
return retData;
}
}

View File

@@ -1209,7 +1209,6 @@ export class OneBotMsgApi {
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
const groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
const isAdminOrOwner = groupRole === 3 || groupRole === 4;
if (isAdminOrOwner && !operatorUid) {
let dataNotify: GroupNotify | undefined;
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
@@ -1239,7 +1238,7 @@ export class OneBotMsgApi {
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
groupChange.operatorInfo ? new TextDecoder('utf-8').decode(groupChange.operatorInfo) : undefined
);
return new OB11GroupIncreaseEvent(
this.core,
@@ -1251,13 +1250,42 @@ export class OneBotMsgApi {
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
let operator_uid_parse: string | undefined = undefined;
if (groupChange.operatorInfo) {
// 先判断是否可能是protobuf自身被踢出或以0a开头
if (groupChange.decreaseType === 3 || Buffer.from(groupChange.operatorInfo).toString('hex').startsWith('0a')) {
// 可能是protobuf尝试解析
try {
operator_uid_parse = new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid;
} catch (error) {
// protobuf解析失败fallback到字符串解析
try {
const decoded = new TextDecoder('utf-8').decode(groupChange.operatorInfo);
// 检查是否包含非ASCII字符如果包含则丢弃
const isAsciiOnly = [...decoded].every(char => char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126);
operator_uid_parse = isAsciiOnly ? decoded : '';
} catch (e2) {
operator_uid_parse = '';
}
}
} else {
// 直接进行字符串解析
try {
const decoded = new TextDecoder('utf-8').decode(groupChange.operatorInfo);
// 检查是否包含非ASCII字符如果包含则丢弃
const isAsciiOnly = [...decoded].every(char => char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126);
operator_uid_parse = isAsciiOnly ? decoded : '';
} catch (e) {
operator_uid_parse = '';
}
}
}
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString()
operator_uid_parse
);
if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => {

View File

@@ -270,7 +270,6 @@ export class NapCatOneBot11Adapter {
);
}
};
msgListener.onAddSendMsg = async (msg) => {
try {
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
@@ -282,7 +281,8 @@ export class NapCatOneBot11Adapter {
}, 1, 10 * 60 * 1000);
// 10分钟 超时
const updatemsg = updatemsgs.find((e) => e.msgId === msg.msgId);
if (updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS || updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS_NOSEQ) {
// updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS_NOSEQ NOSEQ一般是服务器未下发SEQ 这意味着这条消息不应该推送network
if (updatemsg?.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS) {
updatemsg.id = MessageUnique.createUniqueMsgId(
{
chatType: updatemsg.chatType,
@@ -304,8 +304,18 @@ export class NapCatOneBot11Adapter {
peerUid: uid,
guildId: ''
};
const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
let msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
const element = msg?.elements.find(e => !!e.grayTipElement?.revokeElement);
if (element?.grayTipElement?.revokeElement.isSelfOperate && msg) {
await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onMsgRecall',
(chatType: ChatType, uid: string, msgSeq: string) => {
return chatType === msg?.chatType && uid === msg?.peerUid && msgSeq === msg?.msgSeq;
}
).catch(() => {
msg = undefined;
this.context.logger.logDebug('自操作消息撤回事件');
});
}
if (msg && element) {
const recallEvent = await this.emitRecallMsg(msg, element);
try {
@@ -316,6 +326,7 @@ export class NapCatOneBot11Adapter {
this.context.logger.logError('处理消息撤回失败', e);
}
}
};
msgListener.onKickedOffLine = async (kick) => {
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc);

View File

@@ -87,8 +87,8 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
this.app.use(async (req, res) => {
await this.handleRequest(req, res);
});
this.server.listen(this.config.port, () => {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.config.port}`);
this.server.listen(this.config.port, this.config.host, () => {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On ${this.config.host}:${this.config.port}`);
});
}

View File

@@ -1,82 +0,0 @@
# QRCode Terminal Edition [![Build Status][travis-ci-img]][travis-ci-url]
> Going where no QRCode has gone before.
![Basic Example][basic-example-img]
# Node Library
## Install
Can be installed with:
$ npm install qrcode-terminal
and used:
var qrcode = require('qrcode-terminal');
## Usage
To display some data to the terminal just call:
qrcode.generate('This will be a QRCode, eh!');
You can even specify the error level (default is 'L'):
qrcode.setErrorLevel('Q');
qrcode.generate('This will be a QRCode with error level Q!');
If you don't want to display to the terminal but just want to string you can provide a callback:
qrcode.generate('http://github.com', function (qrcode) {
console.log(qrcode);
});
If you want to display small output, provide `opts` with `small`:
qrcode.generate('This will be a small QRCode, eh!', {small: true});
qrcode.generate('This will be a small QRCode, eh!', {small: true}, function (qrcode) {
console.log(qrcode)
});
# Command-Line
## Install
$ npm install -g qrcode-terminal
## Usage
$ qrcode-terminal --help
$ qrcode-terminal 'http://github.com'
$ echo 'http://github.com' | qrcode-terminal
# Support
- OS X
- Linux
- Windows
# Server-side
[node-qrcode][node-qrcode-url] is a popular server-side QRCode generator that
renders to a `canvas` object.
# Developing
To setup the development envrionment run `npm install`
To run tests run `npm test`
# Contributers
Gord Tanner <gtanner@gmail.com>
Micheal Brooks <michael@michaelbrooks.ca>
[travis-ci-img]: https://travis-ci.org/gtanner/qrcode-terminal.png
[travis-ci-url]: https://travis-ci.org/gtanner/qrcode-terminal
[basic-example-img]: https://raw.github.com/gtanner/qrcode-terminal/master/example/basic.png
[node-qrcode-url]: https://github.com/soldair/node-qrcode

536
src/remote/pipe.ts Normal file
View File

@@ -0,0 +1,536 @@
import * as net from 'net';
import { randomUUID } from 'crypto';
import { EventEmitter } from 'events';
export interface Packet<T = any> {
command: string;
trace: string;
data: T;
type: 'listener_callback' | 'event_response' | 'event_request' | 'default';
}
// 协议常量
const PROTOCOL_MAGIC = 0x4E415043; // 'NAPC'
const PROTOCOL_VERSION = 0x01;
const HEADER_SIZE = 12;
const MAX_PACKET_SIZE = 16 * 1024 * 1024; // 降低到16MB
const BUFFER_HIGH_WATER_MARK = 2 * 1024 * 1024; // 2MB背压阈值
const BUFFER_LOW_WATER_MARK = 512 * 1024; // 512KB恢复阈值
// 高效缓冲区管理器
class BufferManager {
private buffers: Buffer[] = [];
private totalSize: number = 0;
private readOffset: number = 0;
private isHighWaterMark: boolean = false;
// 添加数据
append(data: Buffer): void {
this.buffers.push(data);
this.totalSize += data.length;
// 检查背压
if (!this.isHighWaterMark && this.totalSize > BUFFER_HIGH_WATER_MARK) {
this.isHighWaterMark = true;
}
}
// 消费数据
consume(length: number): Buffer {
if (length > this.available) {
throw new Error('消费长度超过可用数据');
}
const result = Buffer.allocUnsafe(length);
let resultOffset = 0;
let remaining = length;
while (remaining > 0 && this.buffers.length > 0) {
const currentBuffer = this.buffers[0];
if (!currentBuffer?.[0]) continue;
const availableInCurrent = currentBuffer.length - this.readOffset;
const toCopy = Math.min(remaining, availableInCurrent);
currentBuffer.copy(result, resultOffset, this.readOffset, this.readOffset + toCopy);
resultOffset += toCopy;
remaining -= toCopy;
this.readOffset += toCopy;
// 如果当前buffer用完了移除它
if (this.readOffset >= currentBuffer.length) {
this.buffers.shift();
this.readOffset = 0;
}
}
this.totalSize -= length;
// 检查是否可以恢复读取
if (this.isHighWaterMark && this.totalSize < BUFFER_LOW_WATER_MARK) {
this.isHighWaterMark = false;
}
return result;
}
// 预览数据(不消费)
peek(length: number): Buffer | null {
if (length > this.available) {
return null;
}
const result = Buffer.allocUnsafe(length);
let resultOffset = 0;
let remaining = length;
let bufferIndex = 0;
let currentReadOffset = this.readOffset;
while (remaining > 0 && bufferIndex < this.buffers.length) {
const currentBuffer = this.buffers[bufferIndex];
if (!currentBuffer) continue;
const availableInCurrent = currentBuffer.length - currentReadOffset;
const toCopy = Math.min(remaining, availableInCurrent);
currentBuffer.copy(result, resultOffset, currentReadOffset, currentReadOffset + toCopy);
resultOffset += toCopy;
remaining -= toCopy;
if (currentReadOffset + toCopy >= currentBuffer.length) {
bufferIndex++;
currentReadOffset = 0;
} else {
currentReadOffset += toCopy;
}
}
return result;
}
get available(): number {
return this.totalSize;
}
get shouldPause(): boolean {
return this.isHighWaterMark;
}
reset(): void {
this.buffers = [];
this.totalSize = 0;
this.readOffset = 0;
this.isHighWaterMark = false;
}
}
// 简化的数据包管理器
class PacketManager {
static pack(packet: Packet): Buffer {
const jsonStr = JSON.stringify(packet);
const jsonBuffer = Buffer.from(jsonStr, 'utf8');
if (jsonBuffer.length > MAX_PACKET_SIZE - HEADER_SIZE) {
throw new Error(`数据包过大: ${jsonBuffer.length}`);
}
const buffer = Buffer.allocUnsafe(HEADER_SIZE + jsonBuffer.length);
buffer.writeUInt32BE(PROTOCOL_MAGIC, 0);
buffer.writeUInt32BE(jsonBuffer.length, 4);
buffer.writeUInt32BE(PROTOCOL_VERSION, 8);
jsonBuffer.copy(buffer, HEADER_SIZE);
return buffer;
}
static unpack(bufferManager: BufferManager): Packet[] {
const packets: Packet[] = [];
while (bufferManager.available >= HEADER_SIZE) {
// 检查魔数
const header = bufferManager.peek(HEADER_SIZE);
if (!header) break;
const magic = header.readUInt32BE(0);
if (magic !== PROTOCOL_MAGIC) {
// 简单的同步恢复:跳过一个字节
bufferManager.consume(1);
continue;
}
const dataLength = header.readUInt32BE(4);
//const version = header.readUInt32BE(8);
// 基本验证
if (dataLength <= 0 || dataLength > MAX_PACKET_SIZE - HEADER_SIZE) {
bufferManager.consume(1);
continue;
}
// 检查完整包
const totalSize = HEADER_SIZE + dataLength;
if (bufferManager.available < totalSize) {
break;
}
// 消费完整包
bufferManager.consume(HEADER_SIZE);
const jsonBuffer = bufferManager.consume(dataLength);
try {
const packet = JSON.parse(jsonBuffer.toString('utf8')) as Packet;
if (this.isValidPacket(packet)) {
packets.push(packet);
}
} catch (error) {
console.error('JSON解析失败:', error);
}
}
return packets;
}
private static isValidPacket(packet: any): packet is Packet {
return packet &&
typeof packet.command === 'string' &&
typeof packet.trace === 'string' &&
packet.data !== undefined &&
['listener_callback', 'event_response', 'event_request', 'default'].includes(packet.type);
}
static createRequest<T = any>(command: string, data: T, trace?: string): Packet<T> {
return {
command,
trace: trace || randomUUID(),
data,
type: 'event_request'
};
}
static createResponse<T = any>(trace: string, data: T, command = ''): Packet<T> {
return {
command,
trace,
data,
type: 'event_response'
};
}
static createCallback<T = any>(command: string, data: T, trace?: string): Packet<T> {
return {
command,
trace: trace || randomUUID(),
data,
type: 'listener_callback'
};
}
}
// 响应助手类
class ResponseHelper {
private responseSent = false;
constructor(private socket: net.Socket, private trace: string, private command: string = '') { }
success<T = any>(data: T): void {
if (this.responseSent) return;
const response = PacketManager.createResponse(this.trace, data, this.command);
this.writePacket(response);
this.responseSent = true;
}
error(message: string, code = 500): void {
if (this.responseSent) return;
const response = PacketManager.createResponse(this.trace, { error: message, code }, this.command);
this.writePacket(response);
this.responseSent = true;
}
sendEventResponse<T = any>(trace: string, data: T): void {
const response = PacketManager.createResponse(trace, data, this.command);
this.writePacket(response);
}
sendListenerCallback<T = any>(command: string, data: T): void {
const callback = PacketManager.createCallback(command, data);
this.writePacket(callback);
}
private writePacket(packet: Packet): void {
console.log(`发送数据包: ${packet.command}, trace: ${packet.trace} (${packet.type}) `);
if (!this.socket.destroyed) {
const buffer = PacketManager.pack(packet);
this.socket.write(buffer);
}
}
get hasResponseSent(): boolean {
return this.responseSent;
}
}
// 带背压控制的Socket包装器
class ManagedSocket {
private bufferManager = new BufferManager();
private isPaused = false;
constructor(private socket: net.Socket, private onPacket: (packet: Packet) => void) {
this.setupSocket();
}
private setupSocket(): void {
this.socket.on('data', (chunk) => {
this.bufferManager.append(chunk);
// 背压控制
if (this.bufferManager.shouldPause && !this.isPaused) {
this.socket.pause();
this.isPaused = true;
console.warn('Socket暂停读取 - 缓冲区过大');
}
this.processPackets();
});
this.socket.on('drain', () => {
// 当socket的写缓冲区有空间时检查是否可以恢复读取
if (this.isPaused && !this.bufferManager.shouldPause) {
this.socket.resume();
this.isPaused = false;
console.log('Socket恢复读取');
}
});
}
private processPackets(): void {
try {
const packets = PacketManager.unpack(this.bufferManager);
packets.forEach(packet => this.onPacket(packet));
// 处理完包后检查是否可以恢复读取
if (this.isPaused && !this.bufferManager.shouldPause) {
this.socket.resume();
this.isPaused = false;
console.log('Socket恢复读取');
}
} catch (error) {
console.error('处理数据包失败:', error);
this.bufferManager.reset();
if (this.isPaused) {
this.socket.resume();
this.isPaused = false;
}
}
}
write(buffer: Buffer): boolean {
return this.socket.write(buffer);
}
destroy(): void {
this.socket.destroy();
}
get destroyed(): boolean {
return this.socket.destroyed;
}
}
type PacketHandler = (packet: Packet, helper: ResponseHelper) => Promise<any> | any;
// 简化的管道服务端
class PipeServer extends EventEmitter {
private server: net.Server;
private clients: Map<net.Socket, ManagedSocket> = new Map();
private handler: PacketHandler | null = null;
constructor(private pipeName: string) {
super();
this.server = net.createServer();
this.setupServer();
}
private setupServer(): void {
this.server.on('connection', (socket) => {
console.log('客户端连接');
const managedSocket = new ManagedSocket(socket, (packet) => {
this.handlePacket(packet, socket);
});
this.clients.set(socket, managedSocket);
socket.on('close', () => {
console.log('客户端断开');
this.clients.delete(socket);
});
socket.on('error', (error) => {
console.error('Socket错误:', error);
this.clients.delete(socket);
});
});
}
registerHandler(handler: PacketHandler): void {
this.handler = handler;
}
private async handlePacket(packet: Packet, socket: net.Socket): Promise<void> {
if (packet.type === 'event_response' || packet.type === 'listener_callback') {
this.emit(packet.type, packet);
return;
}
const helper = new ResponseHelper(socket, packet.trace, packet.command);
if (!this.handler) {
helper.error('未注册处理器');
return;
}
try {
const result = await this.handler(packet, helper);
if (result !== undefined && !helper.hasResponseSent) {
helper.success(result);
}
} catch (error) {
if (!helper.hasResponseSent) {
const message = error instanceof Error ? error.message : String(error);
helper.error(message);
}
}
}
async start(): Promise<void> {
return new Promise((resolve, reject) => {
this.server.listen(this.pipeName, () => {
console.log(`管道服务器启动: ${this.pipeName}`);
resolve();
});
this.server.on('error', reject);
});
}
async stop(): Promise<void> {
return new Promise((resolve) => {
this.clients.forEach((managedSocket) => managedSocket.destroy());
this.clients.clear();
this.server.close(() => {
console.log('管道服务器停止');
resolve();
});
});
}
broadcast<T = any>(command: string, data: T, type: Packet['type'] = 'default'): void {
const packet: Packet<T> = {
command,
trace: randomUUID(),
data,
type
};
const buffer = PacketManager.pack(packet);
this.clients.forEach((managedSocket) => {
if (!managedSocket.destroyed) {
managedSocket.write(buffer);
}
});
}
get clientCount(): number {
return this.clients.size;
}
}
// 简化的管道客户端
class PipeClient extends EventEmitter {
private socket: net.Socket | null = null;
private managedSocket: ManagedSocket | null = null;
private isConnected = false;
private handler: PacketHandler | null = null;
constructor(private pipeName: string) {
super();
}
registerHandler(handler: PacketHandler): void {
this.handler = handler;
}
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = net.createConnection(this.pipeName);
this.managedSocket = new ManagedSocket(this.socket, (packet) => {
this.handlePacket(packet);
});
this.socket.on('connect', () => {
console.log('连接到管道服务器');
this.isConnected = true;
resolve();
});
this.socket.on('close', () => {
console.log('与服务器断开连接');
this.isConnected = false;
this.emit('disconnect');
});
this.socket.on('error', (error) => {
console.error('Socket错误:', error);
this.isConnected = false;
reject(error);
});
});
}
private async handlePacket(packet: Packet): Promise<void> {
if (this.handler && this.socket) {
const helper = new ResponseHelper(this.socket, packet.trace, packet.command);
try {
await this.handler(packet, helper);
} catch (error) {
console.error('处理数据包失败:', error);
}
}
}
sendRequest<T = any>(command: string, data: T, trace?: string): void {
if (!this.isConnected || !this.managedSocket) {
throw new Error('未连接到服务器');
}
const packet = PacketManager.createRequest(command, data, trace);
const buffer = PacketManager.pack(packet);
this.managedSocket.write(buffer);
}
sendResponse<T = any>(trace: string, data: T, command = ''): void {
if (!this.isConnected || !this.managedSocket) {
throw new Error('未连接到服务器');
}
const packet = PacketManager.createResponse(trace, data, command);
const buffer = PacketManager.pack(packet);
this.managedSocket.write(buffer);
}
disconnect(): void {
if (this.managedSocket) {
this.managedSocket.destroy();
this.managedSocket = null;
}
this.socket = null;
this.isConnected = false;
}
get connected(): boolean {
return this.isConnected;
}
}
export { PipeServer, PipeClient, PacketManager, ResponseHelper, BufferManager };

109
src/remote/remoteSession.ts Normal file
View File

@@ -0,0 +1,109 @@
import { createRemoteServiceClient } from "@/remote/service";
import {
NodeIQQNTWrapperSession,
WrapperSessionInitConfig
} from "../core/wrapper";
import { NodeIKernelSessionListener } from "../core/listeners/NodeIKernelSessionListener";
import {
NodeIDependsAdapter,
NodeIDispatcherAdapter
} from "../core/adapters";
import { ServiceNamingMapping } from "@/core";
class RemoteServiceManager {
private services: Map<string, any> = new Map();
private handler;
constructor(handler: (client: any, listenerCommand: string, ...args: any[]) => Promise<any>) {
this.handler = handler;
}
private createRemoteService<T extends keyof ServiceNamingMapping>(
serviceName: T
): ServiceNamingMapping[T] {
if (this.services.has(serviceName)) {
return this.services.get(serviceName);
}
let serviceClient: any;
serviceClient = createRemoteServiceClient(serviceName, async (serviceCommand, ...args) => {
return await this.handler(serviceClient, serviceCommand, ...args);
});
this.services.set(serviceName, serviceClient.object);
return serviceClient.object;
}
getService<T extends keyof ServiceNamingMapping>(
serviceName: T
): ServiceNamingMapping[T] {
return this.createRemoteService(serviceName);
}
}
export class RemoteWrapperSession implements NodeIQQNTWrapperSession {
private serviceManager: RemoteServiceManager;
constructor(handler: (client: { object: keyof ServiceNamingMapping, receiverListener: (command: string, ...args: any[]) => void }, listenerCommand: string, ...args: any[]) => Promise<void>) {
this.serviceManager = new RemoteServiceManager(handler);
}
create(): RemoteWrapperSession {
return this;
}
init(
_wrapperSessionInitConfig: WrapperSessionInitConfig,
_nodeIDependsAdapter: NodeIDependsAdapter,
_nodeIDispatcherAdapter: NodeIDispatcherAdapter,
_nodeIKernelSessionListener: NodeIKernelSessionListener,
): void {
}
startNT(_session?: number): void {
}
getBdhUploadService() { return null; }
getECDHService() { return this.serviceManager.getService('NodeIKernelECDHService'); }
getMsgService() { return this.serviceManager.getService('NodeIKernelMsgService'); }
getProfileService() { return this.serviceManager.getService('NodeIKernelProfileService'); }
getProfileLikeService() { return this.serviceManager.getService('NodeIKernelProfileLikeService'); }
getGroupService() { return this.serviceManager.getService('NodeIKernelGroupService'); }
getStorageCleanService() { return this.serviceManager.getService('NodeIKernelStorageCleanService'); }
getBuddyService() { return this.serviceManager.getService('NodeIKernelBuddyService'); }
getRobotService() { return this.serviceManager.getService('NodeIKernelRobotService'); }
getTicketService() { return this.serviceManager.getService('NodeIKernelTicketService'); }
getTipOffService() { return this.serviceManager.getService('NodeIKernelTipOffService'); }
getNodeMiscService() { return this.serviceManager.getService('NodeIKernelNodeMiscService'); }
getRichMediaService() { return this.serviceManager.getService('NodeIKernelRichMediaService'); }
getMsgBackupService() { return this.serviceManager.getService('NodeIKernelMsgBackupService'); }
getAlbumService() { return this.serviceManager.getService('NodeIKernelAlbumService'); }
getTianShuService() { return this.serviceManager.getService('NodeIKernelTianShuService'); }
getUnitedConfigService() { return this.serviceManager.getService('NodeIKernelUnitedConfigService'); }
getSearchService() { return this.serviceManager.getService('NodeIKernelSearchService'); }
getDirectSessionService() { return null; }
getRDeliveryService() { return null; }
getAvatarService() { return this.serviceManager.getService('NodeIKernelAvatarService'); }
getFeedChannelService() { return null; }
getYellowFaceService() { return null; }
getCollectionService() { return this.serviceManager.getService('NodeIKernelCollectionService'); }
getSettingService() { return null; }
getQiDianService() { return null; }
getFileAssistantService() { return this.serviceManager.getService('NodeIKernelFileAssistantService'); }
getGuildService() { return null; }
getSkinService() { return null; }
getTestPerformanceService() { return this.serviceManager.getService('NodeIkernelTestPerformanceService'); }
getQQPlayService() { return null; }
getDbToolsService() { return this.serviceManager.getService('NodeIKernelDbToolsService'); }
getUixConvertService() { return this.serviceManager.getService('NodeIKernelUixConvertService'); }
getOnlineStatusService() { return this.serviceManager.getService('NodeIKernelOnlineStatusService'); }
getRemotingService() { return null; }
getGroupTabService() { return null; }
getGroupSchoolService() { return null; }
getLiteBusinessService() { return null; }
getGuildMsgService() { return null; }
getLockService() { return null; }
getMSFService() { return this.serviceManager.getService('NodeIKernelMSFService'); }
getGuildHotUpdateService() { return null; }
getAVSDKService() { return null; }
getRecentContactService() { return this.serviceManager.getService('NodeIKernelRecentContactService'); }
getConfigMgrService() { return null; }
}

1210
src/remote/serialize.cpp Normal file

File diff suppressed because it is too large Load Diff

131
src/remote/serialize.ts Normal file
View File

@@ -0,0 +1,131 @@
interface EncodedValue {
$type: string;
$value?: unknown;
}
interface EncodedNull {
$type: "null";
}
interface EncodedUndefined {
$type: "undefined";
}
interface EncodedPrimitive {
$type: "number" | "string" | "boolean";
$value: number | string | boolean;
}
interface EncodedBuffer {
$type: "Buffer";
$value: string;
}
interface EncodedMap {
$type: "Map";
$value: [EncodedValue, EncodedValue][];
}
interface EncodedArray {
$type: "Array";
$value: EncodedValue[];
}
interface EncodedObject {
$type: "Object";
$value: { [key: string]: EncodedValue };
}
type SerializedValue = EncodedNull | EncodedUndefined | EncodedPrimitive | EncodedBuffer | EncodedMap | EncodedArray | EncodedObject;
function rpc_encode<T>(value: T): SerializedValue {
if (value === null) return { $type: "null" };
if (value === undefined) return { $type: "undefined" };
if (typeof value === "number") return { $type: "number", $value: value };
if (typeof value === "string") return { $type: "string", $value: value };
if (typeof value === "boolean") return { $type: "boolean", $value: value };
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
// Buffer和Uint8Array都转成base64字符串
let base64: string = Buffer.from(value).toString("base64");
return { $type: "Buffer", $value: base64 };
}
if (value instanceof Map) {
let arr: [SerializedValue, SerializedValue][] = [];
for (let [k, v] of value.entries()) {
arr.push([rpc_encode(k), rpc_encode(v)]);
}
return { $type: "Map", $value: arr };
}
if (Array.isArray(value) || (typeof value === "object" && value !== null && typeof (value as unknown as ArrayLike<unknown>).length === "number")) {
// ArrayLike也认为是Array
let arr: SerializedValue[] = [];
const arrayLike = value as unknown as ArrayLike<unknown>;
for (let i = 0; i < arrayLike.length; i++) {
arr.push(rpc_encode(arrayLike[i]));
}
return { $type: "Array", $value: arr };
}
if (typeof value === "object" && value !== null) {
let obj: { [key: string]: SerializedValue } = {};
for (let k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
obj[k] = rpc_encode((value as Record<string, unknown>)[k]);
}
}
return { $type: "Object", $value: obj };
}
throw new Error("Unsupported type");
}
function rpc_decode<T = unknown>(obj: EncodedValue): T {
if (obj == null || typeof obj !== "object" || !("$type" in obj)) {
throw new Error("Invalid encoded object");
}
switch (obj.$type) {
case "null": return null as T;
case "undefined": return undefined as T;
case "number": return (obj as EncodedPrimitive).$value as T;
case "string": return (obj as EncodedPrimitive).$value as T;
case "boolean": return (obj as EncodedPrimitive).$value as T;
case "Buffer":
return Buffer.from((obj as EncodedBuffer).$value, "base64") as T;
case "Map":
{
let map = new Map();
for (let [k, v] of (obj as EncodedMap).$value) {
map.set(rpc_decode(k), rpc_decode(v));
}
return map as T;
}
case "Array":
{
let arr: unknown[] = [];
for (let item of (obj as EncodedArray).$value) {
arr.push(rpc_decode(item));
}
return arr as T;
}
case "Object":
{
let out: Record<string, unknown> = {};
for (let k in (obj as EncodedObject).$value) {
const value = (obj as EncodedObject).$value[k];
if (value !== undefined) {
out[k] = rpc_decode(value);
}
}
return out as T;
}
default:
throw new Error("Unknown $type: " + obj.$type);
}
}
export { rpc_encode, rpc_decode };
export type { SerializedValue };

114
src/remote/service.ts Normal file
View File

@@ -0,0 +1,114 @@
import { FuncKeys, NTEventWrapper } from "@/common/event";
import { ServiceNamingMapping } from "@/core";
export type ServiceMethodCommand = {
[Service in keyof ServiceNamingMapping]: `${Service}/${FuncKeys<ServiceNamingMapping[Service]>}`
}[keyof ServiceNamingMapping];
const LISTENER_COMMAND_PATTERN = /\/addKernel\w*Listener$/;
function isListenerCommand(command: ServiceMethodCommand): boolean {
return LISTENER_COMMAND_PATTERN.test(command);
}
export function createRemoteServiceServer<T extends keyof ServiceNamingMapping>(
serviceName: T,
ntevent: NTEventWrapper,
callback: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
): ServiceNamingMapping[T] {
return new Proxy(() => { }, {
get: (_target: any, functionName: string) => {
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
if (isListenerCommand(command)) {
return async (..._args: any[]) => {
const listener = new Proxy(new class { }(), {
apply: (_target, _thisArg, _arguments) => {
return callback(command, ..._arguments);
}
});
return await (ntevent.callNoListenerEvent as any)(command, listener);
};
}
return async (...args: any[]) => {
return await (ntevent.callNoListenerEvent as any)(command, ...args);
};
}
});
}
// 避免重复远程注册 多份传输会消耗很大
export const listenerCmdRegisted = new Map<ServiceMethodCommand, boolean>();
// 已经注册的Listener实例托管
export const clientCallback = new Map<string, Array<(...args: any[]) => Promise<any>>>();
export async function handleServiceServerOnce(
command: ServiceMethodCommand,// 服务注册命令
recvListener: (command: string, ...args: any[]) => Promise<any>,//listener监听器
ntevent: NTEventWrapper,// 事件处理器
...args: any[]//实际参数
) {
if (isListenerCommand(command)) {
if (!listenerCmdRegisted.has(command)) {
listenerCmdRegisted.set(command, true);
return (ntevent.callNoListenerEvent as any)(command, new Proxy(new class { }(), {
get: (_target: any, prop: string) => {
return async (..._args: any[]) => {
let listenerCmd = `${command.split('/')[0]}/${prop}`;
recvListener(listenerCmd, ..._args);
};
}
}));
}
return 0;
}
return await (ntevent.callNoListenerEvent as (command: ServiceMethodCommand, ...args: any[]) => Promise<any>)(command, ...args);
}
export function createRemoteServiceClient<T extends keyof ServiceNamingMapping>(
serviceName: T,
receiverEvent: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
) {
const object = new Proxy(() => { }, {
get: (_target: any, functionName: string) => {
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
if (isListenerCommand(command)) {
return async (listener: Record<string, any>) => {
for (const key in listener) {
if (typeof listener[key] === 'function') {
const listenerCmd = `${command.split('/')[0]}/${key}`;
if (!clientCallback.has(listenerCmd)) {
clientCallback.set(listenerCmd, [listener[key].bind(listener)]);
} else {
clientCallback.get(listenerCmd)?.push(listener[key].bind(listener));
}
}
}
return await receiverEvent(command);
};
}
return async (...args: any[]) => {
return await receiverEvent(command, ...args);
};
}
});
const receiverListener = async function (command: string, ...args: any[]) {
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
};
return { receiverListener: receiverListener, object: object as ServiceNamingMapping[T] };
}
export async function receiverServiceListener(
command: string,
...args: any[]
) {
if (clientCallback.has(command)) {
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
}
return 0;
}
export function clearServiceState() {
listenerCmdRegisted.clear();
clientCallback.clear();
}

23
src/remote/wrapper.ts Normal file
View File

@@ -0,0 +1,23 @@
import { NodeIKernelLoginService, NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil, WrapperNodeApi } from "@/core";
import { NodeIO3MiscService } from "@/core/services/NodeIO3MiscService";
import { dirname } from "path";
import { fileURLToPath } from "url";
export const LocalVirtualWrapper: WrapperNodeApi = {
NodeIO3MiscService: {
get: () => LocalVirtualWrapper.NodeIO3MiscService,
addO3MiscListener: () => 0,
setAmgomDataPiece: () => { },
reportAmgomWeather: () => { },
} as NodeIO3MiscService,
NodeQQNTWrapperUtil: {
get: () => LocalVirtualWrapper.NodeQQNTWrapperUtil,
getNTUserDataInfoConfig: function (): string {
let current_path = dirname(fileURLToPath(import.meta.url));
return current_path;
}
} as NodeQQNTWrapperUtil,
NodeIQQNTWrapperSession: {} as NodeIQQNTWrapperSession,
NodeIQQNTWrapperEngine: {} as NodeIQQNTWrapperEngine,
NodeIKernelLoginService: {} as NodeIKernelLoginService,
};

View File

@@ -47,6 +47,9 @@ export const CreateTerminalHandler: RequestHandler = async (req, res) => {
if (isMacOS) {
return sendError(res, 'MacOS不支持终端');
}
if ((await WebUiConfig.GetWebUIConfig()).token === 'napcat') {
return sendError(res, '默认密码禁止创建终端');
}
try {
const { cols, rows } = req.body;
const { id } = terminalManager.createTerminal(cols, rows);

View File

@@ -9,7 +9,7 @@ Object.defineProperty(global, '__dirname', {
// 注意:堆栈格式可能不同,请根据实际环境调整索引及正则表达式
for (const line of stack) {
const match = line.match(/\((.*):\d+:\d+\)/);
if (match) {
if (match?.[1]) {
callerFile = match[1];
if (!callerFile.includes('init-dynamic-dirname.ts')) {
break;

View File

@@ -1,13 +1,13 @@
import multer from 'multer';
import { WebUiConfigWrapper } from '../helper/config';
import path from 'path';
import fs from 'fs';
import type { Request, Response } from 'express';
import { WebUiConfig } from '@/webui';
export const webUIFontStorage = multer.diskStorage({
destination: (_, __, cb) => {
try {
const fontsPath = path.dirname(WebUiConfigWrapper.GetWebUIFontPath());
const fontsPath = path.dirname(WebUiConfig.GetWebUIFontPath());
// 确保字体目录存在
fs.mkdirSync(fontsPath, { recursive: true });
cb(null, fontsPath);

View File

@@ -53,6 +53,7 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
{ src: './src/framework/liteloader.cjs', dest: 'dist' },
{ src: './src/framework/napcat.cjs', dest: 'dist' },
{ src: './src/framework/nativeLoader.cjs', dest: 'dist' },
{ src: './src/framework/preload.cjs', dest: 'dist' },
{ src: './src/framework/renderer.js', dest: 'dist' },
{ src: './package.json', dest: 'dist' },