Compare commits

..

5 Commits

Author SHA1 Message Date
手瓜一十雪
62b04007bb fix 2025-04-04 13:29:00 +08:00
手瓜一十雪
39d938f603 fix 2025-04-04 13:28:50 +08:00
手瓜一十雪
6f6cffbc67 feat: codec wav 2025-04-04 13:28:28 +08:00
手瓜一十雪
f739c88106 feat 2025-04-04 12:40:25 +08:00
手瓜一十雪
320a23de20 feat: audio 2025-04-04 12:16:56 +08:00
138 changed files with 1929 additions and 6003 deletions

View File

@@ -1,9 +1,8 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
_Modern protocol-side framework implemented based on NTQQ._
@@ -38,23 +37,18 @@ _Modern protocol-side framework implemented based on NTQQ._
| Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) |
|:-:|:-:|:-:|:-:|
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://docs.napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
|:-:|:-:|:-:|:-:|
| QQ Group | [![QQ Group#4](https://img.shields.io/badge/QQ%20Group%234-Join-blue)](https://qm.qq.com/q/CMmPbGw0jA) | [![QQ Group#3](https://img.shields.io/badge/QQ%20Group%233-Join-blue)](https://qm.qq.com/q/8zJMLjqy2Y) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/HaRcfrHpUk) | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) |
|:-:|:-:|:-:|:-:|:-:|
| Telegram | [![Telegram](https://img.shields.io/badge/Telegram-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) |
|:-:|:-:|
| Contact | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/HaRcfrHpUk) | [![Telegram](https://img.shields.io/badge/Telegram-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) |
|:-:|:-:|:-:|:-:|
## Thanks
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
+ [LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目部分开发
+ [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
---

Binary file not shown.

BIN
external/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.19-34740",
"verHash": "f31348f2",
"linuxVersion": "3.2.17-34740",
"linuxVerHash": "5aa2d8d6",
"version": "9.9.18-32869",
"verHash": "e735296c",
"linuxVersion": "3.2.16-32869",
"linuxVerHash": "4c192ba9",
"private": true,
"description": "QQ",
"productName": "QQ",
@@ -16,10 +16,27 @@
"bin": {
"qd": "externals/devtools/cli/index.js"
},
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"main": "./loadNapCat.js",
"buildVersion": "34740",
"peerDependenciesMeta": {
"*": {
"optional": true
}
},
"pnpm": {
"patchedDependencies": {
"@vue/runtime-dom@3.5.12": "patches/@vue__runtime-dom@3.5.12.patch",
"@swc/helpers@0.5.3": "patches/@swc__helpers@0.5.3.patch",
"vuex@4.1.0": "patches/vuex@4.1.0.patch"
}
},
"buildVersion": "32869",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",
"eleArch": "x64"
}
}

View File

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

View File

@@ -55,7 +55,6 @@
"ahooks": "^3.8.4",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"echarts": "^5.5.1",
"event-source-polyfill": "^1.0.31",
"framer-motion": "^12.0.6",
@@ -89,7 +88,6 @@
"@eslint/js": "^9.19.0",
"@react-types/shared": "^3.26.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/crypto-js": "^4.2.2",
"@types/event-source-polyfill": "^1.0.5",
"@types/fabric": "^5.3.9",
"@types/node": "^22.12.0",

View File

@@ -92,9 +92,7 @@ const MusicInsert = () => {
className="w-96"
fullWidth
selectedKey={mode}
onSelectionChange={(key) => {
if (key !== null) setMode(key)
}}
onSelectionChange={setMode}
>
<Tab title="主流平台" key="default" className="flex flex-col gap-2">
<Select

View File

@@ -26,7 +26,7 @@ const itemVariants = {
opacity: 1,
scale: 1,
y: 0,
transition: { type: 'spring' as const, stiffness: 300, damping: 20 }
transition: { type: 'spring', stiffness: 300, damping: 20 }
}
}

View File

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

View File

@@ -3,7 +3,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill'
import { LogLevel } from '@/const/enum'
import { serverRequest } from '@/utils/request'
import CryptoJS from "crypto-js";
export interface Log {
level: LogLevel
message: string
@@ -17,10 +17,9 @@ export default class WebUIManager {
}
public static async loginWithToken(token: string) {
const sha256 = CryptoJS.SHA256(token + '.napcat').toString();
const { data } = await serverRequest.post<ServerResponse<AuthResponse>>(
'/auth/login',
{ hash: sha256 }
{ token }
)
return data.data.Credential
}

View File

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

View File

@@ -47,22 +47,6 @@ export default function WebLoginPage() {
}
}
// 处理全局键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !isLoading) {
onSubmit()
}
}
useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
// 清理函数
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [tokenValue, isLoading]) // 依赖项包含用于登录的状态
useEffect(() => {
if (token) {
onSubmit()

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.7.78",
"version": "4.7.13",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -21,6 +21,7 @@
"@eslint/compat": "^1.2.2",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@ffmpeg.wasm/main": "^0.13.1",
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4",
@@ -31,6 +32,7 @@
"@types/express": "^5.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.0.1",
"@types/node-wav": "^0.0.4",
"@types/on-finished": "^2.3.4",
"@types/qrcode-terminal": "^0.12.2",
"@types/react-color": "^3.0.13",
@@ -43,27 +45,33 @@
"commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"esbuild": "0.25.5",
"esbuild": "0.25.0",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.29.1",
"express-rate-limit": "^7.5.0",
"fast-xml-parser": "^4.3.6",
"file-type": "^21.0.0",
"file-type": "^20.0.0",
"globals": "^16.0.0",
"image-size": "^1.1.1",
"json5": "^2.2.3",
"multer": "^2.0.1",
"multer": "^1.4.5-lts.1",
"napcat.protobuf": "^1.1.4",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",
"vite-plugin-cp": "^6.0.0",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0"
},
"dependencies": {
"@breezystack/lamejs": "^1.2.7",
"@ffmpeg.wasm/core-mt": "^0.13.2",
"audio-decode": "^2.2.2",
"express": "^5.0.0",
"node-wav": "^0.0.2",
"silk-wasm": "^3.6.1",
"wavefile": "^11.0.0",
"ws": "^8.18.0"
}
}
}

View File

@@ -0,0 +1,145 @@
// WAV 文件头结构
interface WavHeader {
riffChunkId: string; // "RIFF"
riffChunkSize: number; // 文件大小 - 8
riffFormat: string; // "WAVE"
fmtChunkId: string; // "fmt "
fmtChunkSize: number; // 16
audioFormat: number; // 1 = PCM
numChannels: number; // 声道数
sampleRate: number; // 采样率
byteRate: number; // 字节率 (SampleRate * NumChannels * BitsPerSample / 8)
blockAlign: number; // 块对齐 (NumChannels * BitsPerSample / 8)
bitsPerSample: number; // 采样位数
dataChunkId: string; // "data"
dataChunkSize: number; // 音频数据大小
}
export class WavEncoder {
private header: WavHeader;
private data: Buffer;
private dataOffset: number;
public bitsPerSample: number;
constructor(sampleRate: number, numChannels: number, bitsPerSample: number) {
if (![8, 16, 24, 32].includes(bitsPerSample)) {
throw new Error("Unsupported bitsPerSample value. Must be 8, 16, 24, or 32.");
}
this.bitsPerSample = bitsPerSample;
this.header = {
riffChunkId: "RIFF",
riffChunkSize: 0, // 待计算
riffFormat: "WAVE",
fmtChunkId: "fmt ",
fmtChunkSize: 16,
audioFormat: 1, // PCM
numChannels: numChannels,
sampleRate: sampleRate,
byteRate: sampleRate * numChannels * bitsPerSample / 8,
blockAlign: numChannels * bitsPerSample / 8,
bitsPerSample: bitsPerSample,
dataChunkId: "data",
dataChunkSize: 0 // 待计算
};
this.data = Buffer.alloc(0);
this.dataOffset = 0;
}
public write(buffer: Buffer): void {
this.data = Buffer.concat([this.data, buffer]);
this.dataOffset += buffer.length;
}
public encode(): Buffer {
this.header.dataChunkSize = this.dataOffset;
this.header.riffChunkSize = 36 + this.dataOffset;
const headerBuffer = Buffer.alloc(44);
headerBuffer.write(this.header.riffChunkId, 0, 4, 'ascii');
headerBuffer.writeUInt32LE(this.header.riffChunkSize, 4);
headerBuffer.write(this.header.riffFormat, 8, 4, 'ascii');
headerBuffer.write(this.header.fmtChunkId, 12, 4, 'ascii');
headerBuffer.writeUInt32LE(this.header.fmtChunkSize, 16);
headerBuffer.writeUInt16LE(this.header.audioFormat, 20);
headerBuffer.writeUInt16LE(this.header.numChannels, 22);
headerBuffer.writeUInt32LE(this.header.sampleRate, 24);
headerBuffer.writeUInt32LE(this.header.byteRate, 28);
headerBuffer.writeUInt16LE(this.header.blockAlign, 32);
headerBuffer.writeUInt16LE(this.header.bitsPerSample, 34);
headerBuffer.write(this.header.dataChunkId, 36, 4, 'ascii');
headerBuffer.writeUInt32LE(this.header.dataChunkSize, 40);
return Buffer.concat([headerBuffer, this.data]);
}
}
export class WavDecoder {
private header: WavHeader;
private data: Buffer;
private dataOffset: number;
public bitsPerSample: number;
constructor(private buffer: Buffer) {
this.header = {
riffChunkId: "",
riffChunkSize: 0,
riffFormat: "",
fmtChunkId: "",
fmtChunkSize: 0,
audioFormat: 0,
numChannels: 0,
sampleRate: 0,
byteRate: 0,
blockAlign: 0,
bitsPerSample: 0,
dataChunkId: "",
dataChunkSize: 0
};
this.data = Buffer.alloc(0);
this.dataOffset = 0;
this.decodeHeader();
this.decodeData();
this.bitsPerSample = this.header.bitsPerSample;
}
private decodeHeader(): void {
this.header.riffChunkId = this.buffer.toString('ascii', 0, 4);
this.header.riffChunkSize = this.buffer.readUInt32LE(4);
this.header.riffFormat = this.buffer.toString('ascii', 8, 4);
this.header.fmtChunkId = this.buffer.toString('ascii', 12, 4);
this.header.fmtChunkSize = this.buffer.readUInt32LE(16);
this.header.audioFormat = this.buffer.readUInt16LE(20);
this.header.numChannels = this.buffer.readUInt16LE(22);
this.header.sampleRate = this.buffer.readUInt32LE(24);
this.header.byteRate = this.buffer.readUInt32LE(28);
this.header.blockAlign = this.buffer.readUInt16LE(32);
this.header.bitsPerSample = this.buffer.readUInt16LE(34);
this.header.dataChunkId = this.buffer.toString('ascii', 36, 4);
this.header.dataChunkSize = this.buffer.readUInt32LE(40);
this.dataOffset = 44;
// 可以在此处添加对 header 值的校验
if (this.header.riffChunkId !== "RIFF" || this.header.riffFormat !== "WAVE") {
throw new Error("Invalid WAV file format.");
}
if (![8, 16, 24, 32].includes(this.header.bitsPerSample)) {
throw new Error(`Unsupported bitsPerSample: ${this.header.bitsPerSample}`);
}
}
private decodeData(): void {
this.data = this.buffer.slice(this.dataOffset, this.dataOffset + this.header.dataChunkSize);
}
public getHeader(): WavHeader {
return this.header;
}
public getData(): Buffer {
return this.data;
}
}

814
src/common/audio-pure.ts Normal file
View File

@@ -0,0 +1,814 @@
/**
* 现代音频格式转换库 - 使用纯JavaScript实现的音频格式转换工具
* 支持格式: MP3, WAV, FLAC, OGG, OPUS, AMR, M4A和PCM等格式间的转换
* PCM格式支持8位、16位和32位采样深度
*
* 特点:
* - 纯JavaScript/TypeScript实现无需外部依赖如FFmpeg
* - 完全支持Web和Node.js环境
* - 强类型定义和现代化错误处理
* - 高性能实现,支持流式处理
*/
import { readFile, writeFile } from 'fs/promises';
import path from 'path';
import audioDecode from 'audio-decode'; // 解码 WAV MP3 OGG FLAC
import { Mp3Encoder } from '@breezystack/lamejs'; // 编码 MP3
import { WavEncoder, WavDecoder } from './audio-enhance/codec/wav'; // 导入 WavEncoder 和 WavDecoder
// import { Encoder as FlacEncoder } from 'libflacjs/lib/encoder'; // 编码 FLAC
// import * as Flac from 'libflacjs'; // 编码 FLAC
// import { Muxer } from 'mp4-muxer'; // 替换demux用于编码 AAC/M4A
/* ============================================================================
类型与接口定义
============================================================================ */
/**
* 音频处理错误类 - 提供丰富的错误上下文
*/
export class AudioError extends Error {
constructor(
message: string,
public readonly step: 'decode' | 'encode' | 'convert' | 'validate',
public readonly format?: string,
public readonly cause?: Error
) {
super(message);
this.name = 'AudioError';
// 捕获原始错误堆栈
if (cause && cause.stack) {
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
}
}
}
/** 解码后的PCM数据及相关音频信息 */
export interface PCMData {
/** PCM样本数据统一使用Float32Array表示 */
samples: Float32Array;
/** 采样率Hz) */
sampleRate: number;
/** 声道数 */
channels: number;
/** 音频元数据 (可选) */
metadata?: AudioMetadata;
}
/** 音频元数据 */
export interface AudioMetadata {
title?: string;
artist?: string;
album?: string;
year?: number;
genre?: string;
duration?: number; // 秒
[key: string]: any; // 允许其他元数据
}
/** 音频转换选项 */
export interface ConvertOptions {
/** 目标采样率 */
sampleRate?: number;
/** 目标声道数 */
channels?: number;
/** PCM位深度 (8, 16 或 32) */
bitDepth?: 8 | 16 | 32;
/** 编码比特率 (kbps) */
bitrate?: number;
/** 编码质量 (0-1) */
quality?: number;
/** 保留元数据 */
preserveMetadata?: boolean;
/** 使用Web Worker (仅浏览器环境) */
useWorker?: boolean;
}
/**
* 音频编解码器接口
*/
interface Codec {
/** 编解码器名称 */
readonly name: string;
/** 支持的文件扩展名 */
readonly extensions: string[];
/** 检查是否支持指定格式 */
supports(format: string): boolean;
/** 解码音频数据为PCM */
decode(buffer: Buffer, options?: ConvertOptions): Promise<PCMData>;
/** 编码PCM数据为目标格式 */
encode(pcmData: PCMData, options?: ConvertOptions): Promise<Buffer>;
}
/**
* PCM数据处理工具
*/
class AudioProcessor {
/**
* 将Float32Array PCM数据转换为指定位深度的Buffer
*/
static floatToPCM(samples: Float32Array, bitDepth: number): Buffer {
const bytesPerSample = bitDepth / 8;
const buffer = Buffer.alloc(samples.length * bytesPerSample);
if (bitDepth === 8) {
for (let i = 0; i < samples.length; i++) {
// 将[-1,1]映射到[0,255]
const sample = Math.max(-1, Math.min(1, samples[i]!));
buffer[i] = (sample * 0.5 + 0.5) * 255;
}
} else if (bitDepth === 16) {
for (let i = 0; i < samples.length; i++) {
// 将[-1,1]映射到[-32768,32767]
const sample = Math.max(-1, Math.min(1, samples[i]!));
const val = sample < 0 ? sample * 32768 : sample * 32767;
buffer.writeInt16LE(Math.floor(val), i * 2);
}
} else if (bitDepth === 32) {
for (let i = 0; i < samples.length; i++) {
// 将[-1,1]映射到[-2147483648,2147483647]
const sample = Math.max(-1, Math.min(1, samples[i]!));
const val = sample < 0 ? sample * 2147483648 : sample * 2147483647;
buffer.writeInt32LE(Math.floor(val), i * 4);
}
} else {
throw new AudioError(`不支持的PCM位深度: ${bitDepth}`, 'encode', 'pcm');
}
return buffer;
}
/**
* 将指定位深度的PCM Buffer转换为Float32Array
*/
static pcmToFloat(buffer: Buffer, bitDepth: number): Float32Array {
const samples = new Float32Array(buffer.length / (bitDepth / 8));
if (bitDepth === 8) {
for (let i = 0; i < samples.length; i++) {
// 将[0,255]映射回[-1,1]
samples[i] = (buffer[i]! / 255) * 2 - 1;
}
} else if (bitDepth === 16) {
for (let i = 0; i < samples.length; i++) {
const val = buffer.readInt16LE(i * 2);
// 将[-32768,32767]映射回[-1,1]
samples[i] = val < 0 ? val / 32768 : val / 32767;
}
} else if (bitDepth === 32) {
for (let i = 0; i < samples.length; i++) {
const val = buffer.readInt32LE(i * 4);
// 将[-2147483648,2147483647]映射回[-1,1]
samples[i] = val < 0 ? val / 2147483648 : val / 2147483647;
}
} else {
throw new AudioError(`不支持的PCM位深度: ${bitDepth}`, 'decode', 'pcm');
}
return samples;
}
/**
* 重采样PCM数据
*/
static resample(samples: Float32Array, fromRate: number, toRate: number, channels: number): Float32Array {
if (fromRate === toRate) return samples;
const ratio = toRate / fromRate;
const inputLength = samples.length;
const outputLength = Math.ceil(inputLength * ratio);
const result = new Float32Array(outputLength);
// 线性插值重采样
for (let i = 0; i < outputLength; i++) {
const pos = i / ratio;
const leftPos = Math.floor(pos);
const rightPos = Math.min(leftPos + 1, inputLength - 1);
const fraction = pos - leftPos;
// 对每个通道分别进行插值
for (let channel = 0; channel < channels; channel++) {
const leftIdx = leftPos * channels + channel;
const rightIdx = rightPos * channels + channel;
const leftSample = samples[leftIdx] || 0;
const rightSample = samples[rightIdx] || 0;
// 线性插值
result[i * channels + channel] = leftSample + fraction * (rightSample - leftSample);
}
}
return result;
}
/**
* 混合声道 (多声道到单声道或立体声)
*/
static mixChannels(samples: Float32Array, fromChannels: number, toChannels: number): Float32Array {
if (fromChannels === toChannels) return samples;
const frameCount = samples.length / fromChannels;
const result = new Float32Array(frameCount * toChannels);
if (fromChannels === 1 && toChannels === 2) {
// 单声道到立体声 - 复制到两个声道
for (let i = 0; i < frameCount; i++) {
const sample = samples[i]!;
result[i * 2] = sample; // 左声道
result[i * 2 + 1] = sample; // 右声道
}
} else if (fromChannels === 2 && toChannels === 1) {
// 立体声到单声道 - 取平均值
for (let i = 0; i < frameCount; i++) {
const left = samples[i * 2];
const right = samples[i * 2 + 1];
result[i] = (left! + right!) / 2;
}
} else if (fromChannels > toChannels) {
// 多声道到少声道 - 根据需要混合
for (let i = 0; i < frameCount; i++) {
for (let c = 0; c < toChannels; c++) {
// 根据toChannel位置映射到fromChannel
let sum = 0;
let count = 0;
for (let fc = c; fc < fromChannels; fc += toChannels) {
sum += samples[i * fromChannels + fc]!;
count++;
}
result[i * toChannels + c] = sum / count;
}
}
} else {
// 少声道到多声道 - 根据需要复制
for (let i = 0; i < frameCount; i++) {
for (let c = 0; c < toChannels; c++) {
// 循环复制
const fromChannel = c % fromChannels;
result[i * toChannels + c] = samples[i * fromChannels + fromChannel]!;
}
}
}
return result;
}
/**
* 处理PCM数据包括重采样和声道转换
*/
static processPCM(pcmData: PCMData, options?: ConvertOptions): PCMData {
const targetSampleRate = options?.sampleRate ?? pcmData.sampleRate;
const targetChannels = options?.channels ?? pcmData.channels;
let processedSamples = pcmData.samples;
// 如果需要重采样
console.log(`重采样: ${pcmData.sampleRate}Hz → ${targetSampleRate}Hz`);
if (pcmData.sampleRate !== targetSampleRate) {
processedSamples = this.resample(
processedSamples,
pcmData.sampleRate,
targetSampleRate,
pcmData.channels
);
}
// 如果需要改变声道数
if (pcmData.channels !== targetChannels) {
processedSamples = this.mixChannels(
processedSamples,
pcmData.channels,
targetChannels
);
}
return {
samples: processedSamples,
sampleRate: targetSampleRate,
channels: targetChannels,
metadata: options?.preserveMetadata ? pcmData.metadata : undefined
};
}
/**
* 从Buffer中提取音频元数据
*/
static extractMetadata(data: any): AudioMetadata | undefined {
if (!data) return undefined;
return {
title: data.title || data.TITLE,
artist: data.artist || data.ARTIST || data.performer,
album: data.album || data.ALBUM,
year: data.year ? parseInt(data.year) : (data.date ? parseInt(data.date) : undefined),
genre: data.genre || data.GENRE,
duration: data.duration
};
}
/**
* 将交织的PCM数据分离为各声道数据
*/
static deinterleaveChannels(samples: Float32Array | Int16Array, channels: number): Array<Float32Array | Int16Array> {
const frameCount = samples.length / channels;
const result = new Array(channels);
// 创建每个声道的数组
for (let c = 0; c < channels; c++) {
result[c] = new (samples.constructor as any)(frameCount);
}
// 分离声道数据
for (let i = 0; i < frameCount; i++) {
for (let c = 0; c < channels; c++) {
result[c][i] = samples[i * channels + c];
}
}
return result;
}
/**
* 将Float32Array转换为Int16Array
*/
static floatToInt16(samples: Float32Array): Int16Array {
const int16Samples = new Int16Array(samples.length);
for (let i = 0; i < samples.length; i++) {
const sample = Math.max(-1, Math.min(1, samples[i]!));
int16Samples[i] = Math.round(sample < 0 ? sample * 32768 : sample * 32767);
}
return int16Samples;
}
}
/* ============================================================================
编解码器实现
============================================================================ */
/**
* 通用音频解码器 - 使用audio-decode库处理多种格式
*/
class GenericDecoder {
/**
* 使用audio-decode解码多种格式
*/
static async decode(buffer: Buffer, _options?: ConvertOptions): Promise<PCMData> {
try {
// 使用audio-decode解码音频
const audioData = await audioDecode(buffer);
return {
samples: this.interleaveSamples(audioData),
sampleRate: audioData.sampleRate,
channels: audioData.numberOfChannels,
metadata: AudioProcessor.extractMetadata({})
};
} catch (error: any) {
throw new AudioError(
`音频解码错误: ${error.message}`,
'decode',
'audio',
error
);
}
}
/**
* 将多声道音频数据交织成单个Float32Array
*/
private static interleaveSamples(audioData: AudioBuffer): Float32Array {
const channels = audioData.numberOfChannels;
const length = audioData.length;
const result = new Float32Array(length * channels);
for (let c = 0; c < channels; c++) {
const channelData = audioData.getChannelData(c);
for (let i = 0; i < length; i++) {
result[i * channels + c] = channelData[i]!;
}
}
return result;
}
}
/**
* 基础编解码器类 - 提供通用实现
*/
abstract class BaseCodec implements Codec {
abstract readonly name: string;
abstract readonly extensions: string[];
supports(format: string): boolean {
return this.extensions.includes(format.toLowerCase());
}
async decode(buffer: Buffer, options?: ConvertOptions): Promise<PCMData> {
return GenericDecoder.decode(buffer, options);
}
abstract encode(pcmData: PCMData, options?: ConvertOptions): Promise<Buffer>;
}
/**
* MP3编解码器
*/
class MP3Codec extends BaseCodec {
readonly name = 'MP3 Codec';
readonly extensions = ['mp3'];
async encode(pcmData: PCMData, options?: ConvertOptions): Promise<Buffer> {
try {
const processed = AudioProcessor.processPCM(pcmData, options);
const bitrate = options?.bitrate ?? 128;
// 创建MP3编码器
const encoder = new Mp3Encoder(
processed.channels,
processed.sampleRate,
bitrate
);
// 将Float32Array转换为Int16Array (lamejs需要)
const samples = AudioProcessor.floatToPCM(processed.samples, 16);
const int16Samples = new Int16Array(samples.buffer, samples.byteOffset, samples.length / 2);
const mp3Data: Uint8Array[] = [];
const sampleBlockSize = 1152; // MP3编码的标准帧大小
// 分块处理,避免内存占用过大
for (let i = 0; i < int16Samples.length; i += sampleBlockSize) {
const chunk = int16Samples.subarray(i, i + sampleBlockSize);
const mp3buf = encoder.encodeBuffer(chunk);
if (mp3buf.length > 0) {
mp3Data.push(new Uint8Array(mp3buf));
}
}
// 完成编码,获取最后一块数据
const finalChunk = encoder.flush();
if (finalChunk.length > 0) {
mp3Data.push(new Uint8Array(finalChunk));
}
// 合并所有MP3数据块
const totalLength = mp3Data.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of mp3Data) {
result.set(arr, offset);
offset += arr.length;
}
return Buffer.from(result);
} catch (error: any) {
throw new AudioError(`MP3编码错误: ${error.message}`, 'encode', 'mp3', error);
}
}
}
/**
* WAV编解码器
*/
class WAVCodec extends BaseCodec {
readonly name = 'WAV Codec';
readonly extensions = ['wav'];
override async decode(buffer: Buffer, options?: ConvertOptions): Promise<PCMData> {
try {
const decoder = new WavDecoder(buffer);
const header = decoder.getHeader();
const data = decoder.getData();
const sampleRate = header.sampleRate;
const channels = header.numChannels;
const bitsPerSample = header.bitsPerSample;
// 将Buffer转换为Float32Array
let samples: Float32Array;
if (bitsPerSample === 8 || bitsPerSample === 16 || bitsPerSample === 32) {
samples = AudioProcessor.pcmToFloat(data, bitsPerSample);
} else {
throw new AudioError(`不支持的WAV位深: ${bitsPerSample}`, 'decode', 'wav');
}
return {
samples,
sampleRate,
channels,
metadata: undefined
};
} catch (error: any) {
// WAV解析失败尝试使用通用解码器
return GenericDecoder.decode(buffer, options);
}
}
async encode(pcmData: PCMData, options?: ConvertOptions): Promise<Buffer> {
try {
const processed = AudioProcessor.processPCM(pcmData, options);
const bitDepth = options?.bitDepth ?? 16;
const encoder = new WavEncoder(processed.sampleRate, processed.channels, bitDepth);
// 将Float32Array转换为指定位深度的Buffer
const pcmBuffer = AudioProcessor.floatToPCM(processed.samples, bitDepth);
encoder.write(pcmBuffer);
return encoder.encode();
} catch (error: any) {
throw new AudioError(`WAV编码错误: ${error.message}`, 'encode', 'wav', error);
}
}
}
/**
* OGG Vorbis编解码器
*/
class OGGCodec extends BaseCodec {
readonly name = 'OGG Vorbis Codec';
readonly extensions = ['ogg'];
async encode(pcmData: PCMData, options?: ConvertOptions): Promise<Buffer> {
try {
// 注意这里应该使用专门的OGG Vorbis编码库但为了保持库的纯JavaScript特性
// 我们可以使用一个基于Web Audio API的解决方案或找一个纯JS的OGG编码器
// 由于pure-JS的OGG编码器较为复杂这里提供一个简化的实现
// 在实际应用中应使用专门的库如ogg-vorbis-encoder-js
const processed = AudioProcessor.processPCM(pcmData, options);
// 如果后续需要OGG编码功能请添加合适的库
// 这里返回一个模拟实现
return this.createMockOggFile(processed, options);
} catch (error: any) {
throw new AudioError(`OGG编码错误: ${error.message}`, 'encode', 'ogg', error);
}
}
// 创建模拟的OGG文件仅作示例实际应用中请替换为真实OGG编码
private createMockOggFile(pcmData: PCMData, options?: ConvertOptions): Buffer {
const quality = options?.quality ?? 0.5;
// 创建基本的OGG头部
const header = Buffer.alloc(100);
header.write('OggS', 0);
header.writeUInt8(0, 4); // 版本
header.writeUInt8(pcmData.channels, 5);
header.writeUInt32LE(pcmData.sampleRate, 6);
header.writeUInt8(Math.floor(quality * 10), 10);
// 对音频数据进行简单的处理(仅作示例)
const samplesBuffer = AudioProcessor.floatToPCM(pcmData.samples, 16);
// 组合头部和数据
return Buffer.concat([header, samplesBuffer]);
}
}
/**
* PCM编解码器
*/
class PCMCodec implements Codec {
readonly name = 'PCM Codec';
readonly extensions = ['pcm'];
supports(format: string): boolean {
return this.extensions.includes(format.toLowerCase());
}
async decode(buffer: Buffer, options?: ConvertOptions): Promise<PCMData> {
try {
const bitDepth = options?.bitDepth ?? 16;
// 验证位深度是否受支持
if (![8, 16, 32].includes(bitDepth)) {
throw new AudioError(`不支持的PCM位深: ${bitDepth}`, 'decode', 'pcm');
}
// 获取采样率和声道数 (PCM文件本身不包含这些信息使用默认值或用户提供的值)
const sampleRate = options?.sampleRate ?? 44100;
const channels = options?.channels ?? 2;
// 将PCM数据转换为Float32Array
const samples = AudioProcessor.pcmToFloat(buffer, bitDepth);
return {
samples,
sampleRate,
channels
};
} catch (error: any) {
if (error instanceof AudioError) throw error;
throw new AudioError(`PCM解码错误: ${error.message}`, 'decode', 'pcm', error);
}
}
async encode(pcmData: PCMData, options?: ConvertOptions): Promise<Buffer> {
try {
const processed = AudioProcessor.processPCM(pcmData, options);
const bitDepth = options?.bitDepth ?? 16;
// 验证位深度是否受支持
if (![8, 16, 32].includes(bitDepth)) {
throw new AudioError(`不支持的PCM位深: ${bitDepth}`, 'encode', 'pcm');
}
// 将Float32Array转换为指定位深度的PCM数据
return AudioProcessor.floatToPCM(processed.samples, bitDepth);
} catch (error: any) {
if (error instanceof AudioError) throw error;
throw new AudioError(`PCM编码错误: ${error.message}`, 'encode', 'pcm', error);
}
}
}
/**
* 编解码器注册表
*/
class CodecRegistry {
private static codecs: Map<string, Codec> = new Map();
private static initialized = false;
/**
* 初始化编解码器注册表
*/
static init(): void {
if (this.initialized) return;
// 注册所有支持的编解码器
this.register(new MP3Codec());
this.register(new WAVCodec());
this.register(new OGGCodec());
this.register(new PCMCodec());
this.initialized = true;
}
/**
* 注册编解码器
*/
static register(codec: Codec): void {
codec.extensions.forEach(ext => this.codecs.set(ext.toLowerCase(), codec));
}
/**
* 获取指定格式的编解码器
*/
static getCodec(format: string): Codec {
this.init();
const codec = this.codecs.get(format.toLowerCase());
if (!codec) {
throw new AudioError(`不支持的音频格式: ${format}`, 'validate', format);
}
return codec;
}
/**
* 获取所有支持的格式
*/
static getSupportedFormats(): string[] {
this.init();
return [...new Set(this.codecs.keys())];
}
}
/**
* 转换音频文件格式
*
* @param inputPath 输入文件路径
* @param outputPath 输出文件路径
* @param targetFormat 目标格式
* @param options 转换选项
*/
export async function convertAudio(
inputPath: string,
outputPath: string,
targetFormat: string,
options?: ConvertOptions
): Promise<void> {
// 初始化编解码器注册表
CodecRegistry.init();
// 提取文件扩展名
const inputExt = path.extname(inputPath).slice(1).toLowerCase();
// 验证格式支持
if (!CodecRegistry.getSupportedFormats().includes(inputExt)) {
throw new AudioError(`不支持的输入格式: ${inputExt}`, 'validate', inputExt);
}
if (!CodecRegistry.getSupportedFormats().includes(targetFormat)) {
throw new AudioError(`不支持的目标格式: ${targetFormat}`, 'validate', targetFormat);
}
try {
// 读取输入文件
const inputBuffer = await readFile(inputPath);
// 解码为PCM
const inputCodec = CodecRegistry.getCodec(inputExt);
const decoded = await inputCodec.decode(inputBuffer, options);
// 编码为目标格式
const outputCodec = CodecRegistry.getCodec(targetFormat);
const outputBuffer = await outputCodec.encode(decoded, options);
// 写入输出文件
await writeFile(outputPath, outputBuffer);
console.log(
`转换完成: ${inputExt}${targetFormat}, ` +
`保存到 ${outputPath} ` +
`(${(outputBuffer.length / 1024).toFixed(2)} KB)`
);
} catch (error: any) {
// 统一错误处理
if (error instanceof AudioError) {
throw error;
}
throw new AudioError(
`音频转换失败: ${error.message}`,
'convert',
`${inputExt}->${targetFormat}`,
error
);
}
}
/**
* 从二进制数据转换音频格式
*
* @param inputBuffer 输入音频数据
* @param inputFormat 输入格式
* @param outputFormat 输出格式
* @param options 转换选项
* @returns 转换后的音频数据
*/
export async function convertAudioBuffer(
inputBuffer: Buffer,
inputFormat: string,
outputFormat: string,
options?: ConvertOptions
): Promise<Buffer> {
// 初始化编解码器注册表
CodecRegistry.init();
// 验证格式支持
if (!CodecRegistry.getSupportedFormats().includes(inputFormat)) {
throw new AudioError(`不支持的输入格式: ${inputFormat}`, 'validate', inputFormat);
}
if (!CodecRegistry.getSupportedFormats().includes(outputFormat)) {
throw new AudioError(`不支持的目标格式: ${outputFormat}`, 'validate', outputFormat);
}
try {
// 解码为PCM
const inputCodec = CodecRegistry.getCodec(inputFormat);
const decoded = await inputCodec.decode(inputBuffer, options);
// 编码为目标格式
const outputCodec = CodecRegistry.getCodec(outputFormat);
return await outputCodec.encode(decoded, options);
} catch (error: any) {
// 统一错误处理
if (error instanceof AudioError) {
throw error;
}
throw new AudioError(
`音频转换失败: ${error.message}`,
'convert',
`${inputFormat}->${outputFormat}`,
error
);
}
}
/**
* 创建音频转换函数
*
* @param inputFormat 输入格式
* @param outputFormat 输出格式
* @param options 转换选项
*/
export async function createAudioConverter(
inputFormat: string,
outputFormat: string,
options?: ConvertOptions
): Promise<(inputBuffer: Buffer) => Promise<Buffer>> {
// 初始化编解码器
CodecRegistry.init();
// 验证格式支持
const inputCodec = CodecRegistry.getCodec(inputFormat);
const outputCodec = CodecRegistry.getCodec(outputFormat);
// 返回转换函数
return async (inputBuffer: Buffer): Promise<Buffer> => {
const decoded = await inputCodec.decode(inputBuffer, options);
return outputCodec.encode(decoded, options);
};
}

View File

@@ -1,360 +0,0 @@
// 更正导入语句
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as os from 'os';
import * as compressing from 'compressing'; // 修正导入方式
import { pipeline } from 'stream/promises';
import { fileURLToPath } from 'url';
import { LogWrapper } from './log';
const downloadOri = "https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip"
const urls = [
"https://github.moeyy.xyz/" + downloadOri,
"https://ghp.ci/" + downloadOri,
"https://gh.api.99988866.xyz/" + downloadOri,
"https://gh.api.99988866.xyz/" + downloadOri,
downloadOri
];
/**
* 测试URL是否可用
* @param url 待测试的URL
* @returns 如果URL可访问返回true否则返回false
*/
async function testUrl(url: string): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const req = https.get(url, { timeout: 5000 }, (res) => {
// 检查状态码是否表示成功
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
// 终止请求并返回true
req.destroy();
resolve(true);
} else {
req.destroy();
resolve(false);
}
});
req.on('error', () => {
resolve(false);
});
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
/**
* 查找第一个可用的URL
* @returns 返回第一个可用的URL如果都不可用则返回null
*/
async function findAvailableUrl(): Promise<string | null> {
for (const url of urls) {
try {
const available = await testUrl(url);
if (available) {
return url;
}
} catch (error) {
// 忽略错误
}
}
return null;
}
/**
* 下载文件
* @param url 下载URL
* @param destPath 目标保存路径
* @returns 成功返回true失败返回false
*/
async function downloadFile(url: string, destPath: string, progressCallback?: (percent: number) => void): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const file = fs.createWriteStream(destPath);
const req = https.get(url, (res) => {
const statusCode = res.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
// 获取文件总大小
const totalSize = parseInt(res.headers['content-length'] || '0', 10);
let downloadedSize = 0;
let lastReportedPercent = -1; // 上次报告的百分比
let lastReportTime = 0; // 上次报告的时间戳
// 如果有内容长度和进度回调,则添加数据监听
if (totalSize > 0 && progressCallback) {
// 初始报告 0%
progressCallback(0);
lastReportTime = Date.now();
res.on('data', (chunk) => {
downloadedSize += chunk.length;
const currentPercent = Math.floor((downloadedSize / totalSize) * 100);
const now = Date.now();
// 只在以下条件触发回调:
// 1. 百分比变化至少为1%
// 2. 距离上次报告至少500毫秒
// 3. 确保报告100%完成
if ((currentPercent !== lastReportedPercent &&
(currentPercent - lastReportedPercent >= 1 || currentPercent === 100)) &&
(now - lastReportTime >= 1000 || currentPercent === 100)) {
progressCallback(currentPercent);
lastReportedPercent = currentPercent;
lastReportTime = now;
}
});
}
pipeline(res, file)
.then(() => {
// 确保最后报告100%
if (progressCallback && lastReportedPercent !== 100) {
progressCallback(100);
}
resolve(true);
})
.catch(() => resolve(false));
} else {
file.close();
fs.unlink(destPath, () => { });
resolve(false);
}
});
req.on('error', () => {
file.close();
fs.unlink(destPath, () => { });
resolve(false);
});
});
}
/**
* 解压缩zip文件中的特定内容
* 只解压bin目录中的文件到目标目录
* @param zipPath 压缩文件路径
* @param extractDir 解压目标路径
*/
async function extractBinDirectory(zipPath: string, extractDir: string): Promise<void> {
try {
// 确保目标目录存在
if (!fs.existsSync(extractDir)) {
fs.mkdirSync(extractDir, { recursive: true });
}
// 解压文件
const zipStream = new compressing.zip.UncompressStream({ source: zipPath });
return new Promise<void>((resolve, reject) => {
// 监听条目事件
zipStream.on('entry', (header, stream, next) => {
// 获取文件路径
const filePath = header.name;
// 匹配内层bin目录中的文件
// 例如ffmpeg-n7.1.1-6-g48c0f071d4-win64-lgpl-7.1/bin/ffmpeg.exe
if (filePath.includes('/bin/') && filePath.endsWith('.exe')) {
// 提取文件名
const fileName = path.basename(filePath);
const targetPath = path.join(extractDir, fileName);
// 创建写入流
const writeStream = fs.createWriteStream(targetPath);
// 将流管道连接到文件
stream.pipe(writeStream);
// 监听写入完成事件
writeStream.on('finish', () => {
next();
});
writeStream.on('error', () => {
next();
});
} else {
// 跳过不需要的文件
stream.resume();
next();
}
});
zipStream.on('error', (err) => {
reject(err);
});
zipStream.on('finish', () => {
resolve();
});
});
} catch (err) {
throw err;
}
}
/**
* 下载并设置FFmpeg
* @param destDir 目标安装目录默认为用户临时目录下的ffmpeg文件夹
* @param tempDir 临时文件目录,默认为系统临时目录
* @returns 返回ffmpeg可执行文件的路径如果失败则返回null
*/
export async function downloadFFmpeg(
destDir?: string,
tempDir?: string,
progressCallback?: (percent: number, stage: string) => void
): Promise<string | null> {
// 仅限Windows
if (os.platform() !== 'win32') {
return null;
}
const destinationDir = destDir || path.join(os.tmpdir(), 'ffmpeg');
const tempDirectory = tempDir || os.tmpdir();
const zipFilePath = path.join(tempDirectory, 'ffmpeg.zip'); // 临时下载到指定临时目录
const ffmpegExePath = path.join(destinationDir, 'ffmpeg.exe');
// 确保目录存在
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true });
}
// 确保临时目录存在
if (!fs.existsSync(tempDirectory)) {
fs.mkdirSync(tempDirectory, { recursive: true });
}
// 如果ffmpeg已经存在直接返回路径
if (fs.existsSync(ffmpegExePath)) {
if (progressCallback) progressCallback(100, '已找到FFmpeg');
return ffmpegExePath;
}
// 查找可用URL
if (progressCallback) progressCallback(0, '查找可用下载源');
const availableUrl = await findAvailableUrl();
if (!availableUrl) {
return null;
}
// 下载文件
if (progressCallback) progressCallback(5, '开始下载FFmpeg');
const downloaded = await downloadFile(
availableUrl,
zipFilePath,
(percent) => {
// 下载占总进度的70%
if (progressCallback) progressCallback(5 + Math.floor(percent * 0.7), '下载FFmpeg');
}
);
if (!downloaded) {
return null;
}
try {
// 直接解压bin目录文件到目标目录
if (progressCallback) progressCallback(75, '解压FFmpeg');
await extractBinDirectory(zipFilePath, destinationDir);
// 清理下载文件
if (progressCallback) progressCallback(95, '清理临时文件');
try {
fs.unlinkSync(zipFilePath);
} catch (err) {
// 忽略清理临时文件失败的错误
}
// 检查ffmpeg.exe是否成功解压
if (fs.existsSync(ffmpegExePath)) {
if (progressCallback) progressCallback(100, 'FFmpeg安装完成');
return ffmpegExePath;
} else {
return null;
}
} catch (err) {
return null;
}
}
/**
* 检查系统PATH环境变量中是否存在指定可执行文件
* @param executable 可执行文件名
* @returns 如果找到返回完整路径否则返回null
*/
function findExecutableInPath(executable: string): string | null {
// 仅适用于Windows系统
if (os.platform() !== 'win32') return null;
// 获取PATH环境变量
const pathEnv = process.env['PATH'] || '';
const pathDirs = pathEnv.split(';');
// 检查每个目录
for (const dir of pathDirs) {
if (!dir) continue;
try {
const filePath = path.join(dir, executable);
if (fs.existsSync(filePath)) {
return filePath;
}
} catch (error) {
continue;
}
}
return null;
}
export async function downloadFFmpegIfNotExists(log: LogWrapper) {
// 仅限Windows
if (os.platform() !== 'win32') {
return {
path: null,
reset: false
};
}
const ffmpegInPath = findExecutableInPath('ffmpeg.exe');
const ffprobeInPath = findExecutableInPath('ffprobe.exe');
if (ffmpegInPath && ffprobeInPath) {
const ffmpegDir = path.dirname(ffmpegInPath);
return {
path: ffmpegDir,
reset: true
};
}
// 如果环境变量中没有,检查项目目录中是否存在
const currentPath = path.dirname(fileURLToPath(import.meta.url));
const ffmpeg_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffmpeg.exe'));
const ffprobe_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffprobe.exe'));
if (!ffmpeg_exist || !ffprobe_exist) {
let url = await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
});
if (!url) {
log.log('[FFmpeg] [Error] 下载FFmpeg失败');
return {
path: null,
reset: false
};
}
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true
}
}
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true
}
}

View File

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

308
src/common/ffmpeg-worker.ts Normal file
View File

@@ -0,0 +1,308 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FFmpeg } from '@ffmpeg.wasm/main';
import { randomUUID } from 'crypto';
import { readFileSync, statSync, writeFileSync } from 'fs';
import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
import { parentPort } from 'worker_threads';
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
let ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
}
export function sendLog(_log: string) {
//parentPort?.postMessage({ log });
}
class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const videoFileName = `${randomUUID()}.mp4`;
const outputFileName = `${randomUUID()}.jpg`;
try {
ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath));
const code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName);
if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
}
const thumbnail = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(thumbnailPath, thumbnail);
} catch (error) {
console.error('Error extracting thumbnail:', error);
throw error;
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(videoFileName);
} catch (unlinkError) {
console.error('Error unlinking video file:', unlinkError);
}
}
}
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const inputFileName = `${randomUUID()}.pcm`;
const outputFileName = `${randomUUID()}.${format}`;
try {
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(inputFile));
const params = format === 'amr'
? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, '-ar', '8000', '-b:a', '12.2k', outputFileName]
: ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, outputFileName];
const code = await ffmpegInstance.run(...params);
if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
}
const outputData = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(outputFile, outputData);
} catch (error) {
console.error('Error converting file:', error);
throw error;
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (unlinkError) {
console.error('Error unlinking input file:', unlinkError);
}
}
}
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const inputFileName = `${randomUUID()}.input`;
const outputFileName = `${randomUUID()}.pcm`;
try {
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath));
const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName];
const code = await ffmpegInstance.run(...params);
if (code !== 0) {
throw new Error('FFmpeg process exited with code ' + code);
}
const outputData = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(pcmPath, outputData);
return Buffer.from(outputData);
} catch (error: any) {
throw new Error('FFmpeg处理转换出错: ' + error.message);
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
}
}
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const startTime = Date.now();
sendLog(`开始获取视频信息: ${videoPath}`);
// 创建一个超时包装函数
const withTimeout = <T>(promise: Promise<T>, timeoutMs: number, taskName: string): Promise<T> => {
return Promise.race([
promise,
new Promise<T>((_, reject) => {
setTimeout(() => reject(new Error(`任务超时: ${taskName} (${timeoutMs}ms)`)), timeoutMs);
})
]);
};
// 并行执行多个任务
const [fileInfo, durationInfo] = await Promise.all([
// 任务1: 获取文件信息和提取缩略图
(async () => {
sendLog(`开始任务1: 获取文件信息和提取缩略图`);
// 获取文件信息 (并行)
const fileInfoStartTime = Date.now();
const [fileType, fileSize] = await Promise.all([
withTimeout(fileTypeFromFile(videoPath), 10000, '获取文件类型')
.then(result => {
sendLog(`获取文件类型完成,耗时: ${Date.now() - fileInfoStartTime}ms`);
return result;
}),
(async () => {
const result = statSync(videoPath).size;
sendLog(`获取文件大小完成,耗时: ${Date.now() - fileInfoStartTime}ms`);
return result;
})()
]);
// 直接实现缩略图提取 (不调用extractThumbnail方法)
const thumbStartTime = Date.now();
sendLog(`开始提取缩略图`);
const ffmpegInstance = await withTimeout(
FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }),
15000,
'创建FFmpeg实例(缩略图)'
);
const videoFileName = `${randomUUID()}.mp4`;
const outputFileName = `${randomUUID()}.jpg`;
try {
// 写入视频文件到FFmpeg
const writeFileStartTime = Date.now();
ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath));
sendLog(`写入视频文件到FFmpeg完成耗时: ${Date.now() - writeFileStartTime}ms`);
// 提取缩略图
const extractStartTime = Date.now();
const code = await withTimeout(
ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName),
30000,
'提取缩略图'
);
sendLog(`FFmpeg提取缩略图命令执行完成耗时: ${Date.now() - extractStartTime}ms`);
if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
}
// 读取并保存缩略图
const saveStartTime = Date.now();
const thumbnail = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(thumbnailPath, thumbnail);
sendLog(`读取并保存缩略图完成,耗时: ${Date.now() - saveStartTime}ms`);
// 获取缩略图尺寸
const imageSizeStartTime = Date.now();
const image = imageSize(thumbnailPath);
sendLog(`获取缩略图尺寸完成,耗时: ${Date.now() - imageSizeStartTime}ms`);
sendLog(`提取缩略图完成,总耗时: ${Date.now() - thumbStartTime}ms`);
return {
format: fileType?.ext ?? 'mp4',
size: fileSize,
width: image.width ?? 100,
height: image.height ?? 100
};
} finally {
// 清理资源
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (error) {
sendLog(`清理输出文件失败: ${(error as Error).message}`);
}
try {
ffmpegInstance.fs.unlink(videoFileName);
} catch (error) {
sendLog(`清理视频文件失败: ${(error as Error).message}`);
}
}
})(),
// 任务2: 获取视频时长
(async () => {
const task2StartTime = Date.now();
sendLog(`开始任务2: 获取视频时长`);
// 创建FFmpeg实例
const ffmpegCreateStartTime = Date.now();
const ffmpegInstance = await withTimeout(
FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }),
15000,
'创建FFmpeg实例(时长)'
);
sendLog(`创建FFmpeg实例完成耗时: ${Date.now() - ffmpegCreateStartTime}ms`);
const inputFileName = `${randomUUID()}.mp4`;
try {
// 写入文件
const writeStartTime = Date.now();
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath));
sendLog(`写入文件到FFmpeg完成耗时: ${Date.now() - writeStartTime}ms`);
ffmpegInstance.setLogging(true);
let duration = 60; // 默认值
ffmpegInstance.setLogger((_level, ...msg) => {
const message = msg.join(' ');
const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
if (durationMatch) {
const hours = parseInt(durationMatch[1] ?? '0', 10);
const minutes = parseInt(durationMatch[2] ?? '0', 10);
const seconds = parseFloat(durationMatch[3] ?? '0');
duration = hours * 3600 + minutes * 60 + seconds;
}
});
// 执行FFmpeg
const runStartTime = Date.now();
await withTimeout(
ffmpegInstance.run('-i', inputFileName),
20000,
'获取视频时长'
);
sendLog(`执行FFmpeg命令完成耗时: ${Date.now() - runStartTime}ms`);
sendLog(`任务2(获取视频时长)完成,总耗时: ${Date.now() - task2StartTime}ms`);
return { time: duration };
} finally {
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (error) {
sendLog(`清理输入文件失败: ${(error as Error).message}`);
}
}
})()
]);
// 合并结果并返回
const totalDuration = Date.now() - startTime;
sendLog(`获取视频信息完成,总耗时: ${totalDuration}ms`);
return {
width: fileInfo.width,
height: fileInfo.height,
time: durationInfo.time,
format: fileInfo.format,
size: fileInfo.size,
filePath: videoPath
};
}
}
type FFmpegMethod = 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
interface FFmpegTask {
method: FFmpegMethod;
args: any[];
}
export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
switch (method) {
case 'extractThumbnail':
return await FFmpegService.extractThumbnail(...args as [string, string]);
case 'convertFile':
return await FFmpegService.convertFile(...args as [string, string, string]);
case 'convert':
return await FFmpegService.convert(...args as [string, string]);
case 'getVideoInfo':
return await FFmpegService.getVideoInfo(...args as [string, string]);
default:
throw new Error(`Unknown method: ${method}`);
}
}
recvTask<FFmpegTask>(async ({ method, args }: FFmpegTask) => {
return await handleFFmpegTask({ method, args });
});

View File

@@ -1,195 +1,36 @@
import { readFileSync, statSync, existsSync, mkdirSync } from 'fs';
import path, { dirname } from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type';
import { fileURLToPath } from 'node:url';
import { platform } from 'node:os';
import { LogWrapper } from './log';
import { imageSizeFallBack } from '@/image-size';
const currentPath = dirname(fileURLToPath(import.meta.url));
const execFileAsync = promisify(execFile);
const getFFmpegPath = (tool: string): string => {
if (process.platform === 'win32') {
const exeName = `${tool}.exe`;
const isLocalExeExists = existsSync(path.join(currentPath, 'ffmpeg', exeName));
return isLocalExeExists ? path.join(currentPath, 'ffmpeg', exeName) : exeName;
}
return tool;
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VideoInfo } from './video';
import path from 'path';
import { fileURLToPath } from 'url';
import { runTask } from './worker';
type EncodeArgs = {
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
args: any[];
};
export let FFMPEG_CMD = getFFmpegPath('ffmpeg');
export let FFPROBE_CMD = getFFmpegPath('ffprobe');
type EncodeResult = any;
function getWorkerPath() {
return path.join(path.dirname(fileURLToPath(import.meta.url)), './ffmpeg-worker.mjs');
}
export class FFmpegService {
// 确保目标目录存在
public static setFfmpegPath(ffmpegPath: string,logger:LogWrapper): void {
if (platform() === 'win32') {
FFMPEG_CMD = path.join(ffmpegPath, 'ffmpeg.exe');
FFPROBE_CMD = path.join(ffmpegPath, 'ffprobe.exe');
logger.log('[Check] ffmpeg:', FFMPEG_CMD);
logger.log('[Check] ffprobe:', FFPROBE_CMD);
}
}
private static ensureDirExists(filePath: string): void {
const dir = dirname(filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
try {
this.ensureDirExists(thumbnailPath);
const { stderr } = await execFileAsync(FFMPEG_CMD, [
'-i', videoPath,
'-ss', '00:00:01.000',
'-vframes', '1',
'-y', // 覆盖输出文件
thumbnailPath
]);
if (!existsSync(thumbnailPath)) {
throw new Error(`提取缩略图失败,输出文件不存在: ${stderr}`);
}
} catch (error) {
console.error('Error extracting thumbnail:', error);
throw new Error(`提取缩略图失败: ${(error as Error).message}`);
}
await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
}
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
try {
this.ensureDirExists(outputFile);
const params = format === 'amr'
? [
'-f', 's16le',
'-ar', '24000',
'-ac', '1',
'-i', inputFile,
'-ar', '8000',
'-b:a', '12.2k',
'-y',
outputFile
]
: [
'-f', 's16le',
'-ar', '24000',
'-ac', '1',
'-i', inputFile,
'-y',
outputFile
];
await execFileAsync(FFMPEG_CMD, params);
if (!existsSync(outputFile)) {
throw new Error('转换失败,输出文件不存在');
}
} catch (error) {
console.error('Error converting file:', error);
throw new Error(`文件转换失败: ${(error as Error).message}`);
}
await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convertFile', args: [inputFile, outputFile, format] });
}
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
try {
this.ensureDirExists(pcmPath);
await execFileAsync(FFMPEG_CMD, [
'-y',
'-i', filePath,
'-ar', '24000',
'-ac', '1',
'-f', 's16le',
pcmPath
]);
if (!existsSync(pcmPath)) {
throw new Error('转换PCM失败输出文件不存在');
}
return readFileSync(pcmPath);
} catch (error: any) {
throw new Error(`FFmpeg处理转换出错: ${error.message}`);
}
const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convert', args: [filePath, pcmPath] });
return result;
}
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
try {
// 并行执行获取文件信息和提取缩略图
const [fileInfo, duration] = await Promise.all([
this.getFileInfo(videoPath, thumbnailPath),
this.getVideoDuration(videoPath)
]);
const result: VideoInfo = {
width: fileInfo.width,
height: fileInfo.height,
time: duration,
format: fileInfo.format,
size: fileInfo.size,
filePath: videoPath
};
return result;
} catch (error) {
throw error;
}
const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
return result;
}
private static async getFileInfo(videoPath: string, thumbnailPath: string): Promise<{
format: string,
size: number,
width: number,
height: number
}> {
// 获取文件大小和类型
const [fileType, fileSize] = await Promise.all([
fileTypeFromFile(videoPath).catch(() => {
return null;
}),
Promise.resolve(statSync(videoPath).size)
]);
try {
await this.extractThumbnail(videoPath, thumbnailPath);
// 获取图片尺寸
const dimensions = await imageSizeFallBack(thumbnailPath);
return {
format: fileType?.ext ?? 'mp4',
size: fileSize,
width: dimensions.width ?? 100,
height: dimensions.height ?? 100
};
} catch (error) {
return {
format: fileType?.ext ?? 'mp4',
size: fileSize,
width: 100,
height: 100
};
}
}
private static async getVideoDuration(videoPath: string): Promise<number> {
try {
// 使用FFprobe获取时长
const { stdout } = await execFileAsync(FFPROBE_CMD, [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
videoPath
]);
const duration = parseFloat(stdout.trim());
return isNaN(duration) ? 60 : duration;
} catch (error) {
return 60; // 默认时长
}
}
}
}

View File

@@ -115,7 +115,7 @@ async function tryDownload(options: string | HttpDownloadOptions, useReferer: bo
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
const fetchRes = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause;
}
@@ -145,8 +145,8 @@ export enum FileUriType {
export async function checkUriType(Uri: string) {
const LocalFileRet = await solveProblem((uri: string) => {
if (fs.existsSync(path.normalize(uri))) {
return { Uri: path.normalize(uri), Type: FileUriType.Local };
if (fs.existsSync(uri)) {
return { Uri: uri, Type: FileUriType.Local };
}
return undefined;
}, Uri);

View File

@@ -13,15 +13,11 @@ export class NapCatPathWrapper {
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath;
let writePath: string;
if (process.env['NAPCAT_WORKDIR']) {
writePath = process.env['NAPCAT_WORKDIR'];
} else if (os.platform() === 'darwin') {
if (os.platform() === 'darwin') {
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else {
writePath = this.binaryPath;
}
this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config');
this.cachePath = path.join(writePath, 'cache');

View File

@@ -20,23 +20,3 @@ export function proxyHandlerOf(logger: LogWrapper) {
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
return new Proxy<T>(listener, proxyHandlerOf(logger));
}
export function proxyHandlerOfWithoutLogger() {
return {
get(target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (..._args: unknown[]) => {
console.log(`${target.constructor.name} has no method ${prop}`);
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
};
}
export function proxiedListenerOfWithoutLogger<T extends object>(listener: T) {
return new Proxy<T>(listener, proxyHandlerOfWithoutLogger());
}

View File

@@ -1 +1 @@
export const napCatVersion = '4.7.78';
export const napCatVersion = '4.7.13';

View File

@@ -17,6 +17,8 @@ import fs from 'fs';
import fsPromises from 'fs/promises';
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { RkeyManager } from '@/core/helper/rkey';
import { calculateFileMD5 } from '@/common/file';
import pathLib from 'node:path';
@@ -26,9 +28,6 @@ import { SendMessageContext } from '@/onebot/api';
import { getFileTypeForSendType } from '../helper/msg';
import { FFmpegService } from '@/common/ffmpeg';
import { rkeyDataType } from '../types/file';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { FileId } from '../packet/transformer/proto/misc/fileid';
import { imageSizeFallBack } from '@/image-size';
export class NTQQFileApi {
context: InstanceContext;
@@ -42,8 +41,8 @@ export class NTQQFileApi {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager([
'https://secret-service.bietiaop.com/rkeys',
'http://ss.xingzhige.com/music_card/rkey',
'https://ss.xingzhige.com/music_card/rkey', // 国内
'https://secret-service.bietiaop.com/rkeys',//国内
],
this.context.logger
);
@@ -64,76 +63,6 @@ export class NTQQFileApi {
}
}
async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined) {
if (this.core.apis.PacketApi.available) {
try {
if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID);
} else if (file10MMd5 && fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(peer, fileUUID, file10MMd5);
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
}
}
throw new Error('fileUUID or file10MMd5 is undefined');
}
async getPttUrl(peer: string, fileUUID?: string) {
if (this.core.apis.PacketApi.available && fileUUID) {
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
try {
if (appid && appid === 1403) {
return this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
} else if (fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetPttUrl(peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
}
}
throw new Error('packet cant get ptt url');
}
async getVideoUrlPacket(peer: string, fileUUID?: string) {
if (this.core.apis.PacketApi.available && fileUUID) {
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
try {
if (appid && appid === 1415) {
return this.core.apis.PacketApi.pkt.operation.GetGroupVideoUrl(+peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
} else if (fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetVideoUrl(peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
}
}
throw new Error('packet cant get video url');
}
async copyFile(filePath: string, destPath: string) {
await this.core.util.copyFile(filePath, destPath);
@@ -208,7 +137,7 @@ export class NTQQFileApi {
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
const imageSize = await imageSizeFallBack(picPath);
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
context.deleteAfterSentFiles.push(path);
return {
elementType: ElementType.PIC,
@@ -396,7 +325,6 @@ export class NTQQFileApi {
}
});
});
return res.flat();
}
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
@@ -417,7 +345,6 @@ export class NTQQFileApi {
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
[{
fileModelId: '0',
downSourceType: 0,
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
@@ -436,6 +363,19 @@ export class NTQQFileApi {
return completeRetData.filePath;
}
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
return new Promise((resolve, reject) => {
imageSize(filePath, (err: Error | null, dimensions) => {
if (err) {
reject(new Error(err.message));
} else if (!dimensions) {
reject(new Error('获取图片尺寸失败'));
} else {
resolve(dimensions);
}
});
});
}
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
const randomResultId = 100000 + Math.floor(Math.random() * 10000);

View File

@@ -86,31 +86,4 @@ export class NTQQFriendApi {
accept,
});
}
async handleDoubtFriendRequest(friendUid: string, str1: string = '', str2: string = '') {
this.context.session.getBuddyService().approvalDoubtBuddyReq(friendUid, str1, str2);
}
async getDoubtFriendRequest(count: number) {
let date = Date.now().toString();
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelBuddyService/getDoubtBuddyReq',
'NodeIKernelBuddyListener/onDoubtBuddyReqChange',
[date, count, ''],
() => true,
(data) => data.reqId === date
);
let requests = Promise.all(ret.doubtList.map(async (item) => {
return {
flag: item.uid, //注意强制String 非isNumeric 不遵守则不符合设计
uin: await this.core.apis.UserApi.getUinByUidV2(item.uid) ?? 0,// 信息字段
nick: item.nick, // 信息字段 这个不是nickname 可能是来源的群内的昵称
source: item.source, // 信息字段
reason: item.reason, // 信息字段
msg: item.msg, // 信息字段
group_code: item.groupCode, // 信息字段
time: item.reqTime, // 信息字段
type: 'doubt' //保留字段
};
}))
return requests;
}
}

View File

@@ -10,14 +10,11 @@ import {
GroupNotify,
GroupInfoSource,
ShutUpGroupMember,
Peer,
ChatType,
} from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique';
import { NTEventWrapper } from '@/common/event';
import { CancelableTask, TaskExecutor } from '@/common/cancel-task';
import { createGroupDetailInfoV2Param, createGroupExtFilter, createGroupExtInfo } from '../data';
export class NTQQGroupApi {
context: InstanceContext;
@@ -50,22 +47,6 @@ export class NTQQGroupApi {
this.initCache().then().catch(e => this.context.logger.logError(e));
}
async createGrayTip(groupCode: string, tip: string) {
return this.context.session.getMsgService().addLocalJsonGrayTipMsg(
{
chatType: ChatType.KCHATTYPEGROUP,
peerUid: groupCode,
} as Peer,
{
busiId: 2201,
jsonStr: JSON.stringify({ "align": "center", "items": [{ "txt": tip, "type": "nor" }] }),
recentAbstract: tip,
isServer: false
},
true,
true
)
}
async initCache() {
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
@@ -114,58 +95,6 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().setHeader(groupCode, filePath);
}
// 0 0 无需管理员审核
// 0 2 需要管理员审核
// 1 2 禁止Bot入群( 最好只传一个1 )
async setGroupRobotAddOption(groupCode: string, robotMemberSwitch?: number, robotMemberExamine?: number) {
let extInfo = createGroupExtInfo(groupCode);
let groupExtFilter = createGroupExtFilter();
if (robotMemberSwitch !== undefined) {
extInfo.extInfo.inviteRobotMemberSwitch = robotMemberSwitch;
groupExtFilter.inviteRobotMemberSwitch = 1;
}
if (robotMemberExamine !== undefined) {
extInfo.extInfo.inviteRobotMemberExamine = robotMemberExamine;
groupExtFilter.inviteRobotMemberExamine = 1;
}
return this.context.session.getGroupService().modifyGroupExtInfoV2(extInfo, groupExtFilter);
}
async setGroupAddOption(groupCode: string, option: {
addOption: number;
groupQuestion?: string;
groupAnswer?: string;
}) {
let param = createGroupDetailInfoV2Param(groupCode);
// 设置要修改的目标
param.filter.addOption = 1;
if (option.addOption == 4 || option.addOption == 5) {
// 4 问题进入答案 5 问题管理员批准
param.filter.groupQuestion = 1;
param.filter.groupAnswer = option.addOption == 4 ? 1 : 0;
param.modifyInfo.groupQuestion = option.groupQuestion || '';
param.modifyInfo.groupAnswer = option.addOption == 4 ? option.groupAnswer || '' : '';
}
param.modifyInfo.addOption = option.addOption;
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
}
async setGroupSearch(groupCode: string, option: {
noCodeFingerOpenFlag?: number;
noFingerOpenFlag?: number;
}) {
let param = createGroupDetailInfoV2Param(groupCode);
if (option.noCodeFingerOpenFlag) {
param.filter.noCodeFingerOpenFlag = 1;
param.modifyInfo.noCodeFingerOpenFlag = option.noCodeFingerOpenFlag;
}
if (option.noFingerOpenFlag) {
param.filter.noFingerOpenFlag = 1;
param.modifyInfo.noFingerOpenFlag = option.noFingerOpenFlag;
}
return this.context.session.getGroupService().modifyGroupDetailInfoV2(param, 0);
}
async getGroups(forced: boolean = false) {
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupList',
@@ -289,10 +218,6 @@ export class NTQQGroupApi {
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
}
async transGroupFile(groupCode: string, fileId: string) {
return this.context.session.getRichMediaService().transGroupFile(groupCode, fileId);
}
async addGroupEssence(groupCode: string, msgId: string) {
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2,

View File

@@ -12,7 +12,7 @@ export class NTQQMsgApi {
this.context = context;
this.core = core;
}
async clickInlineKeyboardButton(...params: Parameters<NodeIKernelMsgService['clickInlineKeyboardButton']>) {
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
}
@@ -71,7 +71,6 @@ export class NTQQMsgApi {
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
//searchFields: 3,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
@@ -85,7 +84,6 @@ export class NTQQMsgApi {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
//searchFields: 3,
filterSendersUid: SendersUid,
filterMsgToTime: MsgTime,
filterMsgFromTime: MsgTime,
@@ -102,7 +100,6 @@ export class NTQQMsgApi {
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
//searchFields: 3,
isIncludeCurrent: true,
pageLimit: 1,
});
@@ -113,7 +110,6 @@ export class NTQQMsgApi {
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
//searchFields: 3,
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
@@ -132,7 +128,6 @@ export class NTQQMsgApi {
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
filterMsgType: [],
filterSendersUid: [],
//searchFields: 3,
filterMsgToTime: filterMsgToTime,
filterMsgFromTime: filterMsgFromTime,
isReverseOrder: false,
@@ -142,11 +137,11 @@ export class NTQQMsgApi {
}
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
//searchFields: 3,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,

View File

@@ -264,7 +264,7 @@ export class NTQQWebApi {
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
let HonorInfo = {
group_id: Number(groupCode),
group_id: groupCode,
current_talkative: {},
talkative_list: [],
performer_list: [],

View File

@@ -1,245 +0,0 @@
import { GroupDetailInfoV2Param, GroupExtInfo, GroupExtFilter } from "../types";
export function createGroupDetailInfoV2Param(group_code: string): GroupDetailInfoV2Param {
return {
groupCode: group_code,
filter:
{
noCodeFingerOpenFlag: 0,
noFingerOpenFlag: 0,
groupName: 0,
classExt: 0,
classText: 0,
fingerMemo: 0,
richFingerMemo: 0,
tagRecord: 0,
groupGeoInfo:
{
ownerUid: 0,
setTime: 0,
cityId: 0,
longitude: 0,
latitude: 0,
geoContent: 0,
poiId: 0
},
groupExtAdminNum: 0,
flag: 0,
groupMemo: 0,
groupAioSkinUrl: 0,
groupBoardSkinUrl: 0,
groupCoverSkinUrl: 0,
groupGrade: 0,
activeMemberNum: 0,
certificationType: 0,
certificationText: 0,
groupNewGuideLines:
{
enabled: 0,
content: 0
},
groupFace: 0,
addOption: 0,
shutUpTime: 0,
groupTypeFlag: 0,
appPrivilegeFlag: 0,
appPrivilegeMask: 0,
groupExtOnly:
{
tribeId: 0,
moneyForAddGroup: 0
}, groupSecLevel: 0,
groupSecLevelInfo: 0,
subscriptionUin: 0,
subscriptionUid: "",
allowMemberInvite: 0,
groupQuestion: 0,
groupAnswer: 0,
groupFlagExt3: 0,
groupFlagExt3Mask: 0,
groupOpenAppid: 0,
rootId: 0,
msgLimitFrequency: 0,
hlGuildAppid: 0,
hlGuildSubType: 0,
hlGuildOrgId: 0,
groupFlagExt4: 0,
groupFlagExt4Mask: 0,
groupSchoolInfo: {
location: 0,
grade: 0,
school: 0
},
groupCardPrefix:
{
introduction: 0,
rptPrefix: 0
}, allianceId: 0,
groupFlagPro1: 0,
groupFlagPro1Mask: 0
},
modifyInfo: {
noCodeFingerOpenFlag: 0,
noFingerOpenFlag: 0,
groupName: "",
classExt: 0,
classText: "",
fingerMemo: "",
richFingerMemo: "",
tagRecord: [],
groupGeoInfo: {
ownerUid: "",
SetTime: 0,
CityId: 0,
Longitude: "",
Latitude: "",
GeoContent: "",
poiId: ""
},
groupExtAdminNum: 0,
flag: 0,
groupMemo: "",
groupAioSkinUrl: "",
groupBoardSkinUrl: "",
groupCoverSkinUrl: "",
groupGrade: 0,
activeMemberNum: 0,
certificationType: 0,
certificationText: "",
groupNewGuideLines: {
enabled: false,
content: ""
}, groupFace: 0,
addOption: 0,
shutUpTime: 0,
groupTypeFlag: 0,
appPrivilegeFlag: 0,
appPrivilegeMask: 0,
groupExtOnly: {
tribeId: 0,
moneyForAddGroup: 0
},
groupSecLevel: 0,
groupSecLevelInfo: 0,
subscriptionUin: "",
subscriptionUid: "",
allowMemberInvite: 0,
groupQuestion: "",
groupAnswer: "",
groupFlagExt3: 0,
groupFlagExt3Mask: 0,
groupOpenAppid: 0,
rootId: "",
msgLimitFrequency: 0,
hlGuildAppid: 0,
hlGuildSubType: 0,
hlGuildOrgId: 0,
groupFlagExt4: 0,
groupFlagExt4Mask: 0,
groupSchoolInfo: {
location: "",
grade: 0,
school: ""
},
groupCardPrefix:
{
introduction: "",
rptPrefix: []
},
allianceId: "",
groupFlagPro1: 0,
groupFlagPro1Mask: 0
}
}
}
export function createGroupExtInfo(group_code: string): GroupExtInfo {
return {
groupCode: group_code,
resultCode: 0,
extInfo: {
groupInfoExtSeq: 0,
reserve: 0,
luckyWordId: '',
lightCharNum: 0,
luckyWord: '',
starId: 0,
essentialMsgSwitch: 0,
todoSeq: 0,
blacklistExpireTime: 0,
isLimitGroupRtc: 0,
companyId: 0,
hasGroupCustomPortrait: 0,
bindGuildId: '',
groupOwnerId: {
memberUin: '',
memberUid: '',
memberQid: '',
},
essentialMsgPrivilege: 0,
msgEventSeq: '',
inviteRobotSwitch: 0,
gangUpId: '',
qqMusicMedalSwitch: 0,
showPlayTogetherSwitch: 0,
groupFlagPro1: '',
groupBindGuildIds: {
guildIds: [],
},
viewedMsgDisappearTime: '',
groupExtFlameData: {
switchState: 0,
state: 0,
dayNums: [],
version: 0,
updateTime: '',
isDisplayDayNum: false,
},
groupBindGuildSwitch: 0,
groupAioBindGuildId: '',
groupExcludeGuildIds: {
guildIds: [],
},
fullGroupExpansionSwitch: 0,
fullGroupExpansionSeq: '',
inviteRobotMemberSwitch: 0,
inviteRobotMemberExamine: 0,
groupSquareSwitch: 0,
}
}
}
export function createGroupExtFilter(): GroupExtFilter {
return {
groupInfoExtSeq: 0,
reserve: 0,
luckyWordId: 0,
lightCharNum: 0,
luckyWord: 0,
starId: 0,
essentialMsgSwitch: 0,
todoSeq: 0,
blacklistExpireTime: 0,
isLimitGroupRtc: 0,
companyId: 0,
hasGroupCustomPortrait: 0,
bindGuildId: 0,
groupOwnerId: 0,
essentialMsgPrivilege: 0,
msgEventSeq: 0,
inviteRobotSwitch: 0,
gangUpId: 0,
qqMusicMedalSwitch: 0,
showPlayTogetherSwitch: 0,
groupFlagPro1: 0,
groupBindGuildIds: 0,
viewedMsgDisappearTime: 0,
groupExtFlameData: 0,
groupBindGuildSwitch: 0,
groupAioBindGuildId: 0,
groupExcludeGuildIds: 0,
fullGroupExpansionSwitch: 0,
fullGroupExpansionSeq: 0,
inviteRobotMemberSwitch: 0,
inviteRobotMemberExamine: 0,
groupSquareSwitch: 0,
}
};

View File

@@ -1 +0,0 @@
export * from "./group";

View File

@@ -234,85 +234,5 @@
"3.2.16-33800": {
"appid": 537274009,
"qua": "V1_LNX_NQ_3.2.16_33800_GW_B"
},
"9.9.19-34231": {
"appid": 537279209,
"qua": "V1_WIN_NQ_9.9.19_34231_GW_B"
},
"3.2.17-34231": {
"appid": 537279245,
"qua": "V1_LNX_NQ_3.2.17_34231_GW_B"
},
"9.9.19-34362": {
"appid": 537279260,
"qua": "V1_WIN_NQ_9.9.19_34362_GW_B"
},
"3.2.17-34362": {
"appid": 537279296,
"qua": "V1_LNX_NQ_3.2.17_34362_GW_B"
},
"9.9.19-34467": {
"appid": 537282256,
"qua": "V1_WIN_NQ_9.9.19_34467_GW_B"
},
"3.2.17-34467": {
"appid": 537282292,
"qua": "V1_LNX_NQ_3.2.17_34467_GW_B"
},
"9.9.19-34566": {
"appid": 537282307,
"qua": "V1_WIN_NQ_9.9.19_34566_GW_B"
},
"3.2.17-34566": {
"appid": 537282343,
"qua": "V1_LNX_NQ_3.2.17_34566_GW_B"
},
"3.2.17-34606": {
"appid": 537282343,
"qua": "V1_LNX_NQ_3.2.17_34606_GW_B"
},
"9.9.19-34606": {
"appid": 537282307,
"qua": "V1_WIN_NQ_9.9.19_34606_GW_B"
},
"9.9.19-34740": {
"appid": 537290691,
"qua": "V1_WIN_NQ_9.9.19_34740_GW_B"
},
"3.2.17-34740": {
"appid": 537290727,
"qua": "V1_LNX_NQ_3.2.17_34740_GW_B"
},
"9.9.19-34958": {
"appid": 537290742,
"qua": "V1_WIN_NQ_9.9.19_34958_GW_B"
},
"3.2.17-35184": {
"appid": 537291084,
"qua": "V1_LNX_NQ_3.2.17_35184_GW_B"
},
"9.9.19-35184": {
"appid": 537291048,
"qua": "V1_WIN_NQ_9.9.19_35184_GW_B"
},
"3.2.17-35341": {
"appid": 537291383,
"qua": "V1_LNX_NQ_3.2.17_35341_GW_B"
},
"9.9.19-35341": {
"appid": 537291347,
"qua": "V1_WIN_NQ_9.9.19_35341_GW_B"
},
"9.9.19-35469": {
"appid": 537291398,
"qua": "V1_WIN_NQ_9.9.19_35469_GW_B"
},
"3.2.18-35951": {
"appid": 537296013,
"qua": "V1_LNX_NQ_3.2.18_35951_GW_B"
},
"9.9.20-35951": {
"appid": 537295977,
"qua": "V1_WIN_NQ_9.9.20_35951_GW_B"
}
}

View File

@@ -314,93 +314,5 @@
"3.2.16-33800-arm64": {
"send": "7262BB0",
"recv": "72664E0"
},
"9.9.19-34231-x64": {
"send": "3BD73D0",
"recv": "3BDBBD0"
},
"3.2.17-34231-x64": {
"send": "AD787E0",
"recv": "AD7C200"
},
"3.2.17-34231-arm64": {
"send": "770CDC0",
"recv": "77106F0"
},
"9.9.19-34362-x64": {
"send": "3BD80D0",
"recv": "3BDC8D0"
},
"9.9.19-34467-x64": {
"send": "3BD8690",
"recv": "3BDCE90"
},
"9.9.19-34566-x64": {
"send": "3BDA110",
"recv": "3BDE910"
},
"9.9.19-34606-x64": {
"send": "3BDA110",
"recv": "3BDE910"
},
"3.2.17-34606-x64": {
"send": "AD7DC60",
"recv": "AD81680"
},
"3.2.17-34606-arm64": {
"send": "7711270",
"recv": "7714BA0"
},
"9.9.19-34740-x64": {
"send": "3BDD8D0",
"recv": "3BE20D0"
},
"3.2.17-34740-x64": {
"send": "ADDF0A0",
"recv": "ADE2AC0"
},
"3.2.17-34740-arm64": {
"send": "7753BB8",
"recv": "77574E8"
},
"9.9.19-34958-x64": {
"send": "3BDD8D0",
"recv": "3BE20D0"
},
"3.2.17-35184-x64": {
"send": "AE0DDE0",
"recv": "AE11800"
},
"3.2.17-35184-arm64": {
"send": "7776028",
"recv": "7779958"
},
"9.9.19-35184-x64": {
"send": "3BE5A10",
"recv": "3BEA210"
},
"9.9.19-35341-x64": {
"send": "3BF1D50",
"recv": "3BF6550"
},
"9.9.19-35469-x64": {
"send": "3BF1D50",
"recv": "3BF6550"
},
"3.2.17-35341-x64": {
"send": "AE2F700",
"recv": "AE33120"
},
"3.2.17-35341-arm64": {
"send": "778D840",
"recv": "7791170"
},
"9.9.20-35951-x64": {
"send": "3034BAC",
"recv": "3038354"
},
"3.2.18-35951-x64": {
"send": "AFBBB00",
"recv": "AFBF520"
}
}

View File

@@ -6,17 +6,6 @@ interface ServerRkeyData {
private_rkey: string;
expired_time: number;
}
interface OneBotApiRet {
status: string,
retcode: number,
data: ServerRkeyData,
message: string,
wording: string,
}
interface UrlFailureInfo {
count: number;
lastTimestamp: number;
}
export class RkeyManager {
serverUrl: string[] = [];
@@ -26,8 +15,9 @@ export class RkeyManager {
private_rkey: '',
expired_time: 0,
};
private urlFailures: Map<string, UrlFailureInfo> = new Map();
private readonly FAILURE_LIMIT: number = 4;
private failureCount: number = 0;
private lastFailureTimestamp: number = 0;
private readonly FAILURE_LIMIT: number = 8;
private readonly ONE_DAY: number = 24 * 60 * 60 * 1000;
constructor(serverUrl: string[], logger: LogWrapper) {
@@ -36,92 +26,50 @@ export class RkeyManager {
}
async getRkey() {
const availableUrls = this.getAvailableUrls();
if (availableUrls.length === 0) {
this.logger.logError('[Rkey] 所有服务均已禁用, 图片使用FallBack机制');
throw new Error('获取rkey失败所有服务URL均已被禁用');
const now = new Date().getTime();
if (now - this.lastFailureTimestamp > this.ONE_DAY) {
this.failureCount = 0; // 重置失败计数器
}
if (this.failureCount >= this.FAILURE_LIMIT) {
this.logger.logError('[Rkey] 服务存在异常, 图片使用FallBack机制');
throw new Error('获取rkey失败次数过多请稍后再试');
}
if (this.isExpired()) {
try {
await this.refreshRkey();
} catch (e) {
throw new Error(`${e}`);
throw new Error(`${e}`);//外抛
}
}
return this.rkeyData;
}
private getAvailableUrls(): string[] {
return this.serverUrl.filter(url => !this.isUrlDisabled(url));
}
private isUrlDisabled(url: string): boolean {
const failureInfo = this.urlFailures.get(url);
if (!failureInfo) return false;
const now = new Date().getTime();
// 如果已经过了一天,重置失败计数
if (now - failureInfo.lastTimestamp > this.ONE_DAY) {
failureInfo.count = 0;
this.urlFailures.set(url, failureInfo);
return false;
}
return failureInfo.count >= this.FAILURE_LIMIT;
}
private updateUrlFailure(url: string) {
const now = new Date().getTime();
const failureInfo = this.urlFailures.get(url) || { count: 0, lastTimestamp: 0 };
// 如果已经过了一天,重置失败计数
if (now - failureInfo.lastTimestamp > this.ONE_DAY) {
failureInfo.count = 1;
} else {
failureInfo.count++;
}
failureInfo.lastTimestamp = now;
this.urlFailures.set(url, failureInfo);
if (failureInfo.count >= this.FAILURE_LIMIT) {
this.logger.logError(`[Rkey] URL ${url} 已被禁用,失败次数达到 ${this.FAILURE_LIMIT}`);
}
}
isExpired(): boolean {
const now = new Date().getTime() / 1000;
return now > this.rkeyData.expired_time;
}
async refreshRkey() {
const availableUrls = this.getAvailableUrls();
if (availableUrls.length === 0) {
this.logger.logError('[Rkey] 所有服务均已禁用');
throw new Error('获取rkey失败所有服务URL均已被禁用');
}
for (const url of availableUrls) {
//刷新rkey
for (const url of this.serverUrl) {
try {
let temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
if ('retcode' in temp) {
// 支持Onebot Ret风格
temp = (temp as unknown as OneBotApiRet).data;
}
const temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
this.rkeyData = {
group_rkey: temp.group_rkey.slice(6),
private_rkey: temp.private_rkey.slice(6),
expired_time: temp.expired_time
};
this.failureCount = 0;
return;
} catch (e) {
this.logger.logError(`[Rkey] 异常服务 ${url} 异常 / `, e);
this.updateUrlFailure(url);
if (url === availableUrls[availableUrls.length - 1]) {
throw new Error(`获取rkey失败: ${e}`);
this.failureCount++;
this.lastFailureTimestamp = new Date().getTime();
//是否为最后一个url
if (url === this.serverUrl[this.serverUrl.length - 1]) {
throw new Error(`获取rkey失败: ${e}`);//外抛
}
}
}

View File

@@ -30,10 +30,6 @@ import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { handleServiceServerOnce, receiverServiceListener, ServiceMethodCommand } from '@/remote/service';
import { rpc_decode, rpc_encode } from '@/remote/serialize';
import { PipeClient, PipeServer } from '@/remote/pipe';
import { RemoteWrapperSession } from '@/remote/remoteSession';
export * from './wrapper';
export * from './types';
export * from './services';
@@ -101,63 +97,9 @@ export class NapCatCore {
constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session);
// 管道服务端测试
let pipe_server = new PipeServer('//./pipe/napcat');
pipe_server.registerHandler(async (packet, helper) => {
if (packet.type !== 'event_request') {
return helper.error('Invalid packet type');
}
let event_rpc_data = rpc_decode<{ params: any[] }>(JSON.parse(packet.data));
let event_rpc_trace = packet.trace;
let event_rpc_command = packet.command as ServiceMethodCommand;
let event_rpc_result = await handleServiceServerOnce(event_rpc_command,
async (listenerCommand: string, ...args: any[]) => {
let listener_data = rpc_encode<{ params: any[] }>({ params: args });
helper.sendListenerCallback(listenerCommand, JSON.stringify(rpc_encode(listener_data)));
},
this.eventWrapper,
...event_rpc_data.params
);
return helper.sendEventResponse(event_rpc_trace, JSON.stringify(rpc_encode(event_rpc_result)));
});
pipe_server.start().then(() => {
this.context.logger.log('Pipe server started successfully');
let pipe_client = new PipeClient('//./pipe/napcat');
let trace_callback_map = new Map<string, (trace: string, data: any) => void>();
pipe_client.registerHandler(async (packet, _helper) => {
if (packet.type == 'event_response') {
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
trace_callback_map.get(packet.trace)?.(packet.trace, event_rpc_data);
} else if (packet.type == 'listener_callback') {
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
await receiverServiceListener(packet.command, ...event_rpc_data);
}
});
this.context.session = new RemoteWrapperSession(async (_serviceClient, serviceCommand, ...args) => {
let trace = crypto.randomUUID();
return await new Promise((resolve, _reject) => {
trace_callback_map.set(trace, (_trace, data) => {
//console.log('Received response for trace:', _trace, 'with data:', data);
resolve(data);
});
pipe_client.sendRequest(serviceCommand, JSON.stringify(rpc_encode({ params: args })), trace);
});
});
pipe_client.connect().then(() => {
this.context.logger.log('Pipe client connected successfully');
}).catch((e) => {
this.context.logger.logError('Pipe client connection failed: ' + e.message);
});
}).catch((e) => {
this.context.logger.logError('Pipe server start failed: ' + e.message);
});
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath,NapcatConfigSchema);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this),
@@ -309,13 +251,13 @@ export async function genSessionConfig(
}
export interface InstanceContext {
session: NodeIQQNTWrapperSession;
workingEnv: NapCatCoreWorkingEnv;
wrapper: WrapperNodeApi;
logger: LogWrapper;
loginService: NodeIKernelLoginService;
basicInfoWrapper: QQBasicInfoWrapper;
pathWrapper: NapCatPathWrapper;
readonly workingEnv: NapCatCoreWorkingEnv;
readonly wrapper: WrapperNodeApi;
readonly session: NodeIQQNTWrapperSession;
readonly logger: LogWrapper;
readonly loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper;
}
export interface StableNTApiWrapper {

View File

@@ -3,74 +3,57 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/types';
export type OnBuddyChangeParams = BuddyCategoryType[];
export class NodeIKernelBuddyListener {
onBuddyListChangedV2(_arg: unknown): any {
onBuddyListChangedV2(arg: unknown): any {
}
onAddBuddyNeedVerify(_arg: unknown): any {
onAddBuddyNeedVerify(arg: unknown): any {
}
onAddMeSettingChanged(_arg: unknown): any {
onAddMeSettingChanged(arg: unknown): any {
}
onAvatarUrlUpdated(_arg: unknown): any {
onAvatarUrlUpdated(arg: unknown): any {
}
onBlockChanged(_arg: unknown): any {
onBlockChanged(arg: unknown): any {
}
onBuddyDetailInfoChange(_arg: unknown): any {
onBuddyDetailInfoChange(arg: unknown): any {
}
onBuddyInfoChange(_arg: unknown): any {
onBuddyInfoChange(arg: unknown): any {
}
onBuddyListChange(_arg: OnBuddyChangeParams): any {
onBuddyListChange(arg: OnBuddyChangeParams): any {
}
onBuddyRemarkUpdated(_arg: unknown): any {
onBuddyRemarkUpdated(arg: unknown): any {
}
onBuddyReqChange(_arg: FriendRequestNotify): any {
onBuddyReqChange(arg: FriendRequestNotify): any {
}
onBuddyReqUnreadCntChange(_arg: unknown): any {
onBuddyReqUnreadCntChange(arg: unknown): any {
}
onCheckBuddySettingResult(_arg: unknown): any {
onCheckBuddySettingResult(arg: unknown): any {
}
onDelBatchBuddyInfos(_arg: unknown): any {
onDelBatchBuddyInfos(arg: unknown): any {
}
onDoubtBuddyReqChange(_arg:
{
reqId: string;
cookie: string;
doubtList: Array<{
uid: string;
nick: string;
age: number,
sex: number;
commFriendNum: number;
reqTime: string;
msg: string;
source: string;
reason: string;
groupCode: string;
nameMore?: null;
}>;
}): void | Promise<void> {
onDoubtBuddyReqChange(arg: unknown): any {
}
onDoubtBuddyReqUnreadNumChange(_num: number): void | Promise<void> {
onDoubtBuddyReqUnreadNumChange(arg: unknown): any {
}
onNickUpdated(_arg: unknown): any {
onNickUpdated(arg: unknown): any {
}
onSmartInfos(_arg: unknown): any {
onSmartInfos(arg: unknown): any {
}
onSpacePermissionInfos(_arg: unknown): any {
onSpacePermissionInfos(arg: unknown): any {
}
}

View File

@@ -21,8 +21,7 @@ export interface OnRichMediaDownloadCompleteParams {
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown,
userUsedSpacePerDay: unknown,
chatType: number,
userUsedSpacePerDay: unknown
}
export interface GroupFileInfoUpdateParamType {
@@ -98,112 +97,112 @@ export interface TempOnRecvParams {
}
export class NodeIKernelMsgListener {
onAddSendMsg(_msgRecord: RawMessage): any {
onAddSendMsg(msgRecord: RawMessage): any {
}
onBroadcastHelperDownloadComplete(_broadcastHelperTransNotifyInfo: unknown): any {
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): any {
}
onBroadcastHelperProgressUpdate(_broadcastHelperTransNotifyInfo: unknown): any {
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): any {
}
onChannelFreqLimitInfoUpdate(_contact: unknown, _z: unknown, _freqLimitInfo: unknown): any {
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): any {
}
onContactUnreadCntUpdate(_hashMap: unknown): any {
onContactUnreadCntUpdate(hashMap: unknown): any {
}
onCustomWithdrawConfigUpdate(_customWithdrawConfig: unknown): any {
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): any {
}
onDraftUpdate(_contact: unknown, _arrayList: unknown, _j2: unknown): any {
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): any {
}
onEmojiDownloadComplete(_emojiNotifyInfo: unknown): any {
onEmojiDownloadComplete(emojiNotifyInfo: unknown): any {
}
onEmojiResourceUpdate(_emojiResourceInfo: unknown): any {
onEmojiResourceUpdate(emojiResourceInfo: unknown): any {
}
onFeedEventUpdate(_firstViewDirectMsgNotifyInfo: unknown): any {
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
}
onFileMsgCome(_arrayList: unknown): any {
onFileMsgCome(arrayList: unknown): any {
}
onFirstViewDirectMsgUpdate(_firstViewDirectMsgNotifyInfo: unknown): any {
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
}
onFirstViewGroupGuildMapping(_arrayList: unknown): any {
onFirstViewGroupGuildMapping(arrayList: unknown): any {
}
onGrabPasswordRedBag(_i2: unknown, _str: unknown, _i3: unknown, _recvdOrder: unknown, _msgRecord: unknown): any {
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): any {
}
onGroupFileInfoAdd(_groupItem: unknown): any {
onGroupFileInfoAdd(groupItem: unknown): any {
}
onGroupFileInfoUpdate(_groupFileListResult: GroupFileInfoUpdateParamType): any {
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType): any {
}
onGroupGuildUpdate(_groupGuildNotifyInfo: unknown): any {
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): any {
}
onGroupTransferInfoAdd(_groupItem: unknown): any {
onGroupTransferInfoAdd(groupItem: unknown): any {
}
onGroupTransferInfoUpdate(_groupFileListResult: unknown): any {
onGroupTransferInfoUpdate(groupFileListResult: unknown): any {
}
onGuildInteractiveUpdate(_guildInteractiveNotificationItem: unknown): any {
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): any {
}
onGuildMsgAbFlagChanged(_guildMsgAbFlag: unknown): any {
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): any {
}
onGuildNotificationAbstractUpdate(_guildNotificationAbstractInfo: unknown): any {
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): any {
}
onHitCsRelatedEmojiResult(_downloadRelateEmojiResultInfo: unknown): any {
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): any {
}
onHitEmojiKeywordResult(_hitRelatedEmojiWordsResult: unknown): any {
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): any {
}
onHitRelatedEmojiResult(_relatedWordEmojiInfo: unknown): any {
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): any {
}
onImportOldDbProgressUpdate(_importOldDbMsgNotifyInfo: unknown): any {
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): any {
}
onInputStatusPush(_inputStatusInfo: {
onInputStatusPush(inputStatusInfo: {
chatType: number;
eventType: number;
fromUin: string;
@@ -216,55 +215,55 @@ export class NodeIKernelMsgListener {
}
onKickedOffLine(_kickedInfo: KickedOffLineInfo): any {
onKickedOffLine(kickedInfo: KickedOffLineInfo): any {
}
onLineDev(_arrayList: unknown): any {
onLineDev(arrayList: unknown): any {
}
onLogLevelChanged(_j2: unknown): any {
onLogLevelChanged(j2: unknown): any {
}
onMsgAbstractUpdate(_arrayList: unknown): any {
onMsgAbstractUpdate(arrayList: unknown): any {
}
onMsgBoxChanged(_arrayList: unknown): any {
onMsgBoxChanged(arrayList: unknown): any {
}
onMsgDelete(_contact: unknown, _arrayList: unknown): any {
onMsgDelete(contact: unknown, arrayList: unknown): any {
}
onMsgEventListUpdate(_hashMap: unknown): any {
onMsgEventListUpdate(hashMap: unknown): any {
}
onMsgInfoListAdd(_arrayList: unknown): any {
onMsgInfoListAdd(arrayList: unknown): any {
}
onMsgInfoListUpdate(_msgList: RawMessage[]): any {
onMsgInfoListUpdate(msgList: RawMessage[]): any {
}
onMsgQRCodeStatusChanged(_i2: unknown): any {
onMsgQRCodeStatusChanged(i2: unknown): any {
}
onMsgRecall(_chatType: ChatType, _uid: string, _msgSeq: string): any {
onMsgRecall(chatType: ChatType, uid: string, msgSeq: string): any {
}
onMsgSecurityNotify(_msgRecord: unknown): any {
onMsgSecurityNotify(msgRecord: unknown): any {
}
onMsgSettingUpdate(_msgSetting: unknown): any {
onMsgSettingUpdate(msgSetting: unknown): any {
}
@@ -280,108 +279,108 @@ export class NodeIKernelMsgListener {
}
onReadFeedEventUpdate(_firstViewDirectMsgNotifyInfo: unknown): any {
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
}
onRecvGroupGuildFlag(_i2: unknown): any {
onRecvGroupGuildFlag(i2: unknown): any {
}
onRecvMsg(_arrayList: RawMessage[]): any {
onRecvMsg(arrayList: RawMessage[]): any {
}
onRecvMsgSvrRspTransInfo(_j2: unknown, _contact: unknown, _i2: unknown, _i3: unknown, _str: unknown, _bArr: unknown): any {
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): any {
}
onRecvOnlineFileMsg(_arrayList: unknown): any {
onRecvOnlineFileMsg(arrayList: unknown): any {
}
onRecvS2CMsg(_arrayList: unknown): any {
onRecvS2CMsg(arrayList: unknown): any {
}
onRecvSysMsg(_arrayList: Array<number>): any {
onRecvSysMsg(arrayList: Array<number>): any {
}
onRecvUDCFlag(_i2: unknown): any {
onRecvUDCFlag(i2: unknown): any {
}
onRichMediaDownloadComplete(_fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
}
onRichMediaProgerssUpdate(_fileTransNotifyInfo: unknown): any {
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): any {
}
onRichMediaUploadComplete(_fileTransNotifyInfo: unknown): any {
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): any {
}
onSearchGroupFileInfoUpdate(_searchGroupFileResult: unknown): any {
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): any {
}
onSendMsgError(_j2: unknown, _contact: unknown, _i2: unknown, _str: unknown): any {
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): any {
}
onSysMsgNotification(_i2: unknown, _j2: unknown, _j3: unknown, _arrayList: unknown): any {
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): any {
}
onTempChatInfoUpdate(_tempChatInfo: TempOnRecvParams): any {
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): any {
}
onUnreadCntAfterFirstView(_hashMap: unknown): any {
onUnreadCntAfterFirstView(hashMap: unknown): any {
}
onUnreadCntUpdate(_hashMap: unknown): any {
onUnreadCntUpdate(hashMap: unknown): any {
}
onUserChannelTabStatusChanged(_z: unknown): any {
onUserChannelTabStatusChanged(z: unknown): any {
}
onUserOnlineStatusChanged(_z: unknown): any {
onUserOnlineStatusChanged(z: unknown): any {
}
onUserTabStatusChanged(_arrayList: unknown): any {
onUserTabStatusChanged(arrayList: unknown): any {
}
onlineStatusBigIconDownloadPush(_i2: unknown, _j2: unknown, _str: unknown): any {
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
}
onlineStatusSmallIconDownloadPush(_i2: unknown, _j2: unknown, _str: unknown): any {
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
}
// 第一次发现于Linux
onUserSecQualityChanged(..._args: unknown[]): any {
onUserSecQualityChanged(...args: unknown[]): any {
}
onMsgWithRichLinkInfoUpdate(..._args: unknown[]): any {
onMsgWithRichLinkInfoUpdate(...args: unknown[]): any {
}
onRedTouchChanged(..._args: unknown[]): any {
onRedTouchChanged(...args: unknown[]): any {
}
// 第一次发现于Win 9.9.9-23159
onBroadcastHelperProgerssUpdate(..._args: unknown[]): any {
onBroadcastHelperProgerssUpdate(...args: unknown[]): any {
}
}

View File

@@ -1,71 +1,71 @@
import { SelfStatusInfo, User, UserDetailInfoListenerArg } from '@/core/types';
import { User, UserDetailInfoListenerArg } from '@/core/types';
export class NodeIKernelProfileListener {
onUserDetailInfoChanged(_arg: UserDetailInfoListenerArg): void {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(..._args: unknown[]): any {
onProfileSimpleChanged(...args: unknown[]): any {
}
onProfileDetailInfoChanged(_profile: User): any {
onProfileDetailInfoChanged(profile: User): any {
}
onStatusUpdate(..._args: unknown[]): any {
onStatusUpdate(...args: unknown[]): any {
}
onSelfStatusChanged(_info: SelfStatusInfo): any {
onSelfStatusChanged(...args: unknown[]): any {
}
onStrangerRemarkChanged(..._args: unknown[]): any {
onStrangerRemarkChanged(...args: unknown[]): any {
}
onMemberListChange(..._args: unknown[]): any {
onMemberListChange(...args: unknown[]): any {
}
onMemberInfoChange(..._args: unknown[]): any {
onMemberInfoChange(...args: unknown[]): any {
}
onGroupListUpdate(..._args: unknown[]): any {
onGroupListUpdate(...args: unknown[]): any {
}
onGroupAllInfoChange(..._args: unknown[]): any {
onGroupAllInfoChange(...args: unknown[]): any {
}
onGroupDetailInfoChange(..._args: unknown[]): any {
onGroupDetailInfoChange(...args: unknown[]): any {
}
onGroupConfMemberChange(..._args: unknown[]): any {
onGroupConfMemberChange(...args: unknown[]): any {
}
onGroupExtListUpdate(..._args: unknown[]): any {
onGroupExtListUpdate(...args: unknown[]): any {
}
onGroupNotifiesUpdated(..._args: unknown[]): any {
onGroupNotifiesUpdated(...args: unknown[]): any {
}
onGroupNotifiesUnreadCountUpdated(..._args: unknown[]): any {
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
}
onGroupMemberLevelInfoChange(..._args: unknown[]): any {
onGroupMemberLevelInfoChange(...args: unknown[]): any {
}
onGroupBulletinChange(..._args: unknown[]): any {
onGroupBulletinChange(...args: unknown[]): any {
}
}

View File

@@ -6,14 +6,13 @@ import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgReplyElement,
PacketMsgVideoElement,
PacketMsgVideoElement
} from '@/core/packet/message/element';
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/core/packet/transformer/proto';
import { IndexNode, LongMsgResult, MsgInfo } from '@/core/packet/transformer/proto';
import { OidbPacket } from '@/core/packet/transformer/base';
import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
import { gunzipSync } from 'zlib';
@@ -30,8 +29,13 @@ export class PacketOperationContext {
return await this.context.client.sendOidbPacket(pkt, rsp);
}
async SendPoke(is_group: boolean, peer: number, target?: number) {
const req = trans.SendPoke.build(is_group, peer, target ?? peer);
async GroupPoke(groupUin: number, uin: number) {
const req = trans.SendPoke.build(uin, groupUin);
await this.context.client.sendOidbPacket(req);
}
async FriendPoke(uin: number) {
const req = trans.SendPoke.build(uin);
await this.context.client.sendOidbPacket(req);
}
@@ -72,24 +76,22 @@ export class PacketOperationContext {
async UploadResources(msg: PacketMsg[], groupUin: number = 0) {
const chatType = groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C;
const peerUid = groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid;
const reqList = msg.flatMap((m) =>
m.msg
.map((e) => {
if (e instanceof PacketMsgPicElement) {
return this.context.highway.uploadImage({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgVideoElement) {
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgPttElement) {
return this.context.highway.uploadPtt({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgFileElement) {
return this.context.highway.uploadFile({ chatType, peerUid }, e);
}
return null;
})
.filter(Boolean)
const reqList = msg.flatMap(m =>
m.msg.map(e => {
if (e instanceof PacketMsgPicElement) {
return this.context.highway.uploadImage({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgVideoElement) {
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgPttElement) {
return this.context.highway.uploadPtt({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgFileElement) {
return this.context.highway.uploadFile({ chatType, peerUid }, e);
}
return null;
}).filter(Boolean)
);
const res = await Promise.allSettled(reqList);
this.context.logger.info(`上传资源${res.length}个,失败${res.filter((r) => r.status === 'rejected').length}`);
this.context.logger.info(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
@@ -98,13 +100,10 @@ export class PacketOperationContext {
}
async UploadImage(img: PacketMsgPicElement) {
await this.context.highway.uploadImage(
{
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.context.napcore.basicInfo.uid,
},
img
);
await this.context.highway.uploadImage({
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.context.napcore.basicInfo.uid
}, img);
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
if (!index) {
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
@@ -119,20 +118,6 @@ export class PacketOperationContext {
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadPtt.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadVideo.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadVideo.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupImage.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
@@ -140,21 +125,6 @@ export class PacketOperationContext {
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupVideoUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupVideo.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async ImageOCR(imgUrl: string) {
const req = trans.ImageOCR.build(imgUrl);
const resp = await this.context.client.sendOidbPacket(req, true);
@@ -167,86 +137,29 @@ export class PacketOperationContext {
coordinates: item.polygon.coordinates.map((c) => {
return {
x: c.x,
y: c.y,
y: c.y
};
}),
};
}),
language: res.ocrRspBody.language,
language: res.ocrRspBody.language
} as ImageOcrResult;
}
private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) {
const ps = msg.map((m) => {
return m.msg.map(async (e) => {
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`);
if (!e.targetPeer?.peerUid) {
this.context.logger.error(`targetPeer is undefined!`);
}
let targetMsg: NapProtoEncodeStructType<typeof PushMsgBody>[] | undefined;
if (e.isGroupReply) {
targetMsg = await this.FetchGroupMessage(+(e.targetPeer?.peerUid ?? 0), e.targetMessageSeq, e.targetMessageSeq);
} else {
targetMsg = await this.FetchC2CMessage(await this.context.napcore.basicInfo.uin2uid(e.targetUin), e.targetMessageSeq, e.targetMessageSeq);
}
e.targetElems = targetMsg.at(0)?.body?.richText?.elems;
e.targetSourceMsg = targetMsg.at(0);
}
});
}).flat();
await Promise.all(ps)
await this.UploadResources(msg, groupUin);
}
async FetchGroupMessage(groupUin: number, startSeq: number, endSeq: number): Promise<NapProtoDecodeStructType<typeof PushMsgBody>[]> {
const req = trans.FetchGroupMessage.build(groupUin, startSeq, endSeq);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchGroupMessage.parse(resp);
return res.body.messages
}
async FetchC2CMessage(targetUid: string, startSeq: number, endSeq: number): Promise<NapProtoDecodeStructType<typeof PushMsgBody>[]> {
const req = trans.FetchC2CMessage.build(targetUid, startSeq, endSeq);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchC2CMessage.parse(resp);
return res.messages
}
async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.SendPreprocess(msg, groupUin);
await this.UploadResources(msg, groupUin);
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async MoveGroupFile(
groupUin: number,
fileUUID: string,
currentParentDirectory: string,
targetParentDirectory: string
) {
const req = trans.MoveGroupFile.build(groupUin, fileUUID, currentParentDirectory, targetParentDirectory);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.MoveGroupFile.parse(resp);
return res.move.retCode;
}
async RenameGroupFile(groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string) {
const req = trans.RenameGroupFile.build(groupUin, fileUUID, currentParentDirectory, newName);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.RenameGroupFile.parse(resp);
return res.rename.retCode;
}
async GetGroupFileUrl(groupUin: number, fileUUID: string) {
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
}
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
const resp = await this.context.client.sendOidbPacket(req, true);
@@ -254,6 +167,13 @@ export class PacketOperationContext {
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadGroupPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) {
const req = trans.GetMiniAppAdaptShareInfo.build(param);
const resp = await this.context.client.sendOidbPacket(req, true);
@@ -269,17 +189,12 @@ export class PacketOperationContext {
return res.content.map((item) => {
return {
category: item.category,
voices: item.voices,
voices: item.voices
};
});
}
async GetAiVoice(
groupUin: number,
voiceId: string,
text: string,
chatType: AIVoiceChatType
): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
async GetAiVoice(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);
@@ -307,7 +222,6 @@ export class PacketOperationContext {
if (!main?.actionData.msgBody) {
throw new Error('msgBody is empty');
}
this.context.logger.debug('rawChains ', inflate.toString('hex'));
const messagesPromises = main.actionData.msgBody.map(async (msg) => {
if (!msg?.body?.richText?.elems) {
@@ -323,12 +237,12 @@ export class PacketOperationContext {
const groupUin = msg?.responseHead.grp?.groupUin ?? 0;
element.picElement = {
...element.picElement,
originImageUrl: await this.GetGroupImageUrl(groupUin, index!),
originImageUrl: await this.GetGroupImageUrl(groupUin, index!)
};
} else {
element.picElement = {
...element.picElement,
originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!),
originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!)
};
}
return element;
@@ -341,7 +255,7 @@ export class PacketOperationContext {
elements: elements,
guildId: '',
isOnlineMsg: false,
msgId: '7467703692092974645', // TODO: no necessary
msgId: '7467703692092974645', // TODO: no necessary
msgRandom: '0',
msgSeq: String(msg.contentHead.sequence ?? 0),
msgTime: String(msg.contentHead.timeStamp ?? 0),

View File

@@ -24,15 +24,12 @@ export class PacketMsgBuilder {
}
return {
responseHead: {
fromUin: node.senderUin,
type: 0,
sigMap: 0,
toUin: 0,
fromUid: '',
fromUin: node.senderUin,
toUid: node.groupId ? undefined : selfUid,
forward: node.groupId ? undefined : {
friendName: node.senderName,
},
toUid: node.groupId ? undefined : selfUid,
grp: node.groupId ? {
groupUin: node.groupId,
memberName: node.senderName,
@@ -43,13 +40,16 @@ export class PacketMsgBuilder {
type: node.groupId ? 82 : 9,
subType: node.groupId ? undefined : 4,
divSeq: node.groupId ? undefined : 4,
autoReply: 0,
msgId: crypto.randomBytes(4).readUInt32LE(0),
sequence: crypto.randomBytes(4).readUInt32LE(0),
timeStamp: +node.time.toString().substring(0, 10),
field7: BigInt(1),
field8: 0,
field9: 0,
forward: {
field1: 0,
field2: 0,
field3: node.groupId ? 1 : 2,
field3: node.groupId ? 0 : 2,
unknownBase64: avatar,
avatar: avatar
}

View File

@@ -10,7 +10,6 @@ import {
MsgInfo,
NotOnlineImage,
OidbSvcTrpcTcp0XE37_800Response,
PushMsgBody,
QBigFaceExtra,
QSmallFaceExtra,
} from '@/core/packet/transformer/proto';
@@ -30,8 +29,7 @@ import {
SendReplyElement,
SendMultiForwardMsgElement,
SendTextElement,
SendVideoElement,
Peer
SendVideoElement
} from '@/core';
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
@@ -148,40 +146,41 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
}
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
time: number;
targetMessageId: bigint;
targetMessageSeq: number;
targetMessageClientSeq: number;
messageId: bigint;
messageSeq: number;
messageClientSeq: number;
targetUin: number;
targetUid: string;
targetElems?: NapProtoEncodeStructType<typeof Elem>[];
targetSourceMsg?: NapProtoEncodeStructType<typeof PushMsgBody>;
targetPeer?: Peer;
time: number;
elems: PacketMsg[];
constructor(element: SendReplyElement) {
super(element);
this.time = +(element.replyElement.replyMsgTime ?? Math.floor(Date.now() / 1000));
this.targetMessageId = BigInt(element.replyElement.replayMsgId ?? 0);
this.targetMessageSeq = +(element.replyElement.replayMsgSeq ?? 0);
this.targetMessageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
this.messageId = BigInt(element.replyElement.replayMsgId ?? 0);
this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0);
this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
this.targetUin = +(element.replyElement.senderUin ?? 0);
this.targetUid = element.replyElement.senderUidStr ?? '';
this.targetPeer = element.replyElement._replyMsgPeer;
this.time = +(element.replyElement.replyMsgTime ?? 0);
this.elems = []; // TODO: in replyElement.sourceMsgTextElems
}
get isGroupReply(): boolean {
return this.targetMessageClientSeq === 0;
return this.messageClientSeq === 0;
}
override buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
return [{
srcMsg: {
origSeqs: [this.isGroupReply ? this.targetMessageSeq : this.targetMessageClientSeq],
origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq],
senderUin: BigInt(this.targetUin),
time: this.time,
elems: this.targetElems ?? [],
sourceMsg: new NapProtoMsg(PushMsgBody).encode(this.targetSourceMsg ?? {}),
toUin: BigInt(0),
elems: [], // TODO: in replyElement.sourceMsgTextElems
pbReserve: {
messageId: this.messageId,
},
toUin: BigInt(this.targetUin),
type: 1,
}
}];
}

View File

@@ -1,35 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
class MoveGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6Response> {
constructor() {
super();
}
build(groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
move: {
groupUin: groupUin,
appId: 5,
busId: 102,
fileId: fileUUID,
parentDirectory: currentParentDirectory,
targetDirectory: targetParentDirectory,
}
});
return OidbBase.build(0x6D6, 5, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
if (res.move.retCode !== 0) {
throw new Error(`sendGroupFileMoveReq error: ${res.move.clientWording} (code=${res.move.retCode})`);
}
return res;
}
}
export default new MoveGroupFile();

View File

@@ -1,34 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
class RenameGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6Response> {
constructor() {
super();
}
build(groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
rename: {
groupUin: groupUin,
busId: 102,
fileId: fileUUID,
parentFolder: currentParentDirectory,
newFileName: newName,
}
});
return OidbBase.build(0x6D6, 4, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
if (res.rename.retCode !== 0) {
throw new Error(`sendGroupFileRenameReq error: ${res.rename.clientWording} (code=${res.rename.retCode})`);
}
return res;
}
}
export default new RenameGroupFile();

View File

@@ -8,13 +8,13 @@ class SendPoke extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
super();
}
build(is_group: boolean, peer: number, target: number): OidbPacket {
const payload = {
uin: target,
ext: 0,
...(is_group ? { groupUin: peer } : { friendUin: peer })
};
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode(payload);
build(peer: number, group?: number): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode({
uin: peer,
groupUin: group,
friendUin: group ?? peer,
ext: 0
});
return OidbBase.build(0xED3, 1, data);
}

View File

@@ -6,5 +6,3 @@ export { default as GetStrangerInfo } from './GetStrangerInfo';
export { default as SendPoke } from './SendPoke';
export { default as SetSpecialTitle } from './SetSpecialTitle';
export { default as ImageOCR } from './ImageOCR';
export { default as MoveGroupFile } from './MoveGroupFile';
export { default as RenameGroupFile } from './RenameGroupFile';

View File

@@ -1,50 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadGroupVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 2,
businessType: 2,
sceneType: 2,
group: {
groupUin: groupUin
}
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11EA, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadGroupVideo();

View File

@@ -1,51 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadPtt extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 1,
businessType: 3,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: selfUid
},
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x126D, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadPtt();

View File

@@ -1,51 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 2,
businessType: 2,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: selfUid
},
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11E9, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadVideo();

View File

@@ -13,6 +13,3 @@ export { default as UploadPrivatePtt } from './UploadPrivatePtt';
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
export { default as DownloadImage } from './DownloadImage';
export { default as DownloadGroupImage } from './DownloadGroupImage';
export { default as DownloadVideo } from './DownloadVideo';
export { default as DownloadGroupVideo } from './DownloadGroupVideo';
export { default as DownloadPtt } from './DownloadPtt';

View File

@@ -1,27 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base';
class FetchC2CMessage extends PacketTransformer<typeof proto.SsoGetC2cMsgResponse> {
constructor() {
super();
}
build(targetUid: string, startSeq: number, endSeq: number): OidbPacket {
const req = new NapProtoMsg(proto.SsoGetC2cMsg).encode({
friendUid: targetUid,
startSequence: startSeq,
endSequence: endSeq,
});
return {
cmd: 'trpc.msg.register_proxy.RegisterProxy.SsoGetC2cMsg',
data: PacketHexStrBuilder(req)
};
}
parse(data: Buffer) {
return new NapProtoMsg(proto.SsoGetC2cMsgResponse).decode(data);
}
}
export default new FetchC2CMessage();

View File

@@ -1,30 +0,0 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base';
class FetchGroupMessage extends PacketTransformer<typeof proto.SsoGetGroupMsgResponse> {
constructor() {
super();
}
build(groupUin: number, startSeq: number, endSeq: number): OidbPacket {
const req = new NapProtoMsg(proto.SsoGetGroupMsg).encode({
info: {
groupUin: groupUin,
startSequence: startSeq,
endSequence: endSeq
},
direction: true
});
return {
cmd: 'trpc.msg.register_proxy.RegisterProxy.SsoGetGroupMsg',
data: PacketHexStrBuilder(req)
};
}
parse(data: Buffer) {
return new NapProtoMsg(proto.SsoGetGroupMsgResponse).decode(data);
}
}
export default new FetchGroupMessage();

View File

@@ -1,4 +1,2 @@
export { default as UploadForwardMsg } from './UploadForwardMsg';
export { default as FetchGroupMessage } from './FetchGroupMessage';
export { default as FetchC2CMessage } from './FetchC2CMessage';
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
export { default as DownloadForwardMsg } from './DownloadForwardMsg';

View File

@@ -7,7 +7,7 @@ class OidbBase extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
super();
}
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, _isLafter: boolean = false): OidbPacket {
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({
command: cmd,
subCommand: subCmd,

View File

@@ -13,15 +13,13 @@ import {
export const ContentHead = {
type: ProtoField(1, ScalarType.UINT32),
subType: ProtoField(2, ScalarType.UINT32, true),
c2cCmd: ProtoField(3, ScalarType.UINT32, true),
ranDom: ProtoField(4, ScalarType.UINT32, true),
divSeq: ProtoField(3, ScalarType.UINT32, true),
msgId: ProtoField(4, ScalarType.UINT32, true),
sequence: ProtoField(5, ScalarType.UINT32, true),
timeStamp: ProtoField(6, ScalarType.UINT32, true),
pkgNum: ProtoField(7, ScalarType.UINT64, true),
pkgIndex: ProtoField(8, ScalarType.UINT32, true),
divSeq: ProtoField(9, ScalarType.UINT32, true),
autoReply: ProtoField(10, ScalarType.UINT32),
ntMsgSeq: ProtoField(10, ScalarType.UINT32, true),
field7: ProtoField(7, ScalarType.UINT64, true),
field8: ProtoField(8, ScalarType.UINT32, true),
field9: ProtoField(9, ScalarType.UINT32, true),
newId: ProtoField(12, ScalarType.UINT64, true),
forward: ProtoField(15, () => ForwardHead, true),
};

View File

@@ -1,6 +0,0 @@
import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
export const FileId = {
appid: ProtoField(4, ScalarType.UINT32, true),
ttl: ProtoField(10, ScalarType.UINT32, true),
};

View File

@@ -106,15 +106,15 @@ export interface NodeIKernelBuddyService {
getAddMeSetting(): unknown;
getDoubtBuddyReq(reqId: string, num: number,uk:string): Promise<GeneralCallResult>;
getDoubtBuddyReq(): unknown;
getDoubtBuddyUnreadNum(): number;
approvalDoubtBuddyReq(uid: string, str1: string, str2: string): void;
approvalDoubtBuddyReq(uid: number, isAgree: boolean): void;
delDoubtBuddyReq(uid: number): void;
delAllDoubtBuddyReq(): Promise<GeneralCallResult>;
delAllDoubtBuddyReq(): void;
reportDoubtBuddyReqUnread(): void;

View File

@@ -8,22 +8,10 @@ import {
GroupNotifyMsgType,
NTGroupRequestOperateTypes,
KickMemberV2Req,
GroupDetailInfoV2Param,
GroupExtInfo,
GroupExtFilter,
} from '@/core/types';
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService {
modifyGroupExtInfoV2(groupExtInfo: GroupExtInfo, groupExtFilter: GroupExtFilter): Promise<GeneralCallResult &
{
result: {
groupCode: string,
result: number
}
}>;
// --->
// 待启用 For Next Version 3.2.0
// isTroopMember ? 0 : 111
@@ -181,9 +169,6 @@ export interface NodeIKernelGroupService {
modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
// 第二个参数在大多数情况为0 设置群成员权限 例如上传群文件权限和群成员付费/加入邀请加入时为8
modifyGroupDetailInfoV2(param: GroupDetailInfoV2Param, arg: number): Promise<GeneralCallResult>;
setGroupMsgMask(groupCode: string, arg: unknown): void;
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void;
@@ -264,7 +249,7 @@ export interface NodeIKernelGroupService {
reqToJoinGroup(groupCode: string, arg: unknown): void;
setGroupShutUp(groupCode: string, shutUp: boolean): Promise<GeneralCallResult>;
setGroupShutUp(groupCode: string, shutUp: boolean): void;
getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>;

View File

@@ -148,11 +148,10 @@ export interface NodeIKernelMsgService {
msgList: RawMessage[]
}>;
// getMsgService/getMsgs { chatType: 2, peerUid: '975206796', privilegeFlag: 336068800 } 0 20 true
getMsgs(peer: Peer & { privilegeFlag: number }, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>;
//@deprecated
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
//@deprecated
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>;
@@ -426,20 +425,7 @@ export interface NodeIKernelMsgService {
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown;
downloadRichMedia(args: {
fileModelId: string,
downSourceType: number,
triggerType: number,
msgId: string,
chatType: number,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
} & {
downloadSourceType: number, //33800左右一下的老版本 新版34606已经完全上面格式
}): unknown;
downloadRichMedia(...args: unknown[]): unknown;
getFirstUnreadMsgSeq(args: {
peerUid: string

View File

@@ -1,5 +1,5 @@
import { AnyCnameRecord } from 'node:dns';
import { BizKey, ModifyProfileParams, NodeIKernelProfileListener, ProfileBizType, SimpleInfo, UserDetailInfoByUin, UserDetailInfoListenerArg, UserDetailSource } from '@/core';
import { BizKey, ModifyProfileParams, NodeIKernelProfileListener, ProfileBizType, SimpleInfo, UserDetailInfoByUin, UserDetailSource } from '@/core';
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelProfileService {
@@ -15,13 +15,7 @@ export interface NodeIKernelProfileService {
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>;
fetchUserDetailInfo(trace: string, uids: string[], source: UserDetailSource, bizType: ProfileBizType[]): Promise<GeneralCallResult &
{
source: UserDetailSource,
// uid -> detail
detail: Map<string, UserDetailInfoListenerArg>,
}
>;
fetchUserDetailInfo(trace: string, uids: string[], source: UserDetailSource, bizType: ProfileBizType[]): Promise<GeneralCallResult>;
addKernelProfileListener(listener: NodeIKernelProfileListener): number;

View File

@@ -198,29 +198,9 @@ export interface NodeIKernelRichMediaService {
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
moveGroupFile(groupCode: string, busId: Array<number>, fileList: Array<string>, currentParentDirectory: string, targetParentDirectory: string): Promise<GeneralCallResult & {
moveGroupFileResult: {
result: {
retCode: number,
retMsg: symbol,
clientWording: string
},
successFileIdList: Array<string>,
failFileIdList: Array<string>
}
}>;
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
transGroupFile(groupCode: string, fileId: string): Promise<GeneralCallResult & {
transGroupFileResult: {
result: {
retCode: number
retMsg: string
clientWording: string
}
saveBusId: number
saveFilePath: string
}
}>;
transGroupFile(arg1: unknown, arg2: unknown): unknown;
searchGroupFile(
keywords: Array<string>,

View File

@@ -16,16 +16,6 @@ export * from './NodeIKernelDbToolsService';
export * from './NodeIKernelTipOffService';
export * from './NodeIKernelSearchService';
export * from './NodeIKernelCollectionService';
export * from './NodeIKernelAlbumService';
export * from './NodeIKernelECDHService';
export * from './NodeIKernelNodeMiscService';
export * from './NodeIKernelMsgBackupService';
export * from './NodeIKernelTianShuService';
export * from './NodeIKernelUnitedConfigService';
export * from './NodeIkernelTestPerformanceService';
export * from './NodeIKernelUixConvertService';
export * from './NodeIKernelMSFService';
export * from './NodeIKernelRecentContactService';
import type {
NodeIKernelAvatarService,
@@ -46,19 +36,8 @@ import type {
NodeIKernelTicketService,
NodeIKernelTipOffService,
} from '.';
import { NodeIKernelAlbumService } from './NodeIKernelAlbumService';
import { NodeIKernelECDHService } from './NodeIKernelECDHService';
import { NodeIKernelNodeMiscService } from './NodeIKernelNodeMiscService';
import { NodeIKernelMsgBackupService } from './NodeIKernelMsgBackupService';
import { NodeIKernelTianShuService } from './NodeIKernelTianShuService';
import { NodeIKernelUnitedConfigService } from './NodeIKernelUnitedConfigService';
import { NodeIkernelTestPerformanceService } from './NodeIkernelTestPerformanceService';
import { NodeIKernelUixConvertService } from './NodeIKernelUixConvertService';
import { NodeIKernelMSFService } from './NodeIKernelMSFService';
import { NodeIKernelRecentContactService } from './NodeIKernelRecentContactService';
export type ServiceNamingMapping = {
NodeIKernelAlbumService: NodeIKernelAlbumService;
NodeIKernelAvatarService: NodeIKernelAvatarService;
NodeIKernelBuddyService: NodeIKernelBuddyService;
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
@@ -74,15 +53,6 @@ export type ServiceNamingMapping = {
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
NodeIKernelTipOffService: NodeIKernelTipOffService;
NodeIKernelSearchService: NodeIKernelSearchService;
NodeIKernelSearchService: NodeIKernelSearchService,
NodeIKernelCollectionService: NodeIKernelCollectionService;
NodeIKernelECDHService: NodeIKernelECDHService;
NodeIKernelNodeMiscService: NodeIKernelNodeMiscService;
NodeIKernelMsgBackupService: NodeIKernelMsgBackupService;
NodeIKernelTianShuService: NodeIKernelTianShuService;
NodeIKernelUnitedConfigService: NodeIKernelUnitedConfigService;
NodeIkernelTestPerformanceService: NodeIkernelTestPerformanceService;
NodeIKernelUixConvertService: NodeIKernelUixConvertService;
NodeIKernelMSFService: NodeIKernelMSFService;
NodeIKernelRecentContactService: NodeIKernelRecentContactService;
};

View File

@@ -1,15 +1,4 @@
import {
ElementType,
MessageElement,
NTGrayTipElementSubTypeV2,
PicSubType,
PicType,
TipAioOpGrayTipElement,
TipGroupElement,
NTVideoType,
FaceType,
Peer
} from './msg';
import { ElementType, MessageElement, NTGrayTipElementSubTypeV2, PicSubType, PicType, TipAioOpGrayTipElement, TipGroupElement, NTVideoType, FaceType } from './msg';
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
@@ -58,7 +47,6 @@ export interface GrayTipRovokeElement {
operatorUid: string;
operatorNick: string;
operatorRemark: string;
isSelfOperate: boolean; // 是否是自己撤回的
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
@@ -225,9 +213,6 @@ export interface ReplyElement {
senderUidStr?: string;
replyMsgTime?: string;
replyMsgClientSeq?: string;
// HACK: Attributes that were not originally available,
// but were added due to NTQQ and NapCat's internal implementation, are used to supplement NapCat
_replyMsgPeer?: Peer;
}
export interface CalendarElement {

View File

@@ -1,97 +1,4 @@
import { QQLevel, NTSex } from './user';
export interface GroupExtInfo {
groupCode: string;
resultCode: number;
extInfo: EXTInfo;
}
export interface GroupExtFilter {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: number;
lightCharNum: number;
luckyWord: number;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: number;
groupOwnerId: number;
essentialMsgPrivilege: number;
msgEventSeq: number;
inviteRobotSwitch: number;
gangUpId: number;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: number;
groupBindGuildIds: number;
viewedMsgDisappearTime: number;
groupExtFlameData: number;
groupBindGuildSwitch: number;
groupAioBindGuildId: number;
groupExcludeGuildIds: number;
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: number;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
};
export interface EXTInfo {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: string;
lightCharNum: number;
luckyWord: string;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: string;
groupOwnerId: GroupOwnerID;
essentialMsgPrivilege: number;
msgEventSeq: string;
inviteRobotSwitch: number;
gangUpId: string;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: string;
groupBindGuildIds: GroupGuildIDS;
viewedMsgDisappearTime: string;
groupExtFlameData: GroupEXTFlameData;
groupBindGuildSwitch: number;
groupAioBindGuildId: string;
groupExcludeGuildIds: GroupGuildIDS;
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: string;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
}
export interface GroupGuildIDS {
guildIds: any[];
}
export interface GroupEXTFlameData {
switchState: number;
state: number;
dayNums: any[];
version: number;
updateTime: string;
isDisplayDayNum: boolean;
}
export interface GroupOwnerID {
memberUin: string;
memberUid: string;
memberQid: string;
}
export interface KickMemberInfo {
optFlag: number;
@@ -100,185 +7,6 @@ export interface KickMemberInfo {
optBytesMsg: string;
}
export interface GroupDetailInfoV2Param {
groupCode: string;
filter: Filter;
modifyInfo: ModifyInfo;
}
export interface Filter {
noCodeFingerOpenFlag: number;
noFingerOpenFlag: number;
groupName: number;
classExt: number;
classText: number;
fingerMemo: number;
richFingerMemo: number;
tagRecord: number;
groupGeoInfo: FilterGroupGeoInfo;
groupExtAdminNum: number;
flag: number;
groupMemo: number;
groupAioSkinUrl: number;
groupBoardSkinUrl: number;
groupCoverSkinUrl: number;
groupGrade: number;
activeMemberNum: number;
certificationType: number;
certificationText: number;
groupNewGuideLines: FilterGroupNewGuideLines;
groupFace: number;
addOption: number;
shutUpTime: number;
groupTypeFlag: number;
appPrivilegeFlag: number;
appPrivilegeMask: number;
groupExtOnly: GroupEXTOnly;
groupSecLevel: number;
groupSecLevelInfo: number;
subscriptionUin: number;
subscriptionUid: string;
allowMemberInvite: number;
groupQuestion: number;
groupAnswer: number;
groupFlagExt3: number;
groupFlagExt3Mask: number;
groupOpenAppid: number;
rootId: number;
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
hlGuildOrgId: number;
groupFlagExt4: number;
groupFlagExt4Mask: number;
groupSchoolInfo: FilterGroupSchoolInfo;
groupCardPrefix: FilterGroupCardPrefix;
allianceId: number;
groupFlagPro1: number;
groupFlagPro1Mask: number;
}
export interface FilterGroupCardPrefix {
introduction: number;
rptPrefix: number;
}
export interface GroupEXTOnly {
tribeId: number;
moneyForAddGroup: number;
}
export interface FilterGroupGeoInfo {
ownerUid: number;
setTime: number;
cityId: number;
longitude: number;
latitude: number;
geoContent: number;
poiId: number;
}
export interface FilterGroupNewGuideLines {
enabled: number;
content: number;
}
export interface FilterGroupSchoolInfo {
location: number;
grade: number;
school: number;
}
export interface ModifyInfo {
noCodeFingerOpenFlag: number;
noFingerOpenFlag: number;
groupName: string;
classExt: number;
classText: string;
fingerMemo: string;
richFingerMemo: string;
tagRecord: any[];
groupGeoInfo: ModifyInfoGroupGeoInfo;
groupExtAdminNum: number;
flag: number;
groupMemo: string;
groupAioSkinUrl: string;
groupBoardSkinUrl: string;
groupCoverSkinUrl: string;
groupGrade: number;
activeMemberNum: number;
certificationType: number;
certificationText: string;
groupNewGuideLines: ModifyInfoGroupNewGuideLines;
groupFace: number;
addOption: number;// 0 空设置 1 任何人都可以进入 2 需要管理员批准 3 不允许任何人入群 4 问题进入答案 5 问题管理员批准
shutUpTime: number;
groupTypeFlag: number;
appPrivilegeFlag: number;
// 需要管理员审核
// 0000 0000 0000 0000 0000 0000 0000
// 无需审核入群
// 0000 0001 0000 0000 0000 0000 0000
// 成员数100内无审核
// 0100 0000 0000 0000 0000 0000 0000
// 禁用 群成员邀请好友
// 0100 0000 0000 0000 0000 0000 0000
appPrivilegeMask: number;
// 0110 0001 0000 0000 0000 0000 0000
// 101711872
groupExtOnly: GroupEXTOnly;
groupSecLevel: number;
groupSecLevelInfo: number;
subscriptionUin: string;
subscriptionUid: string;
allowMemberInvite: number;
groupQuestion: string;
groupAnswer: string;
groupFlagExt3: number;
groupFlagExt3Mask: number;
groupOpenAppid: number;
rootId: string;
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
hlGuildOrgId: number;
groupFlagExt4: number;
groupFlagExt4Mask: number;
groupSchoolInfo: ModifyInfoGroupSchoolInfo;
groupCardPrefix: ModifyInfoGroupCardPrefix;
allianceId: string;
groupFlagPro1: number;
groupFlagPro1Mask: number;
}
export interface ModifyInfoGroupCardPrefix {
introduction: string;
rptPrefix: any[];
}
export interface ModifyInfoGroupGeoInfo {
ownerUid: string;
SetTime: number;
CityId: number;
Longitude: string;
Latitude: string;
GeoContent: string;
poiId: string;
}
export interface ModifyInfoGroupNewGuideLines {
enabled: boolean;
content: string;
}
export interface ModifyInfoGroupSchoolInfo {
location: string;
grade: number;
school: string;
}
// 获取群详细信息的来源类型
export enum GroupInfoSource {
KUNSPECIFIED,

View File

@@ -403,7 +403,7 @@ export interface NTGroupGrayMember {
}
/**
* 群灰色提示邀请者和被邀请者接口
*
*
* */
export interface NTGroupGrayInviterAndInvite {
invited: NTGroupGrayMember;
@@ -501,15 +501,13 @@ export interface RawMessage {
elements: MessageElement[];// 消息元素
sourceType: MsgSourceType;// 消息来源类型
isOnlineMsg: boolean;// 是否为在线消息
clientSeq?: string;
}
/**
* 查询消息参数接口
*/
export interface QueryMsgsParams {
chatInfo: Peer & { privilegeFlag?: number };
//searchFields: number;
chatInfo: Peer;
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
filterSendersUid: string[];
filterMsgFromTime: string;
@@ -567,4 +565,4 @@ export enum FaceType {
AniSticke = 3, // 动画贴纸
Lottie = 4,// 新格式表情
Poke = 5 // 可变Poke
}
}

View File

@@ -132,26 +132,18 @@ export enum BuddyReqType {
KMEINITIATORWAITPEERCONFIRM = 13
}
// 其中 ? 代表新版本参数
export interface FriendRequest {
isBuddy?: boolean;
isInitiator?: boolean;
isDecide: boolean;
friendUid: string;
reqType: BuddyReqType,
reqTime: string; // 时间戳 秒
flag?: number; // 0
preGroupingId?: number; // 0
commFriendNum?: number; // 共同好友数
extWords: string; // 申请人填写的验证消息
isUnread: boolean;
isDoubt?: boolean; // 是否是可疑的好友请求
nameMore?: string;
friendNick: string;
sourceId: number;
groupCode: string;
isBuddy?: boolean;
isAgreed?: boolean;
relation?: number;
groupCode: string
}
export interface FriendRequestNotify {

View File

@@ -115,7 +115,7 @@ export interface GroupEssenceMsg {
add_digest_uin: string;
add_digest_nick: string;
add_digest_time: number;
msg_content: { msg_type: number, text?: string, image_url?: string }[];
msg_content: unknown[];
can_be_removed: true;
}

View File

@@ -9,8 +9,6 @@ import { NodeIKernelLoginService } from '@/core/services';
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui';
import { NapCatOneBot11Adapter } from '@/onebot';
import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg';
import { FFmpegService } from '@/common/ffmpeg';
//Framework ES入口文件
export async function getWebUiUrl() {
@@ -38,22 +36,7 @@ export async function NCoreInitFramework(
const logger = new LogWrapper(pathWrapper.logsPath);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
if (reset && path) {
FFmpegService.setFfmpegPath(path, logger);
}
}).catch(e => {
logger.logError('[Ffmpeg] Error:', e);
});
}
//直到登录成功后,执行下一步
// const selfInfo = {
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',
// uin: '3684714082',
// nick: '',
// online: true
// }
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener();
loginListener.onQRCodeLoginSucceed = async (loginResult) => {

View File

@@ -1,26 +0,0 @@
const fs = require('fs');
const path = require('path');
async function initializeNapCat(session, loginService, registerCallback) {
//const logFile = path.join(currentPath, 'napcat.log');
console.log('[NapCat] [Info] 开始初始化NapCat');
//fs.writeFileSync(logFile, '', { flag: 'w' });
//fs.writeFileSync(logFile, '[NapCat] [Info] NapCat 初始化成功\n', { flag: 'a' });
try {
const currentPath = path.dirname(__filename);
const { NCoreInitFramework } = await import('file://' + path.join(currentPath, './napcat.mjs'));
await NCoreInitFramework(session, loginService, (callback) => { registerCallback(callback) });
} catch (error) {
console.log('[NapCat] [Error] 初始化NapCat', error);
//fs.writeFileSync(logFile, `[NapCat] [Error] 初始化NapCat失败: ${error.message}\n`, { flag: 'a' });
}
}
module.exports = {
initializeNapCat: initializeNapCat
};

View File

@@ -1,426 +0,0 @@
import * as fs from 'fs';
import { ReadStream } from 'fs';
export interface ImageSize {
width: number;
height: number;
}
export enum ImageType {
JPEG = 'jpeg',
PNG = 'png',
BMP = 'bmp',
GIF = 'gif',
WEBP = 'webp',
UNKNOWN = 'unknown',
}
interface ImageParser {
readonly type: ImageType;
canParse(buffer: Buffer): boolean;
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
}
// 魔术匹配
function matchMagic(buffer: Buffer, magic: number[], offset = 0): boolean {
if (buffer.length < offset + magic.length) {
return false;
}
for (let i = 0; i < magic.length; i++) {
if (buffer[offset + i] !== magic[i]) {
return false;
}
}
return true;
}
// PNG解析器
class PngParser implements ImageParser {
readonly type = ImageType.PNG;
// PNG 魔术头89 50 4E 47 0D 0A 1A 0A
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
canParse(buffer: Buffer): boolean {
return matchMagic(buffer, this.PNG_SIGNATURE);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(24) as Buffer;
if (!buf || buf.length < 24) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32BE(16);
const height = buf.readUInt32BE(20);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// JPEG解析器
class JpegParser implements ImageParser {
readonly type = ImageType.JPEG;
// JPEG 魔术头FF D8
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
// JPEG标记常量
private readonly SOF_MARKERS = {
SOF0: 0xC0, // 基线DCT
SOF1: 0xC1, // 扩展顺序DCT
SOF2: 0xC2, // 渐进式DCT
SOF3: 0xC3, // 无损
} as const;
// 非SOF标记
private readonly NON_SOF_MARKERS: number[] = [
0xC4, // DHT
0xC8, // JPEG扩展
0xCC, // DAC
] as const;
canParse(buffer: Buffer): boolean {
return matchMagic(buffer, this.JPEG_SIGNATURE);
}
isSOFMarker(marker: number): boolean {
return (
marker === this.SOF_MARKERS.SOF0 ||
marker === this.SOF_MARKERS.SOF1 ||
marker === this.SOF_MARKERS.SOF2 ||
marker === this.SOF_MARKERS.SOF3
);
}
isNonSOFMarker(marker: number): boolean {
return this.NON_SOF_MARKERS.includes(marker);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise<ImageSize | undefined>((resolve, reject) => {
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
let buffer = Buffer.alloc(0);
let offset = 0;
let found = false;
// 处理错误
stream.on('error', (err) => {
stream.destroy();
reject(err);
});
// 处理数据块
stream.on('data', (chunk: Buffer | string) => {
// 追加新数据到缓冲区
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
offset = 0;
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
const bufferSize = buffer.length;
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
// 从JPEG头部后开始扫描
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
// 寻找FF标记
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
const marker = buffer[offset + 1];
if (!marker) {
break;
}
// 跳过非SOF标记
if (this.isNonSOFMarker(marker)) {
offset += 2;
continue;
}
// 处理SOF标记 (包含尺寸信息)
if (this.isSOFMarker(marker)) {
// 确保缓冲区中有足够数据读取尺寸
if (offset + 9 < bufferSize) {
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
const height = buffer.readUInt16BE(offset + 5);
const width = buffer.readUInt16BE(offset + 7);
found = true;
stream.destroy();
resolve({ width, height });
return;
} else {
// 如果缓冲区内数据不够,保留当前位置等待更多数据
break;
}
}
}
offset++;
}
// 缓冲区管理: 如果处理了许多数据但没找到标记,
// 保留最后N字节用于跨块匹配丢弃之前的数据
if (offset > BUFFER_SIZE) {
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
if (offset > KEEP_BYTES) {
buffer = buffer.subarray(offset - KEEP_BYTES);
offset = KEEP_BYTES;
}
}
});
// 处理流结束
stream.on('end', () => {
if (!found) {
resolve(undefined);
}
});
});
}
}
// BMP解析器
class BmpParser implements ImageParser {
readonly type = ImageType.BMP;
// BMP 魔术头42 4D (BM)
private readonly BMP_SIGNATURE = [0x42, 0x4D];
canParse(buffer: Buffer): boolean {
return matchMagic(buffer, this.BMP_SIGNATURE);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(26) as Buffer;
if (!buf || buf.length < 26) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32LE(18);
const height = buf.readUInt32LE(22);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// GIF解析器
class GifParser implements ImageParser {
readonly type = ImageType.GIF;
// GIF87a 魔术头47 49 46 38 37 61
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
// GIF89a 魔术头47 49 46 38 39 61
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
canParse(buffer: Buffer): boolean {
return (
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
matchMagic(buffer, this.GIF89A_SIGNATURE)
);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(10) as Buffer;
if (!buf || buf.length < 10) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt16LE(6);
const height = buf.readUInt16LE(8);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
class WebpParser implements ImageParser {
readonly type = ImageType.WEBP;
// WEBP RIFF 头52 49 46 46 (RIFF)
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
// WEBP 魔术头57 45 42 50 (WEBP)
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
// WEBP 块头
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
canParse(buffer: Buffer): boolean {
return (
buffer.length >= 12 &&
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
);
}
isChunkType(buffer: Buffer, offset: number, chunkType: number[]): boolean {
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
// 需要读取足够的字节来检测所有三种格式
const MAX_HEADER_SIZE = 32;
let totalBytes = 0;
let buffer = Buffer.alloc(0);
stream.on('error', reject);
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer, chunkBuffer]);
totalBytes += chunk.length;
// 检查是否有足够的字节进行格式检测
if (totalBytes >= MAX_HEADER_SIZE) {
stream.destroy();
// 检查基本的WEBP签名
if (!this.canParse(buffer)) {
return resolve(undefined);
}
// 检查chunk头部位于字节12-15
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
// VP8格式 - 标准WebP
// 宽度和高度在帧头中
const width = buffer.readUInt16LE(26) & 0x3FFF;
const height = buffer.readUInt16LE(28) & 0x3FFF;
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
// VP8L格式 - 无损WebP
// 1字节标记后是14位宽度和14位高度
const bits = buffer.readUInt32LE(21);
const width = 1 + (bits & 0x3FFF);
const height = 1 + ((bits >> 14) & 0x3FFF);
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
// VP8X格式 - 扩展WebP
// 24位宽度和高度(减去1)
if (!buffer[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) {
return resolve(undefined);
}
const width = 1 + ((buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) & 0xFFFFFF);
const height = 1 + ((buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) & 0xFFFFFF);
return resolve({ width, height });
} else {
// 未知的WebP子格式
return resolve(undefined);
}
}
});
stream.on('end', () => {
// 如果没有读到足够的字节
if (totalBytes < MAX_HEADER_SIZE) {
resolve(undefined);
}
});
});
}
}
const parsers: ReadonlyArray<ImageParser> = [
new PngParser(),
new JpegParser(),
new BmpParser(),
new GifParser(),
new WebpParser(),
];
export async function detectImageType(filePath: string): Promise<ImageType> {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filePath, {
highWaterMark: 64, // 优化读取buffer大小
start: 0,
end: 63
});
let buffer: Buffer | null = null;
stream.once('error', (err) => {
stream.destroy();
reject(err);
});
stream.once('readable', () => {
buffer = stream.read(64) as Buffer;
stream.destroy();
if (!buffer) {
return resolve(ImageType.UNKNOWN);
}
for (const parser of parsers) {
if (parser.canParse(buffer)) {
return resolve(parser.type);
}
}
resolve(ImageType.UNKNOWN);
});
stream.once('end', () => {
if (!buffer) {
resolve(ImageType.UNKNOWN);
}
});
});
}
export async function imageSizeFromFile(filePath: string): Promise<ImageSize | undefined> {
try {
// 先检测类型
const type = await detectImageType(filePath);
const parser = parsers.find(p => p.type === type);
if (!parser) {
return undefined;
}
// 用流式方式解析尺寸
const stream = fs.createReadStream(filePath);
try {
return await parser.parseSize(stream);
} catch (err) {
console.error(`解析图片尺寸出错: ${err}`);
return undefined;
} finally {
if (!stream.destroyed) {
stream.destroy();
}
}
} catch (err) {
console.error(`检测图片类型出错: ${err}`);
return undefined;
}
}
export async function imageSizeFallBack(
filePath: string,
fallback: ImageSize = {
width: 1024,
height: 1024,
}
): Promise<ImageSize> {
return await imageSizeFromFile(filePath) ?? fallback;
}

View File

@@ -1,33 +0,0 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
current_parent_directory: Type.String(),
target_parent_directory: Type.String(),
});
type Payload = Static<typeof SchemaData>;
interface MoveGroupFileResponse {
ok: boolean;
}
export class MoveGroupFile extends GetPacketStatusDepends<Payload, MoveGroupFileResponse> {
override actionName = ActionName.MoveGroupFile;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
return {
ok: true,
};
}
throw new Error('real fileUUID not found!');
}
}

View File

@@ -1,33 +0,0 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
current_parent_directory: Type.String(),
new_name: Type.String(),
});
type Payload = Static<typeof SchemaData>;
interface RenameGroupFileResponse {
ok: boolean;
}
export class RenameGroupFile extends GetPacketStatusDepends<Payload, RenameGroupFileResponse> {
override actionName = ActionName.RenameGroupFile;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
return {
ok: true,
};
}
throw new Error('real fileUUID not found!');
}
}

View File

@@ -1,28 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
add_type: Type.Number(),
group_question: Type.Optional(Type.String()),
group_answer: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupAddOption extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupAddOption;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupAddOption(payload.group_id, {
addOption: payload.add_type,
groupQuestion: payload.group_question,
groupAnswer: payload.group_answer,
});
if (ret.result != 0) {
throw new Error(`设置群添加选项失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -1,23 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
user_id: Type.Array(Type.String()),
reject_add_request: Type.Optional(Type.Union([Type.Boolean(), Type.String()])),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupKickMembers extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupKickMembers;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
const rejectReq = payload.reject_add_request?.toString() == 'true';
const uids: string[] = await Promise.all(payload.user_id.map(async uin => await this.core.apis.UserApi.getUidByUinV2(uin)));
await this.core.apis.GroupApi.kickMember(payload.group_id.toString(), uids.filter(uid => !!uid), rejectReq);
return null;
}
}

View File

@@ -1,27 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
robot_member_switch: Type.Optional(Type.Number()),
robot_member_examine: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupRobotAddOption extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupRobotAddOption;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupRobotAddOption(
payload.group_id,
payload.robot_member_switch,
payload.robot_member_examine,
);
if (ret.result != 0) {
throw new Error(`设置群机器人添加选项失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -1,26 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
no_code_finger_open: Type.Optional(Type.Number()),
no_finger_open: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupSearch extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupSearch;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupSearch(payload.group_id, {
noCodeFingerOpenFlag: payload.no_code_finger_open,
noFingerOpenFlag: payload.no_finger_open,
});
if (ret.result != 0) {
throw new Error(`设置群搜索失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -1,34 +0,0 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
file_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
interface TransGroupFileResponse {
ok: boolean;
}
export class TransGroupFile extends GetPacketStatusDepends<Payload, TransGroupFileResponse> {
override actionName = ActionName.TransGroupFile;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (contextMsgFile?.fileUUID) {
const result = await this.core.apis.GroupApi.transGroupFile(payload.group_id.toString(), contextMsgFile.fileUUID);
if (result.transGroupFileResult.result.retCode === 0) {
return {
ok: true
};
}
throw new Error(result.transGroupFileResult.result.retMsg);
}
throw new Error('real fileUUID not found!');
}
}

View File

@@ -4,10 +4,7 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
// 兼容gocq 与name二选一
folder_name: Type.Optional(Type.String()),
// 兼容gocq 与folder_name二选一
name: Type.Optional(Type.String()),
folder_name: Type.String(),
});
type Payload = Static<typeof SchemaData>;
@@ -19,7 +16,6 @@ export class CreateGroupFileFolder extends OneBotAction<Payload, ResponseType>
override actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const folderName = payload.folder_name || payload.name;
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), folderName!)).resultWithGroupItem;
return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
}
}

View File

@@ -4,14 +4,14 @@ import { ActionName } from '@/onebot/action/router';
import { MessageUnique } from '@/common/message-unique';
import { Static, Type } from '@sinclair/typebox';
import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
import { isNumeric } from '@/common/helper';
const SchemaData = Type.Object({
message_id: Type.Optional(Type.String()),
id: Type.Optional(Type.String()),
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
});
type Payload = Static<typeof SchemaData>;
export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
messages: OB11Message[] | undefined;
}> {
@@ -53,21 +53,19 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
}
async _handle(payload: Payload) {
// 1. 检查消息ID是否存在
const msgId = payload.message_id || payload.id;
if (!msgId) {
throw new Error('message_id is required');
}
// 2. 定义辅助函数 - 创建伪转发消息对象
const createFakeForwardMsg = (resId: string): RawMessage => {
const fakeForwardMsg = (res_id: string) => {
return {
chatType: ChatType.KCHATTYPEGROUP,
elements: [{
elementType: ElementType.MULTIFORWARD,
elementId: '',
multiForwardMsgElement: {
resId: resId,
resId: res_id,
fileName: '',
xmlContent: '',
}
@@ -98,9 +96,8 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
} as RawMessage;
};
// 3. 定义协议回退逻辑函数
const protocolFallbackLogic = async (resId: string) => {
const ob = (await this.obContext.apis.MsgApi.parseMessageV2(createFakeForwardMsg(resId)))?.arrayMsg;
const protocolFallbackLogic = async (res_id: string) => {
const ob = (await this.obContext.apis.MsgApi.parseMessageV2(fakeForwardMsg(res_id)))?.arrayMsg;
if (ob) {
return {
messages: (ob?.message?.[0] as OB11MessageForward)?.data?.content
@@ -108,37 +105,31 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
}
throw new Error('protocolFallbackLogic: 找不到相关的聊天记录');
};
// 4. 尝试通过正常渠道获取消息
// 如果是数字ID优先使用getMsgsByMsgId获取
if (!isNumeric(msgId)) {
let ret = await protocolFallbackLogic(msgId);
if (ret.messages) {
return ret;
}
throw new Error('ResId无效: 找不到相关的聊天记录');
}
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
if (rootMsg) {
// 5. 获取消息内容
const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]);
if (data && data.result === 0 && data.msgList.length > 0) {
const singleMsg = data.msgList[0];
if (!singleMsg) {
throw new Error('消息不存在或已过期');
}
// 6. 解析消息内容
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;
const forwardContent = (resMsg?.message?.[0] as OB11MessageForward)?.data?.content;
if (forwardContent) {
return { messages: forwardContent };
}
}
if (!rootMsg) {
return await protocolFallbackLogic(msgId.toString());
}
// 说明消息已过期或者为内层消息 NapCat 一次返回不处理内层消息
throw new Error('消息已过期或者为内层消息,无法获取转发消息');
const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]);
if (!data || data.result !== 0) {
return await protocolFallbackLogic(msgId.toString());
}
const singleMsg = data.msgList[0];
if (!singleMsg) {
return await protocolFallbackLogic(msgId.toString());
}
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
return await protocolFallbackLogic(msgId.toString());
}
return {
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
};
//}
// return { message: resMsg };
}
}

View File

@@ -11,10 +11,10 @@ interface Response {
messages: OB11Message[];
}
const SchemaData = Type.Object({
user_id: Type.String(),
message_seq: Type.Optional(Type.String()),
count: Type.Number({ default: 20 }),
reverseOrder: Type.Boolean({ default: false })
user_id: Type.Union([Type.Number(), Type.String()]),
message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
});
@@ -27,14 +27,18 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
async _handle(payload: Payload, _adapter: string, config: NetworkAdapterConfig): Promise<Response> {
//处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
if (!uid) throw new Error(`记录${payload.user_id}不存在`);
const friend = await this.core.apis.FriendApi.isBuddy(uid);
const peer = { chatType: friend ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid: uid };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ?
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count, payload.reverseOrder)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
//翻转消息
if (isReverseOrder) msgList.reverse();
//转换序号
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);

View File

@@ -11,10 +11,10 @@ interface Response {
}
const SchemaData = Type.Object({
group_id: Type.String(),
message_seq: Type.Optional(Type.String()),
count: Type.Number({ default: 20 }),
reverseOrder: Type.Boolean({ default: false })
group_id: Type.Union([Type.Number(), Type.String()]),
message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
});
@@ -26,13 +26,17 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
override payloadSchema = SchemaData;
async _handle(payload: Payload, _adapter: string, config: NetworkAdapterConfig): Promise<Response> {
//处理参数
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
//拉取消息
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ?
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count, payload.reverseOrder)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
//翻转消息
if (isReverseOrder) msgList.reverse();
//转换序号
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);

View File

@@ -3,7 +3,7 @@ import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
model: Type.Optional(Type.String()),
model: Type.String(),
});
type Payload = Static<typeof SchemaData>;

View File

@@ -1,4 +1,4 @@
import { ContextMode, normalize, ReturnDataType, SendMsgBase } from '@/onebot/action/msg/SendMsg';
import { normalize, SendMsgBase } from '@/onebot/action/msg/SendMsg';
import { OB11PostSendMsg } from '@/onebot/types';
import { ActionName } from '@/onebot/action/router';
@@ -19,14 +19,8 @@ export class GoCQHTTPSendForwardMsg extends GoCQHTTPSendForwardMsgBase {
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsgBase {
override actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
return this.base_handle(payload, ContextMode.Private);
}
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsgBase {
override actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
return this.base_handle(payload, ContextMode.Group);
}
}

View File

@@ -38,7 +38,6 @@ export default class GoCQHTTPUploadGroupFile extends OneBotAction<Payload, null>
deleteAfterSentFiles: []
};
const sendFileEle = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, payload.folder ?? payload.folder_id);
msgContext.deleteAfterSentFiles.push(downloadResult.path);
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], msgContext.deleteAfterSentFiles);
return null;
}

View File

@@ -23,7 +23,7 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction<Payload, nul
if (payload.user_id) {
const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!peerUid) {
throw new Error(`私聊${payload.user_id}不存在`);
throw new Error( `私聊${payload.user_id}不存在`);
}
const isBuddy = await this.core.apis.FriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid };
@@ -48,7 +48,6 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction<Payload, nul
deleteAfterSentFiles: []
};
const sendFileEle: SendFileElement = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name);
msgContext.deleteAfterSentFiles.push(downloadResult.path);
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], msgContext.deleteAfterSentFiles);
return null;
}

View File

@@ -1,27 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GetGroupDetailInfo extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetGroupDetailInfo;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
return {
...data,
group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
group_remark: '',
group_id: +payload.group_id,
group_name: data.groupName,
member_count: data.memberNum,
max_member_count: data.maxMemberNum,
};
}
}

View File

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

View File

@@ -20,7 +20,6 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
return {
...data,
group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
group_remark: '',
group_id: +payload.group_id,
group_name: data.groupName,

View File

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

View File

@@ -0,0 +1,19 @@
import { ActionName } from '@/onebot/action/router';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GroupPoke extends GetPacketStatusDepends<Payload, void> {
override actionName = ActionName.GroupPoke;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
}
}

View File

@@ -1,19 +1,17 @@
import { ContextMode, ReturnDataType, SendMsgBase } from '@/onebot/action/msg/SendMsg';
import { ContextMode, SendMsgBase } from '@/onebot/action/msg/SendMsg';
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import { OB11PostSendMsg } from '@/onebot/types';
// 未检测参数
class SendGroupMsg extends SendMsgBase {
override actionName = ActionName.SendGroupMsg;
override contextMode: ContextMode = ContextMode.Group;
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
delete payload.user_id;
payload.message_type = 'group';
return super.check(payload);
}
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
return this.base_handle(payload, ContextMode.Group);
}
}
export default SendGroupMsg;

View File

@@ -15,10 +15,7 @@ export default class SetGroupWholeBan extends OneBotAction<Payload, null> {
async _handle(payload: Payload): Promise<null> {
const enable = payload.enable?.toString() !== 'false';
let res = await this.core.apis.GroupApi.banGroup(payload.group_id.toString(), enable);
if (res.result !== 0) {
throw new Error(`SetGroupWholeBan failed: ${res.errMsg} ${res.result}`);
}
await this.core.apis.GroupApi.banGroup(payload.group_id.toString(), enable);
return null;
}
}

View File

@@ -78,6 +78,7 @@ import { GetGroupFileSystemInfo } from '@/onebot/action/go-cqhttp/GetGroupFileSy
import { GetGroupRootFiles } from '@/onebot/action/go-cqhttp/GetGroupRootFiles';
import { GetGroupFilesByFolder } from '@/onebot/action/go-cqhttp/GetGroupFilesByFolder';
import { GetGroupSystemMsg } from './system/GetSystemMsg';
import { GroupPoke } from './group/GroupPoke';
import { GetUserStatus } from './extends/GetUserStatus';
import { GetRkey } from './extends/GetRkey';
import { SetSpecialTitle } from './extends/SetSpecialTitle';
@@ -85,6 +86,7 @@ import { GetGroupShutList } from './group/GetGroupShutList';
import { GetGroupMemberList } from './group/GetGroupMemberList';
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
import { GetPacketStatus } from '@/onebot/action/packet/GetPacketStatus';
import { FriendPoke } from '@/onebot/action/user/FriendPoke';
import { GetCredentials } from './system/GetCredentials';
import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign';
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
@@ -100,43 +102,17 @@ import { GetGuildList } from './guild/GetGuildList';
import { GetGuildProfile } from './guild/GetGuildProfile';
import { GetClientkey } from './extends/GetClientkey';
import { SendPacket } from './extends/SendPacket';
import { FriendPoke, GroupPoke, SendPoke } from '@/onebot/action/packet/SendPoke';
import { SendPoke } from '@/onebot/action/packet/SendPoke';
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
import { BotExit } from './extends/BotExit';
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
import { GetPrivateFileUrl } from './file/GetPrivateFileUrl';
import { GetUnidirectionalFriendList } from './extends/GetUnidirectionalFriendList';
import SetGroupRemark from './extends/SetGroupRemark';
import { MoveGroupFile } from './extends/MoveGroupFile';
import { TransGroupFile } from './extends/TransGroupFile';
import { RenameGroupFile } from './extends/RenameGroupFile';
import { GetRkeyServer } from './packet/GetRkeyServer';
import { GetRkeyEx } from './packet/GetRkeyEx';
import { CleanCache } from './system/CleanCache';
import SetFriendRemark from './user/SetFriendRemark';
import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
import SetGroupAddOption from './extends/SetGroupAddOption';
import SetGroupSearch from './extends/SetGroupSearch';
import SetGroupRobotAddOption from './extends/SetGroupRobotAddOption';
import SetGroupKickMembers from './extends/SetGroupKickMembers';
import { GetGroupDetailInfo } from './group/GetGroupDetailInfo';
import GetGroupAddRequest from './extends/GetGroupAddRequest';
import { GetCollectionList } from './extends/GetCollectionList';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [
new GetGroupDetailInfo(obContext, core),
new SetGroupKickMembers(obContext, core),
new SetGroupAddOption(obContext, core),
new SetGroupRobotAddOption(obContext, core),
new SetGroupSearch(obContext, core),
new SetDoubtFriendsAddRequest(obContext, core),
new GetDoubtFriendsAddRequest(obContext, core),
new SetFriendRemark(obContext, core),
new GetRkeyEx(obContext, core),
new GetRkeyServer(obContext, core),
new SetGroupRemark(obContext, core),
new GetGroupInfoEx(obContext, core),
new FetchEmojiLike(obContext, core),
@@ -156,9 +132,6 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SetGroupSign(obContext, core),
new SendGroupSign(obContext, core),
new GetClientkey(obContext, core),
new MoveGroupFile(obContext, core),
new RenameGroupFile(obContext, core),
new TransGroupFile(obContext, core),
// onebot11
new SendLike(obContext, core),
new GetMsg(obContext, core),
@@ -258,9 +231,6 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new ClickInlineKeyboardButton(obContext, core),
new GetPrivateFileUrl(obContext, core),
new GetUnidirectionalFriendList(obContext, core),
new CleanCache(obContext, core),
new GetGroupAddRequest(obContext, core),
new GetCollectionList(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];

View File

@@ -19,7 +19,6 @@ import { rawMsgWithSendMsg } from '@/core/packet/message/converter';
export interface ReturnDataType {
message_id: number;
res_id?: string;
forward_id?: string;
}
export enum ContextMode {
@@ -39,7 +38,7 @@ export function normalize(message: OB11MessageMixType, autoEscape = false): OB11
export async function createContext(core: NapCatCore, payload: OB11PostContext | undefined, contextMode: ContextMode = ContextMode.Normal): Promise<Peer> {
if (!payload) {
throw new Error('请传递请求内容');
throw new Error('请指定 group_id 或 user_id');
}
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
@@ -49,16 +48,7 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext |
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!Uid) {
if (payload.group_id) {
return {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id.toString(),
guildId: ''
}
}
throw new Error('无法获取用户信息');
}
if (!Uid) throw new Error('无法获取用户信息');
const isBuddy = await core.apis.FriendApi.isBuddy(Uid);
if (!isBuddy) {
const ret = await core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, Uid);
@@ -88,13 +78,7 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext |
guildId: '',
};
}
if (contextMode === ContextMode.Private && payload.group_id) {
throw new Error('当前私聊发送,请指定 user_id 而不是 group_id');
}
if (contextMode === ContextMode.Group && payload.user_id) {
throw new Error('当前群聊发送,请指定 group_id 而不是 user_id');
}
throw new Error('请指定正确的 group_id 或 user_id');
throw new Error('请指定 group_id 或 user_id');
}
function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
@@ -105,6 +89,8 @@ function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType
}
export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
contextMode = ContextMode.Normal;
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = normalize(payload.message);
const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node);
@@ -116,13 +102,12 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
}
return { valid: true };
}
async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
return this.base_handle(payload);
}
async base_handle(payload: OB11PostSendMsg, contextMode: ContextMode = ContextMode.Normal): Promise<ReturnDataType> {
if (payload.message_type === 'group') contextMode = ContextMode.Group;
if (payload.message_type === 'private') contextMode = ContextMode.Private;
const peer = await createContext(this.core, payload, contextMode);
this.contextMode = ContextMode.Normal;
if (payload.message_type === 'group') this.contextMode = ContextMode.Group;
if (payload.message_type === 'private') this.contextMode = ContextMode.Private;
const peer = await createContext(this.core, payload, this.contextMode);
const messages = normalize(
payload.message,
@@ -148,10 +133,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
peerUid: peer.peerUid,
chatType: peer.chatType,
}, (returnMsgAndResId.message).msgId);
// 对gocq的forward_id进行兼容
const resId = returnMsgAndResId.res_id!;
return { message_id: msgShortId!, res_id: resId, forward_id: resId };
return { message_id: msgShortId!, res_id: returnMsgAndResId.res_id! };
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
throw Error(`发送转发消息res_id${returnMsgAndResId.res_id} 失败`);
}
@@ -177,11 +159,9 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
nickname: string,
}, dp: number = 0): Promise<{
finallySendElements: SendArkElement,
res_id?: string,
deleteAfterSentFiles: string[],
res_id?: string
} | null> {
const packetMsg: PacketMsg[] = [];
let delFiles: string[] = [];
for (const node of messageNodes) {
if (dp >= 3) {
this.core.context.logger.logWarn('转发消息深度超过3层将停止解析');
@@ -197,11 +177,9 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? 'QQ用户',
}, dp + 1);
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || []));
} else {
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
sendElements = sendElementsCreateReturn.sendElements;
delFiles.push(...sendElementsCreateReturn.deleteAfterSentFiles);
}
const packetMsgElements: rawMsgWithSendMsg = {
@@ -225,8 +203,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
if (msg) {
let msgCache = await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
delFiles.push(...msgCache);
await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg);
@@ -242,7 +219,6 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
return {
deleteAfterSentFiles: delFiles,
finallySendElements: {
elementType: ElementType.ARK,
elementId: '',
@@ -264,7 +240,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const res_id = uploadReturnData?.res_id;
const finallySendElements = uploadReturnData?.finallySendElements;
if (!finallySendElements) throw Error('转发消息失败,生成节点为空');
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], uploadReturnData.deleteAfterSentFiles || []).catch(() => undefined);
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], []).catch(() => undefined);
return { message: returnMsg ?? null, res_id: res_id! };
}

View File

@@ -1,18 +1,16 @@
import { ContextMode, ReturnDataType, SendMsgBase } from './SendMsg';
import { ContextMode, SendMsgBase } from './SendMsg';
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import { OB11PostSendMsg } from '@/onebot/types';
// 未检测参数
class SendPrivateMsg extends SendMsgBase {
override actionName = ActionName.SendPrivateMsg;
override contextMode: ContextMode = ContextMode.Private;
protected override async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
payload.message_type = 'private';
return super.check(payload);
}
override async _handle(payload: OB11PostSendMsg): Promise<ReturnDataType> {
return this.base_handle(payload, ContextMode.Private);
}
}
export default SendPrivateMsg;

View File

@@ -1,18 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
count: Type.Number({ default: 50 }),
});
type Payload = Static<typeof SchemaData>;
export class GetDoubtFriendsAddRequest extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetDoubtFriendsAddRequest;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.FriendApi.getDoubtFriendRequest(payload.count);
}
}

View File

@@ -1,21 +0,0 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
flag: Type.String(),
//注意强制String 非isNumeric 不遵守则不符合设计
approve: Type.Boolean({ default: true }),
//该字段没有语义 仅做保留 强制为True
});
type Payload = Static<typeof SchemaData>;
export class SetDoubtFriendsAddRequest extends OneBotAction<Payload, unknown> {
override actionName = ActionName.SetDoubtFriendsAddRequest;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.FriendApi.handleDoubtFriendRequest(payload.flag);
}
}

View File

@@ -1,18 +0,0 @@
import { ActionName } from '@/onebot/action/router';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
export class GetRkeyEx extends GetPacketStatusDepends<void, unknown> {
override actionName = ActionName.GetRkeyEx;
async _handle() {
let rkeys = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
return rkeys.map(rkey => {
return {
type: rkey.type === 10 ? "private" : "group",
rkey: rkey.rkey,
created_at: rkey.time,
ttl: rkey.ttl,
};
});
}
}

Some files were not shown because too many files have changed in this diff Show More