feat: auto encode silk

This commit is contained in:
linyuchen 2024-02-20 22:39:24 +08:00
parent 0e4de038ca
commit c4e54fa259
11 changed files with 973 additions and 84 deletions

View File

@ -43,8 +43,8 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] @群成员 - [x] @群成员
- [x] 语音 - [x] 语音
- [x] json消息(只上报) - [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收)
- [ ] 红包 - [ ] 红包
- [ ] 转发消息记录
- [ ] xml - [ ] xml
支持的api: 支持的api:
@ -109,7 +109,7 @@ LiteLoaderQQNT的OneBot11协议插件
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR - [x] 支持正、反向websocket感谢@disymayufei的PR
- [ ] 转发消息记录 - [x] 转发消息记录
- [ ] 好友点赞api - [ ] 好友点赞api
## onebot11文档 ## onebot11文档

892
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,13 @@
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save", "postinstall": "cross-env ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save",
"build": "npm run build-main && npm run build-preload && npm run build-renderer", "build": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js", "build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js", "build-preload": "webpack --config webpack.preload.config.js",
"build-renderer": "webpack --config webpack.renderer.config.js", "build-renderer": "webpack --config webpack.renderer.config.js",
"build-mac": "npm run build && cp manifest.json dist/ && npm run deploy-mac", "build-mac": "npm run build && cp manifest.json dist/ && npm run deploy-mac",
"deploy-mac": "cp dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOnebot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOnebot/",
"build-win": "npm run build && cp manifest.json dist/ && npm run deploy-win", "build-win": "npm run build && cp manifest.json dist/ && npm run deploy-win",
"deploy-win": "cmd /c \"copy dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOnebot\\\"" "deploy-win": "cmd /c \"copy dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOnebot\\\""
}, },
@ -20,6 +20,8 @@
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"music-metadata": "^8.1.4",
"silk-wasm": "^3.2.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
@ -29,10 +31,11 @@
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4"
"ws": "^8.16.0"
} }
} }

View File

@ -2,8 +2,9 @@ import * as path from "path";
import {selfInfo} from "./data"; import {selfInfo} from "./data";
import {ConfigUtil} from "./config"; import {ConfigUtil} from "./config";
import util from "util"; import util from "util";
import {encode, getDuration} from "silk-wasm";
const fs = require('fs'); import fs from 'fs';
import {v4 as uuidv4} from "uuid";
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
@ -13,7 +14,7 @@ export function getConfigUtil() {
} }
export function log(...msg: any[]) { export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log){ if (!getConfigUtil().getConfig().log) {
return return
} }
let currentDateTime = new Date().toLocaleString(); let currentDateTime = new Date().toLocaleString();
@ -24,9 +25,9 @@ export function log(...msg: any[]) {
const currentDate = `${year}-${month}-${day}`; const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : "" const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = ""; let logMsg = "";
for (let msgItem of msg){ for (let msgItem of msg) {
// 判断是否是对象 // 判断是否是对象
if (typeof msgItem === "object"){ if (typeof msgItem === "object") {
logMsg += JSON.stringify(msgItem) + " "; logMsg += JSON.stringify(msgItem) + " ";
continue; continue;
} }
@ -35,7 +36,7 @@ export function log(...msg: any[]) {
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg); // sendLog(...msg);
// console.log(msg) // console.log(msg)
fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), logMsg, (err: any) => { fs.appendFile(path.join(CONFIG_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
}) })
} }
@ -54,7 +55,7 @@ export function sleep(ms: number): Promise<void> {
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number=3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const startTime = Date.now(); const startTime = Date.now();
@ -72,7 +73,7 @@ export function checkFileReceived(path: string, timeout: number=3000): Promise<v
}); });
} }
export async function file2base64(path: string){ export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile);
let result = { let result = {
err: "", err: "",
@ -109,10 +110,69 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
// 如果老对象和新对象的当前属性都是对象,则递归合并 // 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]); mergeNewProperties(newObj[key], oldObj[key]);
} else if(typeof oldObj[key] === 'object' || typeof newObj[key] === 'object'){ } else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖 // 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]; oldObj[key] = newObj[key];
} }
} }
}); });
} }
export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7;
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: "r",
});
const fileHeader = buffer.toString("hex", 0, bytesToRead);
return fileHeader;
} catch (err) {
console.error("读取文件错误:", err);
return;
}
}
async function getAudioSampleRate(filePath: string) {
try {
const mm = await import('music-metadata');
const metadata = await mm.parseFile(filePath);
log(`${filePath}采样率`, metadata.format.sampleRate);
return metadata.format.sampleRate;
} catch (error) {
log(`${filePath}采样率获取失败`, error.stack);
// console.error(error);
}
}
try {
const fileName = path.basename(filePath);
const pcm = fs.readFileSync(filePath);
const pttPath = path.join(CONFIG_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换`)
const sampleRate = await getAudioSampleRate(filePath) || 44100;
const silk = await encode(pcm, sampleRate);
fs.writeFileSync(pttPath, silk.data);
log(`语音文件${filePath}转换成功!`)
return {
converted: true,
path: pttPath,
duration: silk.duration,
};
} else {
const duration = getDuration(pcm);
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}

View File

@ -2,7 +2,6 @@
import {BrowserWindow, ipcMain} from 'electron'; import {BrowserWindow, ipcMain} from 'electron';
import fs from 'fs'; import fs from 'fs';
import {Config} from "../common/types"; import {Config} from "../common/types";
import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "../common/channels"; import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "../common/channels";
import {initWebsocket, postMsg} from "../onebot11/server"; import {initWebsocket, postMsg} from "../onebot11/server";

View File

@ -8,6 +8,8 @@ import {
SendTextElement SendTextElement
} from "./types"; } from "./types";
import {NTQQApi} from "./ntcall"; import {NTQQApi} from "./ntcall";
import {encodeSilk, log} from "../common/utils";
import fs from "fs";
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
@ -79,7 +81,12 @@ export class SendMsgElementConstructor {
} }
static async ptt(pttPath: string): Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(pttPath); const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath);
if (converted){
fs.unlink(silkPath, ()=>{});
}
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
elementId: "", elementId: "",
@ -88,7 +95,8 @@ export class SendMsgElementConstructor {
filePath: path, filePath: path,
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: fileSize,
duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算 // duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration / 1000,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
voiceChangeType: 0, voiceChangeType: 0,

View File

@ -3,10 +3,10 @@ import {log, sleep} from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {Group, RawMessage, User} from "./types"; import {Group, RawMessage, User} from "./types";
import {addHistoryMsg, friends, groups, msgHistory} from "../common/data"; import {addHistoryMsg, friends, groups, msgHistory} from "../common/data";
import {v4 as uuidv4} from 'uuid';
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent"; import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {postMsg} from "../onebot11/server"; import {postMsg} from "../onebot11/server";
import {v4 as uuidv4} from "uuid"
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}

View File

@ -1,10 +1,10 @@
import {ipcMain} from "electron"; import {ipcMain} from "electron";
import {v4 as uuidv4} from "uuid";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook"; import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log} from "../common/utils"; import {log} from "../common/utils";
import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types"; import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types";
import * as fs from "fs"; import * as fs from "fs";
import {addHistoryMsg, msgHistory, selfInfo} from "../common/data"; import {addHistoryMsg, msgHistory, selfInfo} from "../common/data";
import {v4 as uuidv4} from "uuid"
interface IPCReceiveEvent { interface IPCReceiveEvent {
eventName: string eventName: string

View File

@ -4,11 +4,11 @@ import {OB11MessageData, OB11MessageDataType, OB11MessageNode, OB11PostSendMsg}
import {NTQQApi, Peer} from "../../ntqqapi/ntcall"; import {NTQQApi, Peer} from "../../ntqqapi/ntcall";
import {SendMsgElementConstructor} from "../../ntqqapi/constructor"; import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
import {uri2local} from "../utils"; import {uri2local} from "../utils";
import {v4 as uuid4} from 'uuid';
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types"; import {ActionName, BaseCheckResult} from "./types";
import * as fs from "fs"; import * as fs from "fs";
import {log, sleep} from "../../common/utils"; import {log} from "../../common/utils";
import {v4 as uuidv4} from "uuid"
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@ -55,7 +55,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload); const messages = this.convertMessage2List(payload);
const fmNum = this.forwardMsgNum(payload) const fmNum = this.forwardMsgNum(payload)
if ( fmNum && fmNum != messages.length) { if (fmNum && fmNum != messages.length) {
return { return {
valid: false, valid: false,
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素" message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
@ -227,7 +227,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const file = sendMsg.data?.file const file = sendMsg.data?.file
if (file) { if (file) {
const {path, isLocal} = (await uri2local(uuid4(), file)) const {path, isLocal} = (await uri2local(uuidv4(), file))
if (path) { if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件 if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path) deleteAfterSentFiles.push(path)
@ -239,15 +239,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
} }
} } break;
break;
// case OB11MessageDataType.node: {
// try {
// await this.handleForwardNode(peer, sendMsg, group);
// } catch (e) {
// log("forward msg crash", e.stack)
// }
// }
} }
} }
@ -258,7 +250,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete=false) { private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = false) {
if (!sendElements.length) { if (!sendElements.length) {
throw ("消息体无法解析") throw ("消息体无法解析")
} }

View File

@ -8,6 +8,7 @@
"allowJs": true, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"moduleResolution": "node", "moduleResolution": "node",
// "sourceMap": true
}, },
"include": ["src/*"], "include": ["src/*"],
"exclude": [ "exclude": [

View File

@ -1,8 +1,14 @@
// import path from "path"; // import path from "path";
const path = require('path'); const path = require('path');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = { const ignoreModules = [
"silk-wasm", "electron"
];
const copyModules = ["silk-wasm"]
let config = {
// target: 'node', // target: 'node',
entry: { entry: {
// main: './src/main.ts', // main: './src/main.ts',
@ -15,11 +21,10 @@ module.exports = {
// libraryTarget: "commonjs2", // libraryTarget: "commonjs2",
// chunkFormat: "commonjs", // chunkFormat: "commonjs",
}, },
externals: [ externals: ignoreModules,
// "express",
"electron", "fs"],
experiments: { experiments: {
// outputModule: true // outputModule: true
// asyncWebAssembly: true
}, },
resolve: { resolve: {
extensions: ['.js', '.ts'] extensions: ['.js', '.ts']
@ -33,7 +38,6 @@ module.exports = {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: ['@babel/preset-env'], presets: ['@babel/preset-env'],
} }
} }
}, },
@ -46,8 +50,7 @@ module.exports = {
// configFile: 'src/tsconfig.json' // configFile: 'src/tsconfig.json'
} }
} }
} }]
]
}, },
optimization: { optimization: {
minimize: false, minimize: false,
@ -56,5 +59,18 @@ module.exports = {
extractComments: false, extractComments: false,
}), }),
], ],
},
plugins: [
new CopyPlugin({
patterns: copyModules.map(m=>{
m = `node_modules/${m}`
return {
from: m,
to: m
} }
})
}),
], // devtool: 'source-map',
} }
module.exports = config