mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-12 07:50:25 +00:00
Compare commits
26 Commits
copilot/fi
...
v4.9.40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aedacb27f | ||
|
|
c9b45ec1a2 | ||
|
|
54cacc30e4 | ||
|
|
9b26fc99d3 | ||
|
|
204846b404 | ||
|
|
3d654791b9 | ||
|
|
63f42f1592 | ||
|
|
0ab0b939da | ||
|
|
522a123f9a | ||
|
|
ec9f8d6e12 | ||
|
|
3c750c75a9 | ||
|
|
5b2b1f499b | ||
|
|
531ffcd55d | ||
|
|
068e4d8bb5 | ||
|
|
5dc33e78ad | ||
|
|
d76a2170a0 | ||
|
|
ec2af3120c | ||
|
|
8de49a3109 | ||
|
|
dbb5a0022e | ||
|
|
cb8c8d6b57 | ||
|
|
93c140ed4e | ||
|
|
457b072f0e | ||
|
|
1869493473 | ||
|
|
f33c66ce15 | ||
|
|
e8d6f86458 | ||
|
|
a000ffdf0d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ devconfig/*
|
||||
checkVersion.sh
|
||||
bun.lockb
|
||||
tests/run/
|
||||
guild1.db-wal
|
||||
guild1.db-shm
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.9.27",
|
||||
"version": "4.9.38",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
30
napiloader/napiLoader-debug.bat
Normal file
30
napiloader/napiLoader-debug.bat
Normal file
@@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
set NAPCAT_INJECT_PATH=%cd%\napiloader.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\napimain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\nativeLoader.cjs
|
||||
set NAPCAT_DEBUG_CONSOLE=1
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_MAIN_PATH%"
|
||||
|
||||
pause
|
||||
27
napiloader/napiLoader.bat
Normal file
27
napiloader/napiLoader.bat
Normal file
@@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
set NAPCAT_INJECT_PATH=%cd%\napiloader.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\napimain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\nativeLoader.cjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
|
||||
start "" "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" "%NAPCAT_MAIN_PATH%"
|
||||
BIN
napiloader/napiloader.dll
Normal file
BIN
napiloader/napiloader.dll
Normal file
Binary file not shown.
BIN
napiloader/napimain.exe
Normal file
BIN
napiloader/napimain.exe
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.9.27",
|
||||
"version": "4.9.38",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && npm run dev:universal || exit 1",
|
||||
"build:framework": "npm run build:webui && npm run dev:framework || exit 1",
|
||||
|
||||
@@ -66,13 +66,15 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
|
||||
*/
|
||||
async getVideoInfo (videoPath: string): Promise<VideoInfoResult> {
|
||||
const addon = this.ensureAddon();
|
||||
const info = await addon.getVideoInfo(videoPath, 'bmp24');
|
||||
const info = await addon.getVideoInfo(videoPath);
|
||||
|
||||
let format = info.format.includes(',') ? info.format.split(',')[0] ?? info.format : info.format;
|
||||
console.log('[FFmpegAddonAdapter] Detected format:', format);
|
||||
return {
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
duration: info.duration,
|
||||
format: info.format,
|
||||
format: format,
|
||||
thumbnail: info.image,
|
||||
};
|
||||
}
|
||||
@@ -88,7 +90,7 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
|
||||
/**
|
||||
* 转换为 PCM
|
||||
*/
|
||||
async convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number }> {
|
||||
async convertToPCM (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }> {
|
||||
const addon = this.ensureAddon();
|
||||
const result = await addon.decodeAudioToPCM(filePath, pcmPath, 24000);
|
||||
|
||||
@@ -100,13 +102,8 @@ export class FFmpegAddonAdapter implements IFFmpegAdapter {
|
||||
*/
|
||||
async convertFile (inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||
const addon = this.ensureAddon();
|
||||
|
||||
if (format === 'silk' || format === 'ntsilk') {
|
||||
// 使用 Addon 的 NTSILK 转换
|
||||
await addon.convertToNTSilkTct(inputFile, outputFile);
|
||||
} else {
|
||||
throw new Error(`Format '${format}' is not supported by FFmpeg Addon`);
|
||||
}
|
||||
console.log('[FFmpegAddonAdapter] Converting file:', inputFile, 'to', outputFile, 'as', format);
|
||||
await addon.decodeAudioToFmt(inputFile, outputFile, format);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface AudioPCMResult {
|
||||
* FFmpeg interface providing all audio/video processing methods
|
||||
*/
|
||||
export interface FFmpeg {
|
||||
convertFile (inputFile: string, outputFile: string, format: string): Promise<{ success: boolean; }>;
|
||||
/**
|
||||
* Get video information including resolution, duration, format, codec and first frame thumbnail
|
||||
*/
|
||||
@@ -67,5 +68,6 @@ export interface FFmpeg {
|
||||
/**
|
||||
* Decode audio file to raw PCM data
|
||||
*/
|
||||
decodeAudioToPCM(filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number }>;
|
||||
decodeAudioToPCM (filePath: string, pcmPath: string, sampleRate?: number): Promise<{ result: boolean, sampleRate: number; }>;
|
||||
decodeAudioToFmt (filePath: string, pcmPath: string, format: string): Promise<{ channels: number; sampleRate: number; format: string; }>;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,14 @@ export class FFmpegService {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public static getAdapterName (): string {
|
||||
if (!this.adapter) {
|
||||
throw new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
|
||||
}
|
||||
return this.adapter.name;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 FFmpeg 适配器
|
||||
*/
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.9.27';
|
||||
export const napCatVersion = '4.9.38';
|
||||
|
||||
@@ -17,8 +17,14 @@ export class NTQQFriendApi {
|
||||
|
||||
async getBuddyV2SimpleInfoMap () {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
let uids: string[] = [];
|
||||
if (this.core.context.basicInfoWrapper.requireMinNTQQBuild('41679')) {
|
||||
const buddyListV2NT = await buddyService.getBuddyListV2('0', true, BuddyListReqType.KNOMAL);
|
||||
uids = buddyListV2NT.data.flatMap(item => item.buddyUids);
|
||||
} else {
|
||||
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
||||
uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
||||
}
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
@@ -47,10 +53,15 @@ export class NTQQFriendApi {
|
||||
|
||||
async getBuddyV2ExWithCate () {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
||||
const uids = buddyListV2.flatMap(item => {
|
||||
return item.buddyUids;
|
||||
});
|
||||
let uids: string[] = [];
|
||||
let buddyListV2: Awaited<ReturnType<typeof buddyService.getBuddyListV2>>['data'];
|
||||
if (this.core.context.basicInfoWrapper.requireMinNTQQBuild('41679')) {
|
||||
buddyListV2 = (await buddyService.getBuddyListV2('0', true, BuddyListReqType.KNOMAL)).data;
|
||||
uids = buddyListV2.flatMap(item => item.buddyUids);
|
||||
} else {
|
||||
buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
||||
uids = buddyListV2.flatMap(item => item.buddyUids);
|
||||
}
|
||||
const data = await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface NativePacketExportType {
|
||||
}
|
||||
|
||||
export type PacketType = 0 | 1; // 0: send, 1: recv
|
||||
export type PacketCallback = (data: { type: PacketType, uin: string, cmd: string, seq: number, hex_data: string }) => void;
|
||||
export type PacketCallback = (data: { type: PacketType, uin: string, cmd: string, seq: number, hex_data: string; }) => void;
|
||||
|
||||
interface ListenerEntry {
|
||||
callback: PacketCallback;
|
||||
@@ -27,14 +27,30 @@ interface ListenerEntry {
|
||||
|
||||
export class NativePacketHandler {
|
||||
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64'];
|
||||
private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
|
||||
private readonly MoeHooExport: { exports: NativePacketExportType; } = { exports: {} };
|
||||
protected readonly logger: LogWrapper;
|
||||
private loaded: boolean = false;
|
||||
|
||||
// 统一的监听器存储 - key: 'all' | 'type:0' | 'type:1' | 'cmd:xxx' | 'exact:type:cmd'
|
||||
private readonly listeners: Map<string, Set<ListenerEntry>> = new Map();
|
||||
|
||||
constructor ({ logger }: { logger: LogWrapper }) {
|
||||
constructor ({ logger }: { logger: LogWrapper; }) {
|
||||
this.logger = logger;
|
||||
try {
|
||||
const platform = process.platform + '.' + process.arch;
|
||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/packet/MoeHoo.' + platform + '.node');
|
||||
if (!fs.existsSync(moehoo_path)) {
|
||||
this.logger.logWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`);
|
||||
this.loaded = false;
|
||||
}
|
||||
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
|
||||
this.loaded = true;
|
||||
this.logger.log('[PacketHandler] 加载成功');
|
||||
} catch (error) {
|
||||
this.logger.logError('NativePacketClient 加载出错:', error);
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,6 +198,10 @@ export class NativePacketHandler {
|
||||
async init (version: string): Promise<boolean> {
|
||||
const version_arch = version + '-' + process.arch;
|
||||
try {
|
||||
if (!this.loaded) {
|
||||
this.logger.logWarn('NativePacketClient 未成功加载,无法初始化');
|
||||
return false;
|
||||
}
|
||||
const send = typedOffset[version_arch]?.send;
|
||||
const recv = typedOffset[version_arch]?.recv;
|
||||
if (!send || !recv) {
|
||||
@@ -193,16 +213,11 @@ export class NativePacketHandler {
|
||||
this.logger.logWarn(`NativePacketClient: 不支持的平台: ${platform}`);
|
||||
return false;
|
||||
}
|
||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/packet/MoeHoo.' + platform + '.node');
|
||||
|
||||
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
|
||||
if (!fs.existsSync(moehoo_path)) {
|
||||
this.logger.logWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`);
|
||||
return false;
|
||||
}
|
||||
this.MoeHooExport.exports.initHook?.(send, recv, (type: PacketType, uin: string, cmd: string, seq: number, hex_data: string) => {
|
||||
this.emitPacket(type, uin, cmd, seq, hex_data);
|
||||
}, true);
|
||||
this.logger.log('[PacketHandler] 初始化成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.logError('NativePacketClient 初始化出错:', error);
|
||||
|
||||
@@ -10,9 +10,20 @@ export interface NodeIKernelBuddyService {
|
||||
categroyName: string,
|
||||
categroyMbCount: number,
|
||||
onlineCount: number,
|
||||
buddyUids: Array<string>
|
||||
}>
|
||||
buddyUids: Array<string>;
|
||||
}>;
|
||||
}>;
|
||||
getBuddyListV2 (callFrom: string, isPullRefresh: boolean, reqType: BuddyListReqType): Promise<GeneralCallResult & {
|
||||
data: Array<{
|
||||
categoryId: number,
|
||||
categorySortId: number,
|
||||
categroyName: string,
|
||||
categroyMbCount: number,
|
||||
onlineCount: number,
|
||||
buddyUids: Array<string>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
||||
getBuddyListFromCache (reqType: BuddyListReqType): Promise<Array<
|
||||
{
|
||||
@@ -21,7 +32,7 @@ export interface NodeIKernelBuddyService {
|
||||
categroyName: string, // 分类名
|
||||
categroyMbCount: number, // 不懂
|
||||
onlineCount: number, // 在线数目
|
||||
buddyUids: Array<string>// Uids
|
||||
buddyUids: Array<string>;// Uids
|
||||
}>>;
|
||||
|
||||
addKernelBuddyListener (listener: NodeIKernelBuddyListener): number;
|
||||
@@ -36,7 +47,7 @@ export interface NodeIKernelBuddyService {
|
||||
|
||||
getBuddyRemark (uid: number): string;
|
||||
|
||||
setBuddyRemark(param: { uid: string, remark: string, signInfo?: unknown }): void;
|
||||
setBuddyRemark (param: { uid: string, remark: string, signInfo?: unknown; }): void;
|
||||
|
||||
getAvatarUrl (uid: number): string;
|
||||
|
||||
@@ -118,7 +129,7 @@ export interface NodeIKernelBuddyService {
|
||||
|
||||
reportDoubtBuddyReqUnread (): void;
|
||||
|
||||
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<GeneralCallResult & { arkMsg: string }>;
|
||||
getBuddyRecommendContactArkJson (uid: string, phoneNumber: string): Promise<GeneralCallResult & { arkMsg: string; }>;
|
||||
|
||||
isNull (): boolean;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ import { FFmpegService } from '@/common/ffmpeg';
|
||||
const out_format = ['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac'];
|
||||
|
||||
type Payload = {
|
||||
out_format: string
|
||||
out_format: string;
|
||||
} & GetFilePayload;
|
||||
|
||||
export default class GetRecord extends GetFileBase {
|
||||
@@ -28,9 +28,13 @@ export default class GetRecord extends GetFileBase {
|
||||
try {
|
||||
await fs.access(outputFile);
|
||||
} catch {
|
||||
if (FFmpegService.getAdapterName() === 'FFmpegAddon') {
|
||||
await FFmpegService.convertFile(inputFile, outputFile, payload.out_format);
|
||||
} else {
|
||||
await this.decodeFile(inputFile, pcmFile);
|
||||
await FFmpegService.convertFile(pcmFile, outputFile, payload.out_format);
|
||||
}
|
||||
}
|
||||
const base64Data = await fs.readFile(outputFile, { encoding: 'base64' });
|
||||
res.file = outputFile;
|
||||
res.url = outputFile;
|
||||
|
||||
@@ -37,7 +37,7 @@ export class SendGroupNotice extends OneBotAction<Payload, null> {
|
||||
await checkFileExist(path, 5000);
|
||||
const ImageUploadResult = await this.core.apis.GroupApi.uploadGroupBulletinPic(payload.group_id.toString(), path);
|
||||
if (ImageUploadResult.errCode !== 0) {
|
||||
throw new Error(`群公告${payload.image}设置失败,图片上传失败`);
|
||||
throw new Error(`群公告${payload.image}设置失败,图片上传失败 , 错误信息:${ImageUploadResult.errMsg}`);
|
||||
}
|
||||
|
||||
unlink(path).catch(() => { });
|
||||
|
||||
@@ -47,8 +47,12 @@ export class DownloadFileRecordStream extends BaseDownloadStream<Payload, Downlo
|
||||
streamPath = outputFile;
|
||||
} catch {
|
||||
// 尝试解码 silk 到 pcm 再用 ffmpeg 转换
|
||||
if (FFmpegService.getAdapterName() === 'FFmpegAddon') {
|
||||
await FFmpegService.convertFile(downloadPath, outputFile, payload.out_format);
|
||||
} else {
|
||||
await this.decodeFile(downloadPath, pcmFile);
|
||||
await FFmpegService.convertFile(pcmFile, outputFile, payload.out_format);
|
||||
}
|
||||
streamPath = outputFile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,12 +907,12 @@ export class OneBotMsgApi {
|
||||
|
||||
async parsePrivateMsgEvent (msg: RawMessage, grayTipElement: GrayTipElement) {
|
||||
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
||||
if (grayTipElement.jsonGrayTipElement.busiId === 1061) {
|
||||
if (grayTipElement.jsonGrayTipElement.busiId.toString() === '1061') {
|
||||
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||
if (PokeEvent) {
|
||||
return PokeEvent;
|
||||
}
|
||||
} else if (grayTipElement.jsonGrayTipElement.busiId === 19324 && msg.peerUid !== '') {
|
||||
} else if (grayTipElement.jsonGrayTipElement.busiId.toString() === '19324' && msg.peerUid !== '') {
|
||||
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||
}
|
||||
}
|
||||
|
||||
BIN
tests/QQNT.dll
BIN
tests/QQNT.dll
Binary file not shown.
@@ -56,6 +56,7 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
|
||||
// }),
|
||||
cp({
|
||||
targets: [
|
||||
{ src: './napiloader/', dest: 'dist', flatten: true },
|
||||
{ src: './src/native/', dest: 'dist/native', flatten: false },
|
||||
{ src: './manifest.json', dest: 'dist' },
|
||||
{ src: './src/core/external/napcat.json', dest: 'dist/config/' },
|
||||
|
||||
Reference in New Issue
Block a user