mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
20 Commits
v3.4.5
...
479b971b0c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
479b971b0c | ||
![]() |
347ba5f354 | ||
![]() |
81dbb9d980 | ||
![]() |
c4e1a3ab04 | ||
![]() |
90ec774a21 | ||
![]() |
db7a27e624 | ||
![]() |
f7d965eda2 | ||
![]() |
74ca2e2e16 | ||
![]() |
8ab550f2f5 | ||
![]() |
018aca4db2 | ||
![]() |
d4327166c1 | ||
![]() |
fa25d2e779 | ||
![]() |
3ce1c3f0ec | ||
![]() |
96dff5141e | ||
![]() |
78d85d9965 | ||
![]() |
37ec455b02 | ||
![]() |
6ab82739a6 | ||
![]() |
a36917e7c0 | ||
![]() |
21f3428b36 | ||
![]() |
f8a487db25 |
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "3.4.4",
|
||||
"version": "3.4.7",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "3.4.4",
|
||||
"version": "3.4.7",
|
||||
"scripts": {
|
||||
"build:framework": "vite build --mode framework",
|
||||
"build:shell": "vite build --mode shell",
|
||||
|
@@ -84,7 +84,7 @@ export class QQBasicInfoWrapper {
|
||||
}
|
||||
// 通过Major拉取 性能差
|
||||
try {
|
||||
let majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||
const majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||
if (majorAppid) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
|
||||
return { appid: majorAppid, qua: this.getQUAFallback() };
|
||||
@@ -98,8 +98,8 @@ export class QQBasicInfoWrapper {
|
||||
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
||||
}
|
||||
getAppidV2ByMajor(QQVersion: string) {
|
||||
let majorPath = getMajorPath(QQVersion);
|
||||
let appid = parseAppidFromMajor(majorPath);
|
||||
const majorPath = getMajorPath(QQVersion);
|
||||
const appid = parseAppidFromMajor(majorPath);
|
||||
return appid;
|
||||
}
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '3.4.4';
|
||||
export const napCatVersion = '3.4.7';
|
||||
|
@@ -54,7 +54,7 @@ export class NTQQGroupApi {
|
||||
}, pskey);
|
||||
}
|
||||
async getGroupShutUpMemberList(groupCode: string) {
|
||||
let data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', 1, 1000, (group_id) => group_id === groupCode);
|
||||
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', 1, 1000, (group_id) => group_id === groupCode);
|
||||
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||
return (await data)[1];
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as os from 'os';
|
||||
import { ChatType, InstanceContext, NapCatCore } from '..';
|
||||
import offset from '@/core/external/offset.json';
|
||||
import { PacketClient, RecvPacketData } from '@/core/packet/client';
|
||||
import { PacketSession } from "@/core/packet/session";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||
import { NapProtoMsg } from '@/core/packet/proto/NapProto';
|
||||
import { NapProtoEncodeStructType, NapProtoDecodeStructType, NapProtoMsg } from '@/core/packet/proto/NapProto';
|
||||
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
|
||||
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
|
||||
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
|
||||
@@ -20,6 +20,11 @@ import {
|
||||
} from "@/core/packet/message/element";
|
||||
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
|
||||
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||
import { AIVoiceChatType, AIVoiceItemList } from "@/core/packet/entities/aiChat";
|
||||
import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929";
|
||||
import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||
import { RecvPacketData } from "@/core/packet/client/client";
|
||||
|
||||
|
||||
interface OffsetType {
|
||||
@@ -35,7 +40,6 @@ export class NTQQPacketApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
logger: LogWrapper;
|
||||
serverUrl: string | undefined;
|
||||
qqVersion: string | undefined;
|
||||
packetSession: PacketSession | undefined;
|
||||
|
||||
@@ -45,9 +49,8 @@ export class NTQQPacketApi {
|
||||
this.logger = core.context.logger;
|
||||
this.packetSession = undefined;
|
||||
const config = this.core.configLoader.configData;
|
||||
if (config && config.packetServer && config.packetServer.length > 0) {
|
||||
const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086';
|
||||
this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion())
|
||||
if (config) {
|
||||
this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
||||
.then()
|
||||
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
|
||||
} else {
|
||||
@@ -59,17 +62,14 @@ export class NTQQPacketApi {
|
||||
return this.packetSession?.client.available ?? false;
|
||||
}
|
||||
|
||||
async InitSendPacket(serverUrl: string, qqversion: string) {
|
||||
this.serverUrl = serverUrl;
|
||||
async InitSendPacket(qqversion: string) {
|
||||
this.qqVersion = qqversion;
|
||||
const offsetTable: OffsetType = offset;
|
||||
const table = offsetTable[qqversion + '-' + os.arch()];
|
||||
const table = typedOffset[qqversion + '-' + os.arch()];
|
||||
if (!table) {
|
||||
this.logger.logError('PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
|
||||
return false;
|
||||
}
|
||||
const url = 'ws://' + this.serverUrl + '/ws';
|
||||
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core));
|
||||
this.packetSession = new PacketSession(this.core);
|
||||
const cb = () => {
|
||||
if (this.packetSession && this.packetSession.client) {
|
||||
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
|
||||
@@ -188,10 +188,54 @@ export class NTQQPacketApi {
|
||||
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
|
||||
}
|
||||
|
||||
async sendGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||
const data = this.packetSession?.packer.packGroupPttFileDownloadReq(groupUin, node);
|
||||
const ret = await this.sendOidbPacket(data!, true);
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||
const resp = new NapProtoMsg(NTV2RichMediaResp).decode(body);
|
||||
const info = resp.download.info;
|
||||
return `https://${info.domain}${info.urlPath}${resp.download.rKeyParam}`;
|
||||
}
|
||||
|
||||
async sendMiniAppShareInfoReq(param: MiniAppReqParams) {
|
||||
const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param);
|
||||
const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true);
|
||||
const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex'));
|
||||
return JSON.parse(body.content.jsonContent) as MiniAppRawData;
|
||||
}
|
||||
|
||||
async sendFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType) : Promise<AIVoiceItemList[] | null> {
|
||||
const data = this.packetSession?.packer.packFetchAiVoiceListReq(groupUin, chatType);
|
||||
const ret = await this.sendOidbPacket(data!, true);
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929D_0Resp).decode(body);
|
||||
if (!resp.content) return null;
|
||||
return resp.content.map((item) => {
|
||||
return {
|
||||
category: item.category,
|
||||
voices: item.voices
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async sendAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
|
||||
let reqTime = 0;
|
||||
const reqMaxTime = 30;
|
||||
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
|
||||
while (true) {
|
||||
if (reqTime >= reqMaxTime) {
|
||||
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
|
||||
}
|
||||
reqTime++;
|
||||
const data = this.packetSession?.packer.packAiVoiceChatReq(groupUin, voiceId, text, chatType, sessionId);
|
||||
const ret = await this.sendOidbPacket(data!, true);
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBase).decode(Buffer.from(ret.hex_data, 'hex'));
|
||||
if (body.errorCode) {
|
||||
throw new Error(`sendAiVoiceChatReq retCode: ${body.errorCode} error: ${body.errorMsg}`);
|
||||
}
|
||||
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929B_0Resp).decode(body.body);
|
||||
if (!resp.msgInfo) continue;
|
||||
return resp.msgInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
src/core/external/appid.json
vendored
14
src/core/external/appid.json
vendored
@@ -51,7 +51,7 @@
|
||||
"appid": 537249739,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
||||
},
|
||||
"9.9.16-28971":{
|
||||
"9.9.16-28971": {
|
||||
"appid": 537249775,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
||||
},
|
||||
@@ -62,5 +62,17 @@
|
||||
"6.9.58-28971": {
|
||||
"appid": 537249826,
|
||||
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
|
||||
},
|
||||
"9.9.16-29271": {
|
||||
"appid": 537249813,
|
||||
"qua": "V1_WIN_NQ_9.9.16_29271_GW_B"
|
||||
},
|
||||
"3.2.13-29271": {
|
||||
"appid": 537249913,
|
||||
"qua": "V1_LNX_NQ_3.2.13_29271_GW_B"
|
||||
},
|
||||
"6.9.59-29271": {
|
||||
"appid": 537249863,
|
||||
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
|
||||
}
|
||||
}
|
22
src/core/external/offset.json
vendored
22
src/core/external/offset.json
vendored
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"6.9.56-28418-arm64": {
|
||||
"send": "4471360",
|
||||
"recv": "4473BCC"
|
||||
},
|
||||
"3.2.12-28418-x64": {
|
||||
"recv": "A0723E0",
|
||||
"send": "A06EAE0"
|
||||
@@ -35,8 +39,20 @@
|
||||
"send": "6E91318",
|
||||
"recv": "6E94B50"
|
||||
},
|
||||
"6.9.56-28418-arm64": {
|
||||
"send": "4471360",
|
||||
"recv": "4473BCC"
|
||||
"6.9.58-28971-arm64": {
|
||||
"send": "449ACA0",
|
||||
"recv": "449D50C"
|
||||
},
|
||||
"9.9.16-29271-x64": {
|
||||
"send": "3833510",
|
||||
"recv": "3837944"
|
||||
},
|
||||
"3.2.13-29271-x64": {
|
||||
"send": "A11E680",
|
||||
"recv": "A121F80"
|
||||
},
|
||||
"3.2.13-29271-arm64": {
|
||||
"send": "6ECA098",
|
||||
"recv": "6ECD8D0"
|
||||
}
|
||||
}
|
@@ -1,183 +0,0 @@
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import WebSocket, { Data } from "ws";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
trace_id_md5?: string,
|
||||
data: RecvPacketData
|
||||
}
|
||||
|
||||
export interface RecvPacketData {
|
||||
seq: number
|
||||
cmd: string
|
||||
hex_data: string
|
||||
}
|
||||
|
||||
export class PacketClient {
|
||||
private websocket: WebSocket | undefined;
|
||||
private isConnected: boolean = false;
|
||||
private reconnectAttempts: number = 0;
|
||||
private readonly maxReconnectAttempts: number = 60;//现在暂时不可配置
|
||||
private readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
||||
private readonly clientUrl: string = '';
|
||||
readonly napCatCore: NapCatCore;
|
||||
private readonly logger: LogWrapper;
|
||||
|
||||
constructor(url: string, core: NapCatCore) {
|
||||
this.clientUrl = url;
|
||||
this.napCatCore = core;
|
||||
this.logger = core.context.logger;
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return this.isConnected && this.websocket !== undefined;
|
||||
}
|
||||
|
||||
private randText(len: number) {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < len; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
connect(cb: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
|
||||
this.websocket = new WebSocket(this.clientUrl);
|
||||
this.websocket.on('error', (err) => { }/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`);
|
||||
cb();
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
|
||||
reject(new Error(`${error.message}`));
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
// const message = JSON.parse(event.data.toString());
|
||||
// console.log("Received message:", message);
|
||||
this.handleMessage(event.data).then().catch();
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
this.isConnected = false;
|
||||
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
|
||||
this.attemptReconnect(cb);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private attemptReconnect(cb: any): void {
|
||||
try {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
this.connect(cb).catch((error) => {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 尝试重连失败:${error.message}`);
|
||||
});
|
||||
}, 5000 * this.reconnectAttempts);
|
||||
} else {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] ${this.clientUrl} 已达到最大重连次数!`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 重连时出错: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
||||
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
||||
}
|
||||
|
||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
||||
if (!this.isConnected || !this.websocket) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
const initMessage = {
|
||||
action: 'init',
|
||||
pid: pid,
|
||||
recv: recv,
|
||||
send: send
|
||||
};
|
||||
this.websocket.send(JSON.stringify(initMessage));
|
||||
}
|
||||
|
||||
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
|
||||
}): Promise<RecvPacketData> {
|
||||
return new Promise<RecvPacketData>((resolve, reject) => {
|
||||
if (!this.isConnected || !this.websocket) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
const commandMessage = {
|
||||
action: 'send',
|
||||
cmd: cmd,
|
||||
data: data,
|
||||
trace_id: trace_id
|
||||
};
|
||||
this.websocket.send(JSON.stringify(commandMessage));
|
||||
if (rsp) {
|
||||
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve(json);
|
||||
});
|
||||
}
|
||||
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
||||
sendcb(json);
|
||||
if (!rsp) {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve(json);
|
||||
}
|
||||
});
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMessage(message: Data): Promise<void> {
|
||||
try {
|
||||
const json: RecvPacket = JSON.parse(message.toString());
|
||||
const trace_id_md5 = json.trace_id_md5;
|
||||
const action = json?.type ?? 'init';
|
||||
const event = this.cb.get(trace_id_md5 + action);
|
||||
if (event) {
|
||||
await event(json.data);
|
||||
}
|
||||
//console.log("Received message:", json);
|
||||
} catch (error) {
|
||||
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
||||
// wtfk tx
|
||||
// 校验失败和异常 可能返回undefined
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.available) {
|
||||
this.logger.logError('NapCat.Packet 未初始化!');
|
||||
return undefined;
|
||||
}
|
||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
||||
const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
|
||||
this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
||||
// await sleep(10);
|
||||
await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
|
||||
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||
}
|
||||
}
|
109
src/core/packet/client/client.ts
Normal file
109
src/core/packet/client/client.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||
import { NapCatConfig } from "@/core/helper/config";
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
trace_id_md5?: string,
|
||||
data: RecvPacketData
|
||||
}
|
||||
|
||||
export interface RecvPacketData {
|
||||
seq: number
|
||||
cmd: string
|
||||
hex_data: string
|
||||
}
|
||||
|
||||
export abstract class PacketClient {
|
||||
readonly napCatCore: NapCatCore;
|
||||
protected readonly logger: LogWrapper;
|
||||
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
||||
protected isAvailable: boolean = false;
|
||||
protected config: NapCatConfig;
|
||||
|
||||
protected constructor(core: NapCatCore) {
|
||||
this.napCatCore = core;
|
||||
this.logger = core.context.logger;
|
||||
this.config = core.configLoader.configData;
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return this.isAvailable;
|
||||
}
|
||||
|
||||
private randText(len: number): string {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < len; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static create(core: NapCatCore): PacketClient {
|
||||
throw new Error("Must be implemented by subclasses");
|
||||
}
|
||||
|
||||
static compatibilityScore(logger: LogWrapper): number {
|
||||
throw new Error("Must be implemented by subclasses");
|
||||
}
|
||||
|
||||
abstract init(pid: number, recv: string, send: string): Promise<void>;
|
||||
|
||||
abstract connect(cb: () => void): Promise<void>;
|
||||
|
||||
abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void;
|
||||
|
||||
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
||||
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
||||
}
|
||||
|
||||
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
|
||||
}): Promise<RecvPacketData> {
|
||||
return new Promise<RecvPacketData>((resolve, reject) => {
|
||||
if (!this.isAvailable) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
this.sendCommandImpl(cmd, data, trace_id);
|
||||
if (rsp) {
|
||||
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve(json);
|
||||
});
|
||||
}
|
||||
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
||||
sendcb(json);
|
||||
if (!rsp) {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve(json);
|
||||
}
|
||||
});
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.available) {
|
||||
this.logger.logError('NapCat.Packet 未初始化!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
||||
const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
|
||||
|
||||
this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
||||
await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
|
||||
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||
}
|
||||
}
|
67
src/core/packet/client/nativeClient.ts
Normal file
67
src/core/packet/client/nativeClient.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { NapCatCore } from "@/core";
|
||||
import path, { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import fs from "fs";
|
||||
import { console } from "inspector";
|
||||
import { PacketClient } from "@/core/packet/client/client";
|
||||
import { constants, platform, type } from "node:os";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
|
||||
export class NativePacketClient extends PacketClient {
|
||||
static supportedPlatforms = ['win32.x64'];
|
||||
private MoeHooExport: any = { exports: {} };
|
||||
|
||||
protected constructor(core: NapCatCore) {
|
||||
super(core);
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return this.isAvailable;
|
||||
}
|
||||
|
||||
static compatibilityScore(logger: LogWrapper): number {
|
||||
const platform = process.platform + '.' + process.arch;
|
||||
if (!this.supportedPlatforms.includes(platform)) {
|
||||
logger.logError(`[NativePacketClient] Unsupported platform: ${platform}`);
|
||||
return 0;
|
||||
}
|
||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/moehoo.' + platform + '.node');
|
||||
if (!fs.existsSync(moehoo_path)) {
|
||||
logger.logError(`[NativePacketClient] Missing moehoo binary: ${moehoo_path}`);
|
||||
return 0;
|
||||
}
|
||||
return 10;
|
||||
}
|
||||
|
||||
static create(core: NapCatCore): NativePacketClient {
|
||||
return new NativePacketClient(core);
|
||||
}
|
||||
|
||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
||||
const platform = process.platform + '.' + process.arch;
|
||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/moehoo.' + platform + '.node');
|
||||
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
|
||||
this.MoeHooExport.exports.InitHook(pid, recv, send, (type: number, uin: string, seq: number, cmd: string, hex_data: string) => {
|
||||
const callback = this.cb.get(createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex') + (type === 0 ? 'send' : 'recv'));
|
||||
if (callback) {
|
||||
callback({ seq, cmd, hex_data });
|
||||
} else {
|
||||
this.logger.logError(`Callback not found for hex_data: ${hex_data}`);
|
||||
}
|
||||
// TODO: cannot use console.log here, fxxk tx
|
||||
// Error [ERR_INSPECTOR_NOT_AVAILABLE]: Inspector is not available
|
||||
// console.log('type:', type, 'uin:', uin, 'seq:', seq, 'cmd:', cmd, 'hex_data:', hex_data);
|
||||
});
|
||||
this.isAvailable = true;
|
||||
}
|
||||
|
||||
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
|
||||
this.MoeHooExport.exports.SendPacket(cmd, data, crypto.createHash('md5').update(trace_id).digest('hex'));
|
||||
}
|
||||
|
||||
connect(cb: () => void): Promise<void> {
|
||||
cb();
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
113
src/core/packet/client/wsClient.ts
Normal file
113
src/core/packet/client/wsClient.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Data, WebSocket } from "ws";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { PacketClient, RecvPacket } from "@/core/packet/client/client";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
|
||||
export class wsPacketClient extends PacketClient {
|
||||
private websocket: WebSocket | undefined;
|
||||
private reconnectAttempts: number = 0;
|
||||
private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置
|
||||
private readonly clientUrl: string = '';
|
||||
private clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`;
|
||||
|
||||
protected constructor(core: NapCatCore) {
|
||||
super(core);
|
||||
this.clientUrl = this.clientUrlWrap(this.config.packetServer ?? '127.0.0.1:8086');
|
||||
}
|
||||
|
||||
static compatibilityScore(logger: LogWrapper): number {
|
||||
return 10;
|
||||
}
|
||||
|
||||
static create(core: NapCatCore): wsPacketClient {
|
||||
return new wsPacketClient(core);
|
||||
}
|
||||
|
||||
connect(cb: () => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
|
||||
this.websocket = new WebSocket(this.clientUrl);
|
||||
this.websocket.on('error', (err) => { }/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.isAvailable = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`);
|
||||
cb();
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
|
||||
reject(new Error(`${error.message}`));
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
// const message = JSON.parse(event.data.toString());
|
||||
// console.log("Received message:", message);
|
||||
this.handleMessage(event.data).then().catch();
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
this.isAvailable = false;
|
||||
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
|
||||
this.attemptReconnect(cb);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private attemptReconnect(cb: any): void {
|
||||
try {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
this.connect(cb).catch((error) => {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 尝试重连失败:${error.message}`);
|
||||
});
|
||||
}, 5000 * this.reconnectAttempts);
|
||||
} else {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] ${this.clientUrl} 已达到最大重连次数!`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 重连时出错: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
||||
if (!this.isAvailable || !this.websocket) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
const initMessage = {
|
||||
action: 'init',
|
||||
pid: pid,
|
||||
recv: recv,
|
||||
send: send
|
||||
};
|
||||
this.websocket.send(JSON.stringify(initMessage));
|
||||
}
|
||||
|
||||
sendCommandImpl(cmd: string, data: string, trace_id: string) : void {
|
||||
const commandMessage = {
|
||||
action: 'send',
|
||||
cmd: cmd,
|
||||
data: data,
|
||||
trace_id: trace_id
|
||||
};
|
||||
this.websocket!.send(JSON.stringify(commandMessage));
|
||||
}
|
||||
|
||||
private async handleMessage(message: Data): Promise<void> {
|
||||
try {
|
||||
const json: RecvPacket = JSON.parse(message.toString());
|
||||
const trace_id_md5 = json.trace_id_md5;
|
||||
const action = json?.type ?? 'init';
|
||||
const event = this.cb.get(trace_id_md5 + action);
|
||||
if (event) {
|
||||
await event(json.data);
|
||||
}
|
||||
//console.log("Received message:", json);
|
||||
} catch (error) {
|
||||
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
16
src/core/packet/entities/aiChat.ts
Normal file
16
src/core/packet/entities/aiChat.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export enum AIVoiceChatType {
|
||||
Unknown = 0,
|
||||
Sound = 1,
|
||||
Sing = 2
|
||||
}
|
||||
|
||||
export interface AIVoiceItem {
|
||||
voiceId: string;
|
||||
voiceDisplayName: string;
|
||||
voiceExampleUrl: string;
|
||||
}
|
||||
|
||||
export interface AIVoiceItemList {
|
||||
category: string;
|
||||
voices: AIVoiceItem[];
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import * as fs from "node:fs";
|
||||
import { ChatType, Peer } from "@/core";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketClient } from "@/core/packet/client";
|
||||
import { PacketPacker } from "@/core/packet/packer";
|
||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
|
||||
@@ -19,6 +18,7 @@ import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils
|
||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
import { PacketClient } from "@/core/packet/client/client";
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
|
@@ -241,7 +241,7 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return `[${this.emojiName}]`;
|
||||
return `${this.emojiName}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import * as zlib from "node:zlib";
|
||||
import * as crypto from "node:crypto";
|
||||
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
|
||||
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
|
||||
import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2";
|
||||
import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2";
|
||||
import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
|
||||
import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
import { IndexNode, NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
||||
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
||||
import { PacketMsgBuilder } from "@/core/packet/message/builder";
|
||||
@@ -22,12 +22,14 @@ import { PacketMsg } from "@/core/packet/message/message";
|
||||
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
|
||||
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
||||
import { PacketClient } from "@/core/packet/client";
|
||||
import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700";
|
||||
import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
||||
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
||||
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import { OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0 } from "@/core/packet/proto/oidb/Oidb.0x929";
|
||||
import { PacketClient } from "@/core/packet/client/client";
|
||||
|
||||
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
||||
|
||||
@@ -696,6 +698,37 @@ export class PacketPacker {
|
||||
);
|
||||
}
|
||||
|
||||
packGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||
return this.packOidbPacket(0x126E, 200, new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 4,
|
||||
command: 200
|
||||
},
|
||||
scene: {
|
||||
requestType: 1,
|
||||
businessType: 3,
|
||||
sceneType: 2,
|
||||
group: {
|
||||
groupUin: groupUin
|
||||
}
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
download: {
|
||||
node: node,
|
||||
download: {
|
||||
video: {
|
||||
busiType: 0,
|
||||
sceneType: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}), true, false);
|
||||
}
|
||||
|
||||
packGroupSignReq(uin: string, groupCode: string): OidbPacket {
|
||||
return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode(
|
||||
{
|
||||
@@ -744,4 +777,27 @@ export class PacketPacker {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
packFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType): OidbPacket {
|
||||
return this.packOidbPacket(0x929D, 0,
|
||||
new NapProtoMsg(OidbSvcTrpcTcp0X929D_0).encode({
|
||||
groupUin: groupUin,
|
||||
chatType: chatType
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
packAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType, sessionId: number): OidbPacket {
|
||||
return this.packOidbPacket(0x929B, 0,
|
||||
new NapProtoMsg(OidbSvcTrpcTcp0X929B_0).encode({
|
||||
groupUin: groupUin,
|
||||
voiceId: voiceId,
|
||||
text: text,
|
||||
chatType: chatType,
|
||||
session: {
|
||||
sessionId: sessionId
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -91,15 +91,12 @@ export type NapProtoEncodeStructType<T> = NapProtoStructType<T, true>;
|
||||
|
||||
export type NapProtoDecodeStructType<T> = NapProtoStructType<T, false>;
|
||||
|
||||
const NapProtoMsgCache = new Map<ProtoMessageType, MessageType<NapProtoStructType<ProtoMessageType, boolean>>>();
|
||||
|
||||
export class NapProtoMsg<T extends ProtoMessageType> {
|
||||
private readonly _msg: T;
|
||||
class NapProtoRealMsg<T extends ProtoMessageType> {
|
||||
private readonly _field: PartialFieldInfo[];
|
||||
private readonly _proto_msg: MessageType<NapProtoStructType<T, boolean>>;
|
||||
private static cache = new WeakMap<ProtoMessageType, NapProtoRealMsg<any>>();
|
||||
|
||||
constructor(fields: T) {
|
||||
this._msg = fields;
|
||||
private constructor(fields: T) {
|
||||
this._field = Object.keys(fields).map(key => {
|
||||
const field = fields[key];
|
||||
if (field.kind === 'scalar') {
|
||||
@@ -122,13 +119,22 @@ export class NapProtoMsg<T extends ProtoMessageType> {
|
||||
name: key,
|
||||
kind: 'message',
|
||||
repeat: field.repeat ? RepeatType.PACKED : RepeatType.NO,
|
||||
T: () => new NapProtoMsg(field.type())._proto_msg,
|
||||
T: () => NapProtoRealMsg.getInstance(field.type())._proto_msg,
|
||||
};
|
||||
}
|
||||
}) as PartialFieldInfo[];
|
||||
this._proto_msg = new MessageType<NapProtoStructType<T, boolean>>('nya', this._field);
|
||||
}
|
||||
|
||||
static getInstance<T extends ProtoMessageType>(fields: T): NapProtoRealMsg<T> {
|
||||
let instance = this.cache.get(fields);
|
||||
if (!instance) {
|
||||
instance = new NapProtoRealMsg(fields);
|
||||
this.cache.set(fields, instance);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
encode(data: NapProtoEncodeStructType<T>): Uint8Array {
|
||||
return this._proto_msg.toBinary(this._proto_msg.create(data as PartialMessage<NapProtoEncodeStructType<T>>));
|
||||
}
|
||||
@@ -137,3 +143,19 @@ export class NapProtoMsg<T extends ProtoMessageType> {
|
||||
return this._proto_msg.fromBinary(data) as NapProtoDecodeStructType<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export class NapProtoMsg<T extends ProtoMessageType> {
|
||||
private realMsg: NapProtoRealMsg<T>;
|
||||
|
||||
constructor(fields: T) {
|
||||
this.realMsg = NapProtoRealMsg.getInstance(fields);
|
||||
}
|
||||
|
||||
encode(data: NapProtoEncodeStructType<T>): Uint8Array {
|
||||
return this.realMsg.encode(data);
|
||||
}
|
||||
|
||||
decode(data: Uint8Array): NapProtoDecodeStructType<T> {
|
||||
return this.realMsg.decode(data);
|
||||
}
|
||||
}
|
||||
|
42
src/core/packet/proto/oidb/Oidb.0x929.ts
Normal file
42
src/core/packet/proto/oidb/Oidb.0x929.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
|
||||
export const OidbSvcTrpcTcp0X929D_0 = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
chatType: ProtoField(2, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0X929D_0Resp = {
|
||||
content: ProtoField(1, () => OidbSvcTrpcTcp0X929D_0RespContent, false, true),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0X929D_0RespContent = {
|
||||
category: ProtoField(1, ScalarType.STRING),
|
||||
voices: ProtoField(2, () => OidbSvcTrpcTcp0X929D_0RespContentVoice, false, true),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0X929D_0RespContentVoice = {
|
||||
voiceId: ProtoField(1, ScalarType.STRING),
|
||||
voiceDisplayName: ProtoField(2, ScalarType.STRING),
|
||||
voiceExampleUrl: ProtoField(3, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0X929B_0 = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
voiceId: ProtoField(2, ScalarType.STRING),
|
||||
text: ProtoField(3, ScalarType.STRING),
|
||||
chatType: ProtoField(4, ScalarType.UINT32),
|
||||
session: ProtoField(5, () => OidbSvcTrpcTcp0X929B_0_Session),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0X929B_0_Session = {
|
||||
sessionId: ProtoField(1, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0X929B_0Resp = {
|
||||
statusCode: ProtoField(1, ScalarType.UINT32),
|
||||
field2: ProtoField(2, ScalarType.UINT32, true),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
msgInfo: ProtoField(4, () => MsgInfo, true),
|
||||
};
|
@@ -4,10 +4,11 @@ import { ProtoField } from "../NapProto";
|
||||
export const OidbSvcTrpcTcpBase = {
|
||||
command: ProtoField(1, ScalarType.UINT32),
|
||||
subCommand: ProtoField(2, ScalarType.UINT32),
|
||||
errorCode: ProtoField(3, ScalarType.UINT32),
|
||||
body: ProtoField(4, ScalarType.BYTES),
|
||||
errorMsg: ProtoField(5, ScalarType.STRING, true),
|
||||
isReserved: ProtoField(12, ScalarType.UINT32)
|
||||
};
|
||||
export const OidbSvcTrpcTcpBaseRsp = {
|
||||
body: ProtoField(4, ScalarType.BYTES)
|
||||
};
|
||||
};
|
||||
|
@@ -1,7 +1,19 @@
|
||||
import { PacketClient } from "@/core/packet/client";
|
||||
import { PacketHighwaySession } from "@/core/packet/highway/session";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketPacker } from "@/core/packet/packer";
|
||||
import { PacketClient } from "@/core/packet/client/client";
|
||||
import { NativePacketClient } from "@/core/packet/client/nativeClient";
|
||||
import { wsPacketClient } from "@/core/packet/client/wsClient";
|
||||
import { NapCatCore } from "@/core";
|
||||
|
||||
type clientPriority = {
|
||||
[key: number]: typeof PacketClient;
|
||||
}
|
||||
|
||||
const clientPriority: clientPriority = {
|
||||
10: NativePacketClient,
|
||||
1: wsPacketClient,
|
||||
};
|
||||
|
||||
export class PacketSession {
|
||||
readonly logger: LogWrapper;
|
||||
@@ -9,10 +21,29 @@ export class PacketSession {
|
||||
readonly packer: PacketPacker;
|
||||
readonly highwaySession: PacketHighwaySession;
|
||||
|
||||
constructor(logger: LogWrapper, client: PacketClient) {
|
||||
this.logger = logger;
|
||||
this.client = client;
|
||||
constructor(core: NapCatCore) {
|
||||
this.logger = core.context.logger;
|
||||
this.client = this.judgeClient(core);
|
||||
this.packer = new PacketPacker(this.logger, this.client);
|
||||
this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer);
|
||||
}
|
||||
|
||||
private judgeClient(core: NapCatCore): PacketClient {
|
||||
let selectedClient: typeof PacketClient | null = null;
|
||||
let maxScore = -1;
|
||||
for (const key in clientPriority) {
|
||||
const priority = parseInt(key);
|
||||
const ClientClass = clientPriority[priority];
|
||||
const score = priority * ClientClass.compatibilityScore(core.context.logger);
|
||||
if (score > maxScore) {
|
||||
maxScore = score;
|
||||
selectedClient = ClientClass;
|
||||
}
|
||||
}
|
||||
if (!selectedClient) {
|
||||
throw new Error("No compatible PacketClient found");
|
||||
}
|
||||
this.logger.log(`[Packet] 自动选择了: ${selectedClient.name}`);
|
||||
return selectedClient.create(core);
|
||||
}
|
||||
}
|
||||
|
41
src/onebot/action/extends/GetAiCharacters.ts
Normal file
41
src/onebot/action/extends/GetAiCharacters.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ActionName } from '../types';
|
||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
||||
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
|
||||
const SchemaData = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
group_id: { type: ['number', 'string'] },
|
||||
chat_type: { type: ['number', 'string'] },
|
||||
},
|
||||
required: ['group_id'],
|
||||
} as const satisfies JSONSchema;
|
||||
|
||||
type Payload = FromSchema<typeof SchemaData>;
|
||||
|
||||
interface GetAiCharactersResponse {
|
||||
type: string;
|
||||
characters: {
|
||||
character_id: string;
|
||||
character_name: string;
|
||||
preview_url: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharactersResponse[]> {
|
||||
actionName = ActionName.GetAiCharacters;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const rawList = await this.core.apis.PacketApi.sendFetchAiVoiceListReq(+payload.group_id, +(payload.chat_type ?? 1) as AIVoiceChatType);
|
||||
return rawList?.map((item) => ({
|
||||
type: item.category,
|
||||
characters: item.voices.map((voice) => ({
|
||||
character_id: voice.voiceId,
|
||||
character_name: voice.voiceDisplayName,
|
||||
preview_url: voice.voiceExampleUrl,
|
||||
})),
|
||||
})) ?? [];
|
||||
}
|
||||
}
|
@@ -73,16 +73,14 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
|
||||
|
||||
const singleMsg = data.msgList[0];
|
||||
const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理
|
||||
if (!resMsg) {
|
||||
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
|
||||
throw new Error('找不到相关的聊天记录');
|
||||
}
|
||||
//if (this.obContext.configLoader.configData.messagePostFormat === 'array') {
|
||||
//提取
|
||||
const realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message;
|
||||
//里面都是offline消息 id都是0 没得说话
|
||||
return { message: realmsg };
|
||||
return {
|
||||
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
|
||||
};
|
||||
//}
|
||||
|
||||
// return { message: resMsg };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
src/onebot/action/group/GetAiRecord.ts
Normal file
28
src/onebot/action/group/GetAiRecord.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ActionName } from '../types';
|
||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
||||
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto";
|
||||
import { IndexNode } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
|
||||
const SchemaData = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
character: { type: ['string'] },
|
||||
group_id: { type: ['number', 'string'] },
|
||||
text: { type: 'string' },
|
||||
},
|
||||
required: ['character', 'group_id', 'text'],
|
||||
} as const satisfies JSONSchema;
|
||||
|
||||
type Payload = FromSchema<typeof SchemaData>;
|
||||
|
||||
export class GetAiRecord extends GetPacketStatusDepends<Payload, string> {
|
||||
actionName = ActionName.GetAiRecord;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const rawRsp = await this.core.apis.PacketApi.sendAiVoiceChatReq(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
|
||||
return await this.core.apis.PacketApi.sendGroupPttFileDownloadReq(+payload.group_id, rawRsp.msgInfoBody[0].index);
|
||||
}
|
||||
}
|
38
src/onebot/action/group/SendGroupAiRecord.ts
Normal file
38
src/onebot/action/group/SendGroupAiRecord.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ActionName } from '../types';
|
||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
||||
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import { uri2local } from "@/common/file";
|
||||
import { ChatType, Peer } from "@/core";
|
||||
|
||||
const SchemaData = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
character: { type: ['string'] },
|
||||
group_id: { type: ['number', 'string'] },
|
||||
text: { type: 'string' },
|
||||
},
|
||||
required: ['character', 'group_id', 'text'],
|
||||
} as const satisfies JSONSchema;
|
||||
|
||||
type Payload = FromSchema<typeof SchemaData>;
|
||||
|
||||
export class SendGroupAiRecord extends GetPacketStatusDepends<Payload, {
|
||||
message_id: number
|
||||
}> {
|
||||
actionName = ActionName.SendGroupAiRecord;
|
||||
payloadSchema = SchemaData;
|
||||
|
||||
async _handle(payload: Payload) {
|
||||
const rawRsp = await this.core.apis.PacketApi.sendAiVoiceChatReq(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
|
||||
const url = await this.core.apis.PacketApi.sendGroupPttFileDownloadReq(+payload.group_id, rawRsp.msgInfoBody[0].index);
|
||||
const { path, fileName, errMsg, success } = (await uri2local(this.core.NapCatTempPath, url));
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
const peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() } as Peer;
|
||||
const element = await this.core.apis.FileApi.createValidSendPttElement(path);
|
||||
const sendRes = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [element], [path]);
|
||||
return { message_id: sendRes.id ?? -1 };
|
||||
}
|
||||
}
|
@@ -99,6 +99,9 @@ import { GoCQHTTPGetModelShow } from './go-cqhttp/GoCQHTTPGetModelShow';
|
||||
import { GoCQHTTPSetModelShow } from './go-cqhttp/GoCQHTTPSetModelShow';
|
||||
import { GoCQHTTPDeleteFriend } from './go-cqhttp/GoCQHTTPDeleteFriend';
|
||||
import { GetMiniAppArk } from "@/onebot/action/extends/GetMiniAppArk";
|
||||
import { GetAiRecord } from "@/onebot/action/group/GetAiRecord";
|
||||
import { SendGroupAiRecord } from "@/onebot/action/group/SendGroupAiRecord";
|
||||
import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters";
|
||||
|
||||
|
||||
export type ActionMap = Map<string, BaseAction<any, any>>;
|
||||
@@ -212,6 +215,9 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
new GetGroupShutList(obContext, core),
|
||||
new GetGroupFileUrl(obContext, core),
|
||||
new GetMiniAppArk(obContext, core),
|
||||
new GetAiRecord(obContext, core),
|
||||
new SendGroupAiRecord(obContext, core),
|
||||
new GetAiCharacters(obContext, core),
|
||||
];
|
||||
const actionMap = new Map();
|
||||
for (const action of actionHandlers) {
|
||||
|
@@ -138,4 +138,7 @@ export enum ActionName {
|
||||
SetGroupSign = "set_group_sign",
|
||||
GetMiniAppArk = "get_mini_app_ark",
|
||||
// UploadForwardMsg = "upload_forward_msg",
|
||||
GetAiRecord = "get_ai_record",
|
||||
GetAiCharacters = "get_ai_characters",
|
||||
SendGroupAiRecord = "send_group_ai_record",
|
||||
}
|
||||
|
@@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
|
||||
SettingItem(
|
||||
'<span id="napcat-update-title">Napcat</span>',
|
||||
void 0,
|
||||
SettingButton("V3.4.4", "napcat-update-button", "secondary")
|
||||
SettingButton("V3.4.7", "napcat-update-button", "secondary")
|
||||
)
|
||||
]),
|
||||
SettingList([
|
||||
|
Reference in New Issue
Block a user