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", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "4.7.66", "version": "4.7.78",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,11 +13,15 @@ export class NapCatPathWrapper {
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) { constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath; this.binaryPath = mainPath;
let writePath: string; 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'); writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else { } else {
writePath = this.binaryPath; writePath = this.binaryPath;
} }
this.logsPath = path.join(writePath, 'logs'); this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config'); this.configPath = path.join(writePath, 'config');
this.cachePath = path.join(writePath, 'cache'); 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) { export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
return new Proxy<T>(listener, proxyHandlerOf(logger)); 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, GroupNotify,
GroupInfoSource, GroupInfoSource,
ShutUpGroupMember, ShutUpGroupMember,
Peer,
ChatType,
} from '@/core'; } from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper'; import { isNumeric, 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 { CancelableTask, TaskExecutor } from '@/common/cancel-task'; import { CancelableTask, TaskExecutor } from '@/common/cancel-task';
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
export class NTQQGroupApi { export class NTQQGroupApi {
context: InstanceContext; context: InstanceContext;
@@ -47,6 +50,22 @@ export class NTQQGroupApi {
this.initCache().then().catch(e => this.context.logger.logError(e)); 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() { async initCache() {
for (const group of await this.getGroups(true)) { for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e)); 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); 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) { async getGroups(forced: boolean = false) {
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2( const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupList', 'NodeIKernelGroupService/getGroupList',

View File

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

View File

@@ -264,7 +264,7 @@ export class NTQQWebApi {
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com'); const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
let HonorInfo = { let HonorInfo = {
group_id: groupCode, group_id: Number(groupCode),
current_talkative: {}, current_talkative: {},
talkative_list: [], talkative_list: [],
performer_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": { "9.9.19-35341": {
"appid": 537291347, "appid": 537291347,
"qua": "V1_WIN_NQ_9.9.19_35341_GW_B" "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" "recv": "3BEA210"
}, },
"9.9.19-35341-x64": { "9.9.19-35341-x64": {
"send": "3BE5A10", "send": "3BF1D50",
"recv": "3BEA210" "recv": "3BF6550"
},
"9.9.19-35469-x64": {
"send": "3BF1D50",
"recv": "3BF6550"
}, },
"3.2.17-35341-x64": { "3.2.17-35341-x64": {
"send": "AE2F700", "send": "AE2F700",
@@ -390,5 +394,13 @@
"3.2.17-35341-arm64": { "3.2.17-35341-arm64": {
"send": "778D840", "send": "778D840",
"recv": "7791170" "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 { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler'; import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet'; 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 './wrapper';
export * from './types'; export * from './types';
export * from './services'; export * from './services';
@@ -97,9 +101,63 @@ export class NapCatCore {
constructor(context: InstanceContext, selfInfo: SelfInfo) { constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo; this.selfInfo = selfInfo;
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,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 = { 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),
@@ -251,13 +309,13 @@ export async function genSessionConfig(
} }
export interface InstanceContext { export interface InstanceContext {
readonly workingEnv: NapCatCoreWorkingEnv; session: NodeIQQNTWrapperSession;
readonly wrapper: WrapperNodeApi; workingEnv: NapCatCoreWorkingEnv;
readonly session: NodeIQQNTWrapperSession; wrapper: WrapperNodeApi;
readonly logger: LogWrapper; logger: LogWrapper;
readonly loginService: NodeIKernelLoginService; loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper; basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper; pathWrapper: NapCatPathWrapper;
} }
export interface StableNTApiWrapper { export interface StableNTApiWrapper {

View File

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

View File

@@ -1,71 +1,71 @@
import { User, UserDetailInfoListenerArg } from '@/core/types'; import { SelfStatusInfo, User, UserDetailInfoListenerArg } from '@/core/types';
export class NodeIKernelProfileListener { 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, GroupNotifyMsgType,
NTGroupRequestOperateTypes, NTGroupRequestOperateTypes,
KickMemberV2Req, KickMemberV2Req,
GroupDetailInfoV2Param,
GroupExtInfo,
GroupExtFilter,
} from '@/core/types'; } from '@/core/types';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService { export interface NodeIKernelGroupService {
modifyGroupExtInfoV2(groupExtInfo: GroupExtInfo, groupExtFilter: GroupExtFilter): Promise<GeneralCallResult &
{
result: {
groupCode: string,
result: number
}
}>;
// ---> // --->
// 待启用 For Next Version 3.2.0 // 待启用 For Next Version 3.2.0
// isTroopMember ? 0 : 111 // isTroopMember ? 0 : 111
@@ -169,6 +181,9 @@ export interface NodeIKernelGroupService {
modifyGroupDetailInfo(groupCode: string, arg: unknown): void; modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
// 第二个参数在大多数情况为0 设置群成员权限 例如上传群文件权限和群成员付费/加入邀请加入时为8
modifyGroupDetailInfoV2(param: GroupDetailInfoV2Param, arg: number): Promise<GeneralCallResult>;
setGroupMsgMask(groupCode: string, arg: unknown): void; setGroupMsgMask(groupCode: string, arg: unknown): void;
changeGroupShieldSettingTemp(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 './NodeIKernelTipOffService';
export * from './NodeIKernelSearchService'; export * from './NodeIKernelSearchService';
export * from './NodeIKernelCollectionService'; 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 { import type {
NodeIKernelAvatarService, NodeIKernelAvatarService,
@@ -36,8 +46,19 @@ import type {
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService, NodeIKernelTipOffService,
} from '.'; } 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 = { export type ServiceNamingMapping = {
NodeIKernelAlbumService: NodeIKernelAlbumService;
NodeIKernelAvatarService: NodeIKernelAvatarService; NodeIKernelAvatarService: NodeIKernelAvatarService;
NodeIKernelBuddyService: NodeIKernelBuddyService; NodeIKernelBuddyService: NodeIKernelBuddyService;
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService; NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
@@ -53,6 +74,15 @@ export type ServiceNamingMapping = {
NodeIKernelRichMediaService: NodeIKernelRichMediaService; NodeIKernelRichMediaService: NodeIKernelRichMediaService;
NodeIKernelDbToolsService: NodeIKernelDbToolsService; NodeIKernelDbToolsService: NodeIKernelDbToolsService;
NodeIKernelTipOffService: NodeIKernelTipOffService; NodeIKernelTipOffService: NodeIKernelTipOffService;
NodeIKernelSearchService: NodeIKernelSearchService, NodeIKernelSearchService: NodeIKernelSearchService;
NodeIKernelCollectionService: NodeIKernelCollectionService; 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; operatorUid: string;
operatorNick: string; operatorNick: string;
operatorRemark: string; operatorRemark: string;
isSelfOperate: boolean; // 是否是自己撤回的
operatorMemRemark?: string; operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语 wording: string; // 自定义的撤回提示语
} }

View File

@@ -1,4 +1,97 @@
import { QQLevel, NTSex } from './user'; 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 { export interface KickMemberInfo {
optFlag: number; optFlag: number;
@@ -7,6 +100,185 @@ export interface KickMemberInfo {
optBytesMsg: string; 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 { export enum GroupInfoSource {
KUNSPECIFIED, 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 selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener(); const loginListener = new NodeIKernelLoginListener();
loginListener.onQRCodeLoginSucceed = async (loginResult) => { 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({ const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]), 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>; type Payload = Static<typeof SchemaData>;
@@ -16,6 +19,7 @@ export class CreateGroupFileFolder extends OneBotAction<Payload, ResponseType>
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder; override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
override payloadSchema = SchemaData; override payloadSchema = SchemaData;
async _handle(payload: Payload) { 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'; import { Notify } from '@/onebot/types';
interface RetData { interface RetData {
invited_requests: Notify[];
InvitedRequest: Notify[]; InvitedRequest: Notify[];
join_requests: Notify[]; join_requests: Notify[];
} }
@@ -13,7 +14,7 @@ export class GetGroupIgnoredNotifies extends OneBotAction<void, RetData> {
async _handle(): Promise<RetData> { async _handle(): Promise<RetData> {
const SingleScreenNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(false, 50); 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 notifyPromises = SingleScreenNotifies.map(async (SSNotify) => {
const invitorUin = SSNotify.user1?.uid ? +await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1.uid) : 0; 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); await Promise.all(notifyPromises);
retData.invited_requests = retData.InvitedRequest;
return retData; return retData;
} }
} }

View File

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

View File

@@ -116,10 +116,22 @@ import { CleanCache } from './system/CleanCache';
import SetFriendRemark from './user/SetFriendRemark'; import SetFriendRemark from './user/SetFriendRemark';
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest'; import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest'; 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) { export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [ 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 SetDoubtFriendsAddRequest(obContext, core),
new GetDoubtFriendsAddRequest(obContext, core), new GetDoubtFriendsAddRequest(obContext, core),
new SetFriendRemark(obContext, core), new SetFriendRemark(obContext, core),
@@ -247,6 +259,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetPrivateFileUrl(obContext, core), new GetPrivateFileUrl(obContext, core),
new GetUnidirectionalFriendList(obContext, core), new GetUnidirectionalFriendList(obContext, core),
new CleanCache(obContext, core), new CleanCache(obContext, core),
new GetGroupAddRequest(obContext, core),
new GetCollectionList(obContext, core),
]; ];
type HandlerUnion = typeof actionHandlers[number]; type HandlerUnion = typeof actionHandlers[number];

View File

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

View File

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

View File

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

View File

@@ -1209,7 +1209,6 @@ export class OneBotMsgApi {
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) { 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 groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
const isAdminOrOwner = groupRole === 3 || groupRole === 4; const isAdminOrOwner = groupRole === 3 || groupRole === 4;
if (isAdminOrOwner && !operatorUid) { if (isAdminOrOwner && !operatorUid) {
let dataNotify: GroupNotify | undefined; let dataNotify: GroupNotify | undefined;
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated', await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
@@ -1239,7 +1238,7 @@ export class OneBotMsgApi {
const operatorUid = await this.waitGroupNotify( const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(), groupChange.groupUin.toString(),
groupChange.memberUid, groupChange.memberUid,
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : '' groupChange.operatorInfo ? new TextDecoder('utf-8').decode(groupChange.operatorInfo) : undefined
); );
return new OB11GroupIncreaseEvent( return new OB11GroupIncreaseEvent(
this.core, this.core,
@@ -1251,13 +1250,42 @@ export class OneBotMsgApi {
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) { } else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(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( const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(), groupChange.groupUin.toString(),
groupChange.memberUid, groupChange.memberUid,
groupChange.decreaseType === 3 && groupChange.operatorInfo ? operator_uid_parse
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString()
); );
if (groupChange.memberUid === this.core.selfInfo.uid) { if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => { setTimeout(() => {

View File

@@ -270,7 +270,6 @@ export class NapCatOneBot11Adapter {
); );
} }
}; };
msgListener.onAddSendMsg = async (msg) => { msgListener.onAddSendMsg = async (msg) => {
try { try {
if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) { if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) {
@@ -282,7 +281,8 @@ export class NapCatOneBot11Adapter {
}, 1, 10 * 60 * 1000); }, 1, 10 * 60 * 1000);
// 10分钟 超时 // 10分钟 超时
const updatemsg = updatemsgs.find((e) => e.msgId === msg.msgId); 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( updatemsg.id = MessageUnique.createUniqueMsgId(
{ {
chatType: updatemsg.chatType, chatType: updatemsg.chatType,
@@ -304,8 +304,18 @@ export class NapCatOneBot11Adapter {
peerUid: uid, peerUid: uid,
guildId: '' 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); 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) { if (msg && element) {
const recallEvent = await this.emitRecallMsg(msg, element); const recallEvent = await this.emitRecallMsg(msg, element);
try { try {
@@ -316,6 +326,7 @@ export class NapCatOneBot11Adapter {
this.context.logger.logError('处理消息撤回失败', e); this.context.logger.logError('处理消息撤回失败', e);
} }
} }
}; };
msgListener.onKickedOffLine = async (kick) => { msgListener.onKickedOffLine = async (kick) => {
const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc); 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) => { this.app.use(async (req, res) => {
await this.handleRequest(req, res); await this.handleRequest(req, res);
}); });
this.server.listen(this.config.port, () => { this.server.listen(this.config.port, this.config.host, () => {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.config.port}`); 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) { if (isMacOS) {
return sendError(res, 'MacOS不支持终端'); return sendError(res, 'MacOS不支持终端');
} }
if ((await WebUiConfig.GetWebUIConfig()).token === 'napcat') {
return sendError(res, '默认密码禁止创建终端');
}
try { try {
const { cols, rows } = req.body; const { cols, rows } = req.body;
const { id } = terminalManager.createTerminal(cols, rows); const { id } = terminalManager.createTerminal(cols, rows);

View File

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

View File

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

View File

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