Compare commits

...

7 Commits

Author SHA1 Message Date
手瓜一十雪
964014fc5c fix: #355 2024-09-10 22:27:06 +08:00
手瓜一十雪
fc2bb6d8c3 docs: 移除注释 2024-09-10 18:58:27 +08:00
手瓜一十雪
1b10252d76 remove: NTQQCacheApi 2024-09-10 18:42:49 +08:00
手瓜一十雪
ad8af12a10 refactor: fsPromise catch 2024-09-10 18:41:01 +08:00
手瓜一十雪
b040c9b118 refactor: audio 2024-09-10 18:39:14 +08:00
Alen
f6da7da90b Merge pull request #352 from cnxysoft/upmain
fix: 踢官方机器人报错
2024-09-10 00:40:58 +08:00
Alen
a745185408 fix: 踢官方机器人报错 2024-09-10 00:38:10 +08:00
11 changed files with 101 additions and 146 deletions

View File

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

View File

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

View File

@@ -1,66 +1,64 @@
import fs from 'fs'; import fs from 'fs';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import fsPromise from 'fs/promises'; import fsPromise from 'fs/promises';
import path from 'node:path'; import path from 'node:path';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from './log'; import { LogWrapper } from './log';
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) { const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
async function guessDuration(pttPath: string) { const EXIT_CODES = [0, 255];
const pttFileInfo = await fsPromise.stat(pttPath); const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
let duration = pttFileInfo.size / 1024 / 3; // 3kb/s
duration = Math.floor(duration);
duration = Math.max(1, duration);
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
let duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', err => {
logger.log('FFmpeg处理转换出错: ', err.message);
reject(err);
});
cp.on('exit', async (code, signal) => {
if (code == null || EXIT_CODES.includes(code)) {
try {
const data = await fsPromise.readFile(pcmPath);
await fsPromise.unlink(pcmPath);
resolve(data);
} catch (err) {
reject(err);
}
} else {
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
reject(new Error('FFmpeg处理转换失败'));
}
});
});
}
async function handleWavFile(file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
return await convert(filePath, pcmPath, logger);
}
return file;
}
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try { try {
const file = await fsPromise.readFile(filePath); const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID()); const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) { if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`); logger.log(`语音文件${filePath}需要转换成silk`);
const _isWav = isWav(file); const pcmPath = `${pttPath}.pcm`;
const pcmPath = pttPath + '.pcm'; const input = isWav(file) ? await handleWavFile(file, filePath, pcmPath, logger) : await convert(filePath, pcmPath, logger);
let sampleRate = 0; const silk = await encode(input, 24000);
const convert = () => { await fsPromise.writeFile(pttPath, silk.data);
return new Promise<Buffer>((resolve, reject) => {
// todo: 通过配置文件获取ffmpeg路径
const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', err => {
logger.log('FFmpeg处理转换出错: ', err.message);
return reject(err);
});
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255];
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000;
const data = fs.readFileSync(pcmPath);
fs.unlink(pcmPath, (err) => {
});
return resolve(data);
}
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
reject(Error('FFmpeg处理转换失败'));
});
});
};
let input: Buffer;
if (!_isWav) {
input = await convert();
} else {
input = file;
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const { fmt } = getWavFileInfo(input);
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
input = await convert();
}
}
const silk = await encode(input, sampleRate);
fs.writeFileSync(pttPath, silk.data);
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return { return {
converted: true, converted: true,
@@ -68,15 +66,13 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
duration: silk.duration / 1000, duration: silk.duration / 1000,
}; };
} else { } else {
const silk = file;
let duration = 0; let duration = 0;
try { try {
duration = getDuration(silk) / 1000; duration = getDuration(file) / 1000;
} catch (e: any) { } catch (e: any) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack); logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
duration = await guessDuration(filePath); duration = await guessDuration(filePath, logger);
} }
return { return {
converted: false, converted: false,
path: filePath, path: filePath,
@@ -87,4 +83,4 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
logger.logError('convert silk failed', error.stack); logger.logError('convert silk failed', error.stack);
return {}; return {};
} }
} }

View File

@@ -1 +1 @@
export const napCatVersion = '2.4.3'; export const napCatVersion = '2.4.4';

View File

@@ -1,63 +0,0 @@
import {
CacheFileListItem,
CacheFileType,
ChatCacheListItemBasic,
ChatType,
InstanceContext,
NapCatCore,
} from '@/core';
export class NTQQCacheApi {
context: InstanceContext;
core: NapCatCore;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
async setCacheSilentScan(isSilent: boolean = true) {
return '';
}
getCacheSessionPathList() {
return '';
}
clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
// 参数未验证
return this.context.session.getStorageCleanService().clearCacheDataByKeys(cacheKeys);
}
addCacheScannedPaths(pathMap: object = {}) {
return this.context.session.getStorageCleanService().addCacheScanedPaths(pathMap);
}
scanCache() {
//return (await this.context.session.getStorageCleanService().scanCache()).size;
}
getHotUpdateCachePath() {
// 未实现
return '';
}
getDesktopTmpPath() {
// 未实现
return '';
}
getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return this.context.session.getStorageCleanService().getChatCacheInfo(type, pageSize, 1, pageIndex);
}
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
// const _lastRecord = lastRecord ? lastRecord : { fileType: fileType };
// 需要五个参数
// return napCatCore.session.getStorageCleanService().getFileCacheInfo();
}
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return this.context.session.getStorageCleanService().clearChatCacheInfo(chats, fileKeys);
}
}

View File

@@ -33,7 +33,7 @@ export class NTQQFileApi {
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
this.core = core; this.core = core;
this.rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey', this.context.logger); this.rkeyManager = new RkeyManager('https://llob.linyuchen.net/rkey', this.context.logger);
} }
async copyFile(filePath: string, destPath: string) { async copyFile(filePath: string, destPath: string) {
@@ -207,7 +207,9 @@ export class NTQQFileApi {
throw new Error('文件异常大小为0'); throw new Error('文件异常大小为0');
} }
if (converted) { if (converted) {
fsPromises.unlink(silkPath); fsPromises.unlink(silkPath).then().catch(
(e) => this.context.logger.logError('删除临时文件失败', e)
);
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
@@ -246,7 +248,6 @@ export class NTQQFileApi {
} }
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) { async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
//logDebug('receive downloadMedia task', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force);
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
if (force) { if (force) {
@@ -346,8 +347,8 @@ export class NTQQFileApi {
if (url) { if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url); const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
const imageAppid = parsedUrl.searchParams.get('appid'); const imageAppid = parsedUrl.searchParams.get('appid');
const isNTFlavoredPic = imageAppid && ['1406', '1407'].includes(imageAppid); const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
if (isNTFlavoredPic) { if (isNTV2) {
let rkey = parsedUrl.searchParams.get('rkey'); let rkey = parsedUrl.searchParams.get('rkey');
if (rkey) { if (rkey) {
return IMAGE_HTTP_HOST_NT + url; return IMAGE_HTTP_HOST_NT + url;
@@ -356,11 +357,9 @@ export class NTQQFileApi {
rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey; rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${rkey}`; return IMAGE_HTTP_HOST_NT + url + `${rkey}`;
} else { } else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url; return IMAGE_HTTP_HOST + url;
} }
} else if (fileMd5 || md5HexStr) { } else if (fileMd5 || md5HexStr) {
// 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`; return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
} }
this.context.logger.logDebug('图片url获取失败', element); this.context.logger.logDebug('图片url获取失败', element);

View File

@@ -5,5 +5,4 @@ export * from './msg';
export * from './user'; export * from './user';
export * from './webapi'; export * from './webapi';
export * from './sign'; export * from './sign';
export * from './system'; export * from './system';
export * from './cache';

View File

@@ -140,15 +140,25 @@ export class OneBotGroupApi {
} }
if (element.grayTipElement.jsonGrayTipElement.busiId == 2407) { if (element.grayTipElement.jsonGrayTipElement.busiId == 2407) {
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
const memberUin = json.items[1].param[0]; const type = json.items[json.items.length - 1]?.txt;
const title = json.items[3].txt; switch (type) {
logger.logDebug('收到群成员新头衔消息', json); case "头衔": {
return new OB11GroupTitleEvent( const memberUin = json.items[1].param[0];
this.core, const title = json.items[3].txt;
parseInt(msg.peerUid), logger.logDebug('收到群成员新头衔消息', json);
parseInt(memberUin), return new OB11GroupTitleEvent(
title, this.core,
); parseInt(msg.peerUid),
parseInt(memberUin),
title,
);
};
case "移出":
logger.logDebug('收到机器人被踢消息', json);
return;
default:
logger.logWarn('收到未知的灰条消息', json);
}
} }
} }
} }

View File

@@ -21,6 +21,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
core: NapCatCore; core: NapCatCore;
logger: LogWrapper; logger: LogWrapper;
private heartbeatIntervalId: NodeJS.Timeout | null = null; private heartbeatIntervalId: NodeJS.Timeout | null = null;
wsClientWithEvent: WebSocket[] = [];
constructor( constructor(
ip: string, ip: string,
@@ -46,7 +47,12 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
} }
//鉴权 //鉴权
this.authorize(token, wsClient, wsReq); this.authorize(token, wsClient, wsReq);
this.connectEvent(core, wsClient); let paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
const isEventConnect = paramUrl === '/event' || paramUrl === '' || paramUrl === '/';
if (isEventConnect) {
this.connectEvent(core, wsClient);
}
wsClient.on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Client Error:', err.message)); wsClient.on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Client Error:', err.message));
wsClient.on('message', (message) => { wsClient.on('message', (message) => {
this.handleMessage(wsClient, message).then().catch(this.logger.logError); this.handleMessage(wsClient, message).then().catch(this.logger.logError);
@@ -59,13 +65,21 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
}); });
wsClient.once('close', () => { wsClient.once('close', () => {
this.wsClientsMutex.runExclusive(async () => { this.wsClientsMutex.runExclusive(async () => {
const index = this.wsClients.indexOf(wsClient); const NormolIndex = this.wsClients.indexOf(wsClient);
if (index !== -1) { if (NormolIndex !== -1) {
this.wsClients.splice(index, 1); this.wsClients.splice(NormolIndex, 1);
} }
const EventIndex = this.wsClientWithEvent.indexOf(wsClient);
if (EventIndex !== -1) {
this.wsClientWithEvent.splice(EventIndex, 1);
}
}); });
}); });
await this.wsClientsMutex.runExclusive(async () => { await this.wsClientsMutex.runExclusive(async () => {
if(isEventConnect){
this.wsClientWithEvent.push(wsClient);
}
this.wsClients.push(wsClient); this.wsClients.push(wsClient);
}); });
}).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message)); }).on('error', (err) => this.logger.log('[OneBot] [WebSocket Server] Server Error:', err.message));
@@ -81,7 +95,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
onEvent<T extends OB11EmitEventContent>(event: T) { onEvent<T extends OB11EmitEventContent>(event: T) {
this.wsClientsMutex.runExclusive(async () => { this.wsClientsMutex.runExclusive(async () => {
this.wsClients.forEach((wsClient) => { this.wsClientWithEvent.forEach((wsClient) => {
wsClient.send(JSON.stringify(event)); wsClient.send(JSON.stringify(event));
}); });
}); });

View File

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

View File

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