diff --git a/src/common/ffmpeg-adapter-factory.ts b/src/common/ffmpeg-adapter-factory.ts new file mode 100644 index 00000000..53e1b9d7 --- /dev/null +++ b/src/common/ffmpeg-adapter-factory.ts @@ -0,0 +1,131 @@ +/** + * FFmpeg Adapter Factory + * 自动检测并选择最佳的 FFmpeg 适配器 + */ + +import { LogWrapper } from './log'; +import { FFmpegAddonAdapter } from './ffmpeg-addon-adapter'; +import { FFmpegExecAdapter } from './ffmpeg-exec-adapter'; +import type { IFFmpegAdapter } from './ffmpeg-adapter-interface'; + +/** + * FFmpeg 适配器工厂 + */ +export class FFmpegAdapterFactory { + private static instance: IFFmpegAdapter | null = null; + private static initPromise: Promise | null = null; + + /** + * 初始化并获取最佳的 FFmpeg 适配器 + * @param logger 日志记录器 + * @param ffmpegPath FFmpeg 可执行文件路径(用于 Exec 适配器) + * @param ffprobePath FFprobe 可执行文件路径(用于 Exec 适配器) + * @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath,用于 Addon 适配器) + */ + static async getAdapter( + logger: LogWrapper, + ffmpegPath: string = 'ffmpeg', + ffprobePath: string = 'ffprobe', + binaryPath?: string + ): Promise { + // 如果已经初始化,直接返回 + if (this.instance) { + return this.instance; + } + + // 如果正在初始化,等待初始化完成 + if (this.initPromise) { + return this.initPromise; + } + + // 开始初始化 + this.initPromise = this.initialize(logger, ffmpegPath, ffprobePath, binaryPath); + + try { + this.instance = await this.initPromise; + return this.instance; + } finally { + this.initPromise = null; + } + } + + /** + * 初始化适配器 + */ + private static async initialize( + logger: LogWrapper, + ffmpegPath: string, + ffprobePath: string, + binaryPath?: string + ): Promise { + + // 1. 优先尝试使用 Native Addon + if (binaryPath) { + const addonAdapter = new FFmpegAddonAdapter(binaryPath); + + logger.log('[FFmpeg] 检查 Native Addon 可用性...'); + if (await addonAdapter.isAvailable()) { + logger.log('[FFmpeg] ✓ 使用 Native Addon 适配器'); + return addonAdapter; + } + + logger.log('[FFmpeg] Native Addon 不可用,尝试使用命令行工具'); + } else { + logger.log('[FFmpeg] 未提供 binaryPath,跳过 Native Addon 检测'); + } + + // 2. 降级到 execFile 实现 + const execAdapter = new FFmpegExecAdapter(ffmpegPath, ffprobePath, binaryPath, logger); + + logger.log(`[FFmpeg] 检查命令行工具可用性: ${ffmpegPath}`); + if (await execAdapter.isAvailable()) { + logger.log('[FFmpeg] 使用命令行工具适配器 ✓'); + return execAdapter; + } + + // 3. 都不可用,返回 execAdapter 但会在使用时报错 + logger.logError('[FFmpeg] 警告: FFmpeg 不可用,将使用命令行适配器但可能失败'); + return execAdapter; + } + + /** + * 重置适配器(用于测试或重新初始化) + */ + static reset(): void { + this.instance = null; + this.initPromise = null; + } + + /** + * 更新 FFmpeg 路径并重新初始化 + * @param logger 日志记录器 + * @param ffmpegPath FFmpeg 可执行文件路径 + * @param ffprobePath FFprobe 可执行文件路径 + */ + static async updateFFmpegPath( + logger: LogWrapper, + ffmpegPath: string, + ffprobePath: string + ): Promise { + // 如果当前使用的是 Exec 适配器,更新路径 + if (this.instance && this.instance instanceof FFmpegExecAdapter) { + logger.log(`[FFmpeg] 更新 FFmpeg 路径: ${ffmpegPath}`); + this.instance.setFFmpegPath(ffmpegPath); + this.instance.setFFprobePath(ffprobePath); + + // 验证新路径是否可用 + if (await this.instance.isAvailable()) { + logger.log('[FFmpeg] 新路径验证成功 ✓'); + } else { + logger.logError('[FFmpeg] 警告: 新 FFmpeg 路径不可用'); + } + } + } + + /** + * 获取当前适配器(不初始化) + */ + static getCurrentAdapter(): IFFmpegAdapter | null { + return this.instance; + } +} diff --git a/src/common/ffmpeg-adapter-interface.ts b/src/common/ffmpeg-adapter-interface.ts new file mode 100644 index 00000000..4e9a015c --- /dev/null +++ b/src/common/ffmpeg-adapter-interface.ts @@ -0,0 +1,68 @@ +/** + * FFmpeg Adapter Interface + * 定义统一的 FFmpeg 操作接口,支持多种实现方式 + */ + +/** + * 视频信息结果 + */ +export interface VideoInfoResult { + /** 视频宽度(像素) */ + width: number; + /** 视频高度(像素) */ + height: number; + /** 视频时长(秒) */ + duration: number; + /** 容器格式 */ + format: string; + /** 缩略图 Buffer */ + thumbnail?: Buffer; +} + +/** + * FFmpeg 适配器接口 + */ +export interface IFFmpegAdapter { + /** 适配器名称 */ + readonly name: string; + + /** 是否可用 */ + isAvailable(): Promise; + + /** + * 获取视频信息(包含缩略图) + * @param videoPath 视频文件路径 + * @returns 视频信息 + */ + getVideoInfo(videoPath: string): Promise; + + /** + * 获取音视频文件时长 + * @param filePath 文件路径 + * @returns 时长(秒) + */ + getDuration(filePath: string): Promise; + + /** + * 转换音频为 PCM 格式 + * @param filePath 输入文件路径 + * @param pcmPath 输出 PCM 文件路径 + * @returns PCM 数据 Buffer + */ + convertToPCM(filePath: string, pcmPath: string): Promise; + + /** + * 转换音频文件 + * @param inputFile 输入文件路径 + * @param outputFile 输出文件路径 + * @param format 目标格式 ('amr' | 'silk' 等) + */ + convertFile(inputFile: string, outputFile: string, format: string): Promise; + + /** + * 提取视频缩略图 + * @param videoPath 视频文件路径 + * @param thumbnailPath 缩略图输出路径 + */ + extractThumbnail(videoPath: string, thumbnailPath: string): Promise; +} diff --git a/src/common/ffmpeg-addon-adapter.ts b/src/common/ffmpeg-addon-adapter.ts new file mode 100644 index 00000000..19b85562 --- /dev/null +++ b/src/common/ffmpeg-addon-adapter.ts @@ -0,0 +1,129 @@ +/** + * FFmpeg Native Addon Adapter + * 使用原生 Node.js Addon 实现的 FFmpeg 适配器 + */ + +import { platform, arch } from 'node:os'; +import path from 'node:path'; +import { existsSync } from 'node:fs'; +import { writeFile } from 'node:fs/promises'; +import type { FFmpeg } from './ffmpeg-addon'; +import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface'; +import { dlopen } from 'node:process'; + +/** + * 获取 Native Addon 路径 + * @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath) + */ +function getAddonPath(binaryPath: string): string { + const platformName = platform(); + const archName = arch(); + + let addonFileName: string = process.platform + '.' + process.arch; + let addonPath = path.join(binaryPath, "./native/ffmpeg/", `${addonFileName}.node`); + if (existsSync(addonPath)) { + throw new Error(`Unsupported platform: ${platformName} ${archName}`); + } + return addonPath; +} + +/** + * FFmpeg Native Addon 适配器实现 + */ +export class FFmpegAddonAdapter implements IFFmpegAdapter { + public readonly name = 'FFmpegAddon'; + private addon: FFmpeg | null = null; + private binaryPath: string; + + constructor(binaryPath: string) { + this.binaryPath = binaryPath; + } + + /** + * 检查 Addon 是否可用 + */ + async isAvailable(): Promise { + try { + const addonPath = getAddonPath(this.binaryPath); + if (!existsSync(addonPath)) { + return false; + } + let temp_addon = { exports: {} }; + dlopen(temp_addon, addonPath); + this.addon = temp_addon.exports as FFmpeg; + return this.addon !== null; + } catch (error) { + console.error('[FFmpegAddonAdapter] Failed to load addon:', error); + return false; + } + } + + private ensureAddon(): FFmpeg { + if (!this.addon) { + throw new Error('FFmpeg Addon is not available'); + } + return this.addon; + } + + /** + * 获取视频信息 + */ + async getVideoInfo(videoPath: string): Promise { + const addon = this.ensureAddon(); + const info = await addon.getVideoInfo(videoPath, 'bmp24'); + + return { + width: info.width, + height: info.height, + duration: info.duration, + format: info.format, + thumbnail: info.image, + }; + } + + /** + * 获取时长 + */ + async getDuration(filePath: string): Promise { + const addon = this.ensureAddon(); + return addon.getDuration(filePath); + } + + /** + * 转换为 PCM + */ + async convertToPCM(filePath: string, pcmPath: string): Promise { + const addon = this.ensureAddon(); + const result = await addon.decodeAudioToPCM(filePath); + + // 写入文件 + await writeFile(pcmPath, result.pcm); + + return result.pcm; + } + + /** + * 转换文件 + */ + async convertFile(inputFile: string, outputFile: string, format: string): Promise { + const addon = this.ensureAddon(); + + if (format === 'silk' || format === 'ntsilk') { + // 使用 Addon 的 NTSILK 转换 + await addon.convertToNTSilkTct(inputFile, outputFile); + } else { + throw new Error(`Format '${format}' is not supported by FFmpeg Addon`); + } + } + + /** + * 提取缩略图 + */ + async extractThumbnail(videoPath: string, thumbnailPath: string): Promise { + const addon = this.ensureAddon(); + const info = await addon.getVideoInfo(videoPath); + + // 将缩略图写入文件 + await writeFile(thumbnailPath, info.image); + } +} diff --git a/src/common/ffmpeg-addon.ts b/src/common/ffmpeg-addon.ts new file mode 100644 index 00000000..1712e510 --- /dev/null +++ b/src/common/ffmpeg-addon.ts @@ -0,0 +1,71 @@ +/** + * FFmpeg Node.js Native Addon Type Definitions + * + * This addon provides FFmpeg functionality for Node.js including: + * - Video information extraction with thumbnail generation + * - Audio/Video duration detection + * - Audio format conversion to NTSILK + * - Audio decoding to PCM + */ + +/** + * Video information result object + */ +export interface VideoInfo { + /** Video width in pixels */ + width: number; + + /** Video height in pixels */ + height: number; + + /** Video duration in seconds */ + duration: number; + + /** Container format name (e.g., "mp4", "mkv", "avi") */ + format: string; + + /** Video codec name (e.g., "h264", "hevc", "vp9") */ + videoCodec: string; + + /** First frame thumbnail as BMP image buffer */ + image: Buffer; +} + +/** + * Audio PCM decoding result object + */ +export interface AudioPCMResult { + /** PCM audio data as 16-bit signed integer samples */ + pcm: Buffer; + + /** Sample rate in Hz (e.g., 44100, 48000, 24000) */ + sampleRate: number; + + /** Number of audio channels (1 for mono, 2 for stereo) */ + channels: number; +} + +/** + * FFmpeg interface providing all audio/video processing methods + */ +export interface FFmpeg { + /** + * Get video information including resolution, duration, format, codec and first frame thumbnail + */ + getVideoInfo(filePath: string, format?: 'bmp' | 'bmp24'): Promise; + + /** + * Get duration of audio or video file in seconds + */ + getDuration(filePath: string): Promise; + + /** + * Convert audio file to NTSILK format (WeChat voice message format) + */ + convertToNTSilkTct(inputPath: string, outputPath: string): Promise; + + /** + * Decode audio file to raw PCM data + */ + decodeAudioToPCM(filePath: string): Promise; +} \ No newline at end of file diff --git a/src/common/ffmpeg-exec-adapter.ts b/src/common/ffmpeg-exec-adapter.ts new file mode 100644 index 00000000..8c503701 --- /dev/null +++ b/src/common/ffmpeg-exec-adapter.ts @@ -0,0 +1,244 @@ +/** + * FFmpeg Exec Adapter + * 使用 execFile 调用 FFmpeg 命令行工具的适配器实现 + */ + +import { readFileSync, existsSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { fileTypeFromFile } from 'file-type'; +import { imageSizeFallBack } from '@/image-size'; +import { downloadFFmpegIfNotExists } from './download-ffmpeg'; +import { LogWrapper } from './log'; +import type { IFFmpegAdapter, VideoInfoResult } from './ffmpeg-adapter-interface'; + +const execFileAsync = promisify(execFile); + +/** + * 确保目录存在 + */ +function ensureDirExists(filePath: string): void { + const dir = dirname(filePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } +} + +/** + * FFmpeg 命令行适配器实现 + */ +export class FFmpegExecAdapter implements IFFmpegAdapter { + public readonly name = 'FFmpegExec'; + private downloadAttempted = false; + + constructor( + private ffmpegPath: string = 'ffmpeg', + private ffprobePath: string = 'ffprobe', + private binaryPath?: string, + private logger?: LogWrapper + ) {} + + /** + * 检查 FFmpeg 是否可用,如果不可用则尝试下载 + */ + async isAvailable(): Promise { + // 首先检查当前路径 + try { + await execFileAsync(this.ffmpegPath, ['-version']); + return true; + } catch { + // 如果失败且未尝试下载,尝试下载 + if (!this.downloadAttempted && this.binaryPath && this.logger) { + this.downloadAttempted = true; + + if (process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) { + return false; + } + + this.logger.log('[FFmpeg] 未找到可用的 FFmpeg,尝试自动下载...'); + const result = await downloadFFmpegIfNotExists(this.logger); + + if (result.path && result.reset) { + // 更新路径 + if (process.platform === 'win32') { + this.ffmpegPath = join(result.path, 'ffmpeg.exe'); + this.ffprobePath = join(result.path, 'ffprobe.exe'); + this.logger.log('[FFmpeg] 已更新路径:', this.ffmpegPath); + + // 再次检查 + try { + await execFileAsync(this.ffmpegPath, ['-version']); + return true; + } catch { + return false; + } + } + } + } + return false; + } + } + + /** + * 设置 FFmpeg 路径 + */ + setFFmpegPath(ffmpegPath: string): void { + this.ffmpegPath = ffmpegPath; + } + + /** + * 设置 FFprobe 路径 + */ + setFFprobePath(ffprobePath: string): void { + this.ffprobePath = ffprobePath; + } + + /** + * 获取视频信息 + */ + async getVideoInfo(videoPath: string): Promise { + // 获取文件大小和类型 + const [fileType, duration] = await Promise.all([ + fileTypeFromFile(videoPath).catch(() => null), + this.getDuration(videoPath) + ]); + + // 创建临时缩略图路径 + const thumbnailPath = `${videoPath}.thumbnail.bmp`; + let width = 100; + let height = 100; + let thumbnail: Buffer | undefined; + + try { + await this.extractThumbnail(videoPath, thumbnailPath); + + // 获取图片尺寸 + const dimensions = await imageSizeFallBack(thumbnailPath); + width = dimensions.width ?? 100; + height = dimensions.height ?? 100; + + // 读取缩略图 + if (existsSync(thumbnailPath)) { + thumbnail = readFileSync(thumbnailPath); + } + } catch (error) { + // 使用默认值 + } + + return { + width, + height, + duration, + format: fileType?.ext ?? 'mp4', + thumbnail, + }; + } + + /** + * 获取时长 + */ + async getDuration(filePath: string): Promise { + try { + const { stdout } = await execFileAsync(this.ffprobePath, [ + '-v', 'error', + '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', + filePath + ]); + + const duration = parseFloat(stdout.trim()); + return isNaN(duration) ? 60 : duration; + } catch { + return 60; // 默认时长 + } + } + + /** + * 转换为 PCM + */ + async convertToPCM(filePath: string, pcmPath: string): Promise { + try { + ensureDirExists(pcmPath); + + await execFileAsync(this.ffmpegPath, [ + '-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}`); + } + } + + /** + * 转换文件 + */ + async convertFile(inputFile: string, outputFile: string, format: string): Promise { + try { + 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(this.ffmpegPath, params); + + if (!existsSync(outputFile)) { + throw new Error('转换失败,输出文件不存在'); + } + } catch (error) { + console.error('Error converting file:', error); + throw new Error(`文件转换失败: ${(error as Error).message}`); + } + } + + /** + * 提取缩略图 + */ + async extractThumbnail(videoPath: string, thumbnailPath: string): Promise { + try { + ensureDirExists(thumbnailPath); + + const { stderr } = await execFileAsync(this.ffmpegPath, [ + '-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}`); + } + } +} diff --git a/src/common/ffmpeg.ts b/src/common/ffmpeg.ts index e6100f1d..e71fb254 100644 --- a/src/common/ffmpeg.ts +++ b/src/common/ffmpeg.ts @@ -1,195 +1,144 @@ -import { readFileSync, statSync, existsSync, mkdirSync } from 'fs'; -import path, { dirname } from 'path'; -import { execFile } from 'child_process'; -import { promisify } from 'util'; +import { statSync, existsSync, writeFileSync } from 'fs'; +import path from 'path'; 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') { +import { FFmpegAdapterFactory } from './ffmpeg-adapter-factory'; +import type { IFFmpegAdapter } from './ffmpeg-adapter-interface'; + +const getFFmpegPath = (tool: string, binaryPath?: string): string => { + if (process.platform === 'win32' && binaryPath) { const exeName = `${tool}.exe`; - const isLocalExeExists = existsSync(path.join(currentPath, 'ffmpeg', exeName)); - return isLocalExeExists ? path.join(currentPath, 'ffmpeg', exeName) : exeName; + const localPath = path.join(binaryPath, 'ffmpeg', exeName); + const isLocalExeExists = existsSync(localPath); + return isLocalExeExists ? localPath : exeName; } return tool; }; -export let FFMPEG_CMD = getFFmpegPath('ffmpeg'); -export let FFPROBE_CMD = getFFmpegPath('ffprobe'); + +export let FFMPEG_CMD = 'ffmpeg'; +export let FFPROBE_CMD = 'ffprobe'; export class FFmpegService { - // 确保目标目录存在 - public static setFfmpegPath(ffmpegPath: string,logger:LogWrapper): void { + private static adapter: IFFmpegAdapter | null = null; + private static initialized = false; + + /** + * 初始化 FFmpeg 服务 + * @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath) + * @param logger 日志记录器 + */ + public static async init(binaryPath: string, logger: LogWrapper): Promise { + if (this.initialized) { + return; + } + + // 检查本地 ffmpeg 路径 + FFMPEG_CMD = getFFmpegPath('ffmpeg', binaryPath); + FFPROBE_CMD = getFFmpegPath('ffprobe', binaryPath); + + // 立即初始化适配器(会触发自动下载等逻辑) + this.adapter = await FFmpegAdapterFactory.getAdapter( + logger, + FFMPEG_CMD, + FFPROBE_CMD, + binaryPath + ); + + this.initialized = true; + } + + /** + * 获取 FFmpeg 适配器 + */ + private static async getAdapter(): Promise { + if (!this.adapter) { + throw new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.'); + } + return this.adapter; + } + + /** + * 设置 FFmpeg 路径并更新适配器 + * @deprecated 建议使用 init() 方法初始化 + */ + public static async setFfmpegPath(ffmpegPath: string, logger: LogWrapper): Promise { 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 }); + + // 更新适配器路径 + await FFmpegAdapterFactory.updateFFmpegPath(logger, FFMPEG_CMD, FFPROBE_CMD); } } + /** + * 提取视频缩略图 + */ public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise { - 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}`); - } + const adapter = await this.getAdapter(); + await adapter.extractThumbnail(videoPath, thumbnailPath); } + /** + * 转换音频文件 + */ public static async convertFile(inputFile: string, outputFile: string, format: string): Promise { - 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}`); - } + const adapter = await this.getAdapter(); + await adapter.convertFile(inputFile, outputFile, format); } + /** + * 转换为 PCM 格式 + */ public static async convert(filePath: string, pcmPath: string): Promise { - 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 adapter = await this.getAdapter(); + return adapter.convertToPCM(filePath, pcmPath); } + /** + * 获取视频信息 + */ public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise { + const adapter = await this.getAdapter(); + try { - // 并行执行获取文件信息和提取缩略图 - const [fileInfo, duration] = await Promise.all([ - this.getFileInfo(videoPath, thumbnailPath), - this.getVideoDuration(videoPath) - ]); + // 获取文件大小 + const fileSize = statSync(videoPath).size; + + // 使用适配器获取视频信息 + const videoInfo = await adapter.getVideoInfo(videoPath); + + // 如果提供了缩略图路径且适配器返回了缩略图,保存到指定路径 + if (thumbnailPath && videoInfo.thumbnail) { + writeFileSync(thumbnailPath, videoInfo.thumbnail); + } const result: VideoInfo = { - width: fileInfo.width, - height: fileInfo.height, - time: duration, - format: fileInfo.format, - size: fileInfo.size, + width: videoInfo.width, + height: videoInfo.height, + time: videoInfo.duration, + format: videoInfo.format, + size: fileSize, filePath: videoPath }; + return result; } catch (error) { - throw error; - } - } - - 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); + // 降级处理:返回默认值 + const fileType = await fileTypeFromFile(videoPath).catch(() => null); + const fileSize = statSync(videoPath).size; 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 + height: 100, + time: 60, + format: fileType?.ext ?? 'mp4', + size: fileSize, + filePath: videoPath }; } } - - private static async getVideoDuration(videoPath: string): Promise { - 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; // 默认时长 - } - } } \ No newline at end of file diff --git a/src/common/qq-basic-info.ts b/src/common/qq-basic-info.ts index cc9a9482..3dc30122 100644 --- a/src/common/qq-basic-info.ts +++ b/src/common/qq-basic-info.ts @@ -39,7 +39,7 @@ export class QQBasicInfoWrapper { //基础函数 getQQBuildStr() { - return this.isQuickUpdate ? this.QQVersionConfig?.buildId : this.QQPackageInfo?.buildVersion; + return this.QQVersionConfig?.curVersion.split('-')[1] ?? this.QQPackageInfo?.buildVersion; } getFullQQVersion() { diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 14bf8b87..48b650ae 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -64,7 +64,7 @@ export class NTQQFileApi { } } - async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined,timeout: number = 20000) { + async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined,timeout: number = 5000) { if (this.core.apis.PacketApi.packetStatus) { try { if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) { @@ -79,7 +79,7 @@ export class NTQQFileApi { throw new Error('fileUUID or file10MMd5 is undefined'); } - async getPttUrl(peer: string, fileUUID?: string,timeout: number = 20000) { + async getPttUrl(peer: string, fileUUID?: string,timeout: number = 5000) { if (this.core.apis.PacketApi.packetStatus && fileUUID) { let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid; try { @@ -107,7 +107,7 @@ export class NTQQFileApi { throw new Error('packet cant get ptt url'); } - async getVideoUrlPacket(peer: string, fileUUID?: string,timeout: number = 20000) { + async getVideoUrlPacket(peer: string, fileUUID?: string,timeout: number = 5000) { if (this.core.apis.PacketApi.packetStatus && fileUUID) { let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid; try { diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts index 2952c30b..ca8de1c8 100644 --- a/src/core/apis/group.ts +++ b/src/core/apis/group.ts @@ -49,7 +49,6 @@ export class NTQQGroupApi { async initApi() { this.initCache().then().catch(e => this.context.logger.logError(e)); } - async createGrayTip(groupCode: string, tip: string) { return this.context.session.getMsgService().addLocalJsonGrayTipMsg( { diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 1c9db417..197213ef 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -1,5 +1,5 @@ import * as os from 'os'; -import offset from '@/core/external/offset.json'; +import offset from '@/core/external/napi2native.json'; import { InstanceContext, NapCatCore } from '@/core'; import { LogWrapper } from '@/common/log'; import { PacketClientSession } from '@/core/packet/clientSession'; diff --git a/src/core/external/appid.json b/src/core/external/appid.json index 9224f4fb..dc4d0b4b 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -419,7 +419,7 @@ "appid": 537319880, "qua": "V1_MAC_NQ_6.9.82_40990_GW_B" }, - "9.9.22.40990": { + "9.9.22-40990": { "appid": 537319855, "qua": "V1_WIN_NQ_9.9.22.40990_GW_B" }, diff --git a/src/core/external/napi2native.json b/src/core/external/napi2native.json new file mode 100644 index 00000000..18e7a572 --- /dev/null +++ b/src/core/external/napi2native.json @@ -0,0 +1,38 @@ +{ + "9.9.22-40990-x64": { + "send": "1B5699C", + "recv": "1D8CA9D" + }, + "9.9.22-40824-x64": { + "send": "1B5699C", + "recv": "1D8CA9D" + }, + "9.9.22-40768-x64": { + "send": "1B5699C", + "recv": "1D8CA9D" + }, + "3.2.20-40768-x64": { + "send": "2CC8120", + "recv": "2D28F20" + }, + "3.2.20-40824-x64": { + "send": "2CC8120", + "recv": "2D28F20" + }, + "3.2.20-40990-x64": { + "send": "2CC8120", + "recv": "2D28F20" + }, + "3.2.20-40990-arm64": { + "send": "157C0E8", + "recv": "1546658" + }, + "3.2.20-40824-arm64": { + "send": "157C0E8", + "recv": "1546658" + }, + "3.2.20-40768-arm64": { + "send": "157C0E8", + "recv": "1546658" + } +} \ No newline at end of file diff --git a/src/core/external/offset.json b/src/core/external/packet.json similarity index 100% rename from src/core/external/offset.json rename to src/core/external/packet.json diff --git a/src/core/index.ts b/src/core/index.ts index dff28680..fbe84535 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -30,6 +30,7 @@ import os from 'node:os'; import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners'; import { proxiedListenerOf } from '@/common/proxy-handler'; import { NTQQPacketApi } from './apis/packet'; +import { NativePacketHandler } from './packet/handler/client'; export * from './wrapper'; export * from './types'; export * from './services'; @@ -258,6 +259,7 @@ export interface InstanceContext { readonly loginService: NodeIKernelLoginService; readonly basicInfoWrapper: QQBasicInfoWrapper; readonly pathWrapper: NapCatPathWrapper; + readonly packetHandler: NativePacketHandler; } export interface StableNTApiWrapper { diff --git a/src/core/packet/client/baseClient.ts b/src/core/packet/client/baseClient.ts deleted file mode 100644 index a6025049..00000000 --- a/src/core/packet/client/baseClient.ts +++ /dev/null @@ -1,88 +0,0 @@ -import crypto, { createHash } from 'crypto'; -import { OidbPacket, PacketHexStr } from '@/core/packet/transformer/base'; -import { LogStack } from '@/core/packet/context/clientContext'; -import { NapCoreContext } from '@/core/packet/context/napCoreContext'; -import { PacketLogger } from '@/core/packet/context/loggerContext'; - -export interface RecvPacket { - type: string, // 仅recv - data: RecvPacketData -} - -export interface RecvPacketData { - seq: number - cmd: string - hex_data: string -} - -function randText(len: number): string { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < len; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - - -export abstract class IPacketClient { - protected readonly napcore: NapCoreContext; - protected readonly logger: PacketLogger; - protected readonly cb = new Map Promise | any>(); // hash-type callback - logStack: LogStack; - available: boolean = false; - - protected constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) { - this.napcore = napCore; - this.logger = logger; - this.logStack = logStack; - } - - abstract check(): boolean; - - abstract init(pid: number, recv: string, send: string): Promise; - - abstract sendCommandImpl(cmd: string, data: string, hash: string, timeout: number): void; - - private async sendCommand(cmd: string, data: string, trace_data: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => { - }): Promise { - return new Promise((resolve, reject) => { - if (!this.available) { - reject(new Error('packetBackend 当前不可用!')); - } - let hash = createHash('md5').update(trace_data).digest('hex'); - const timeoutHandle = setTimeout(() => { - this.cb.delete(hash + 'send'); - this.cb.delete(hash + 'recv'); - reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with hash ${hash}`)); - }, timeout); - this.cb.set(hash + 'send', async (json: RecvPacketData) => { - sendcb(json); - if (!rsp) { - clearTimeout(timeoutHandle); - resolve(json); - } - }); - - if (rsp) { - this.cb.set(hash + 'recv', async (json: RecvPacketData) => { - clearTimeout(timeoutHandle); - resolve(json); - }); - } - this.sendCommandImpl(cmd, data, hash, timeout); - }); - } - - async sendPacket(cmd: string, data: PacketHexStr, rsp = false, timeout = 20000): Promise { - const md5 = crypto.createHash('md5').update(data).digest('hex'); - const trace_data = (randText(4) + md5 + data).slice(0, data.length / 2);// trace_data - return this.sendCommand(cmd, data, trace_data, rsp, timeout, async () => { - await this.napcore.sendSsoCmdReqByContend(cmd, trace_data); - }); - } - - async sendOidbPacket(pkt: OidbPacket, rsp = false, timeout = 20000): Promise { - return this.sendPacket(pkt.cmd, pkt.data, rsp, timeout); - } -} diff --git a/src/core/packet/client/nativeClient.ts b/src/core/packet/client/nativeClient.ts index 1d08ded2..ecbb842b 100644 --- a/src/core/packet/client/nativeClient.ts +++ b/src/core/packet/client/nativeClient.ts @@ -1,27 +1,40 @@ -import { createHash } from 'crypto'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; -import { IPacketClient } from '@/core/packet/client/baseClient'; import { constants } from 'node:os'; import { LogStack } from '@/core/packet/context/clientContext'; import { NapCoreContext } from '@/core/packet/context/napCoreContext'; import { PacketLogger } from '@/core/packet/context/loggerContext'; +import { OidbPacket, PacketBuf } from '@/core/packet/transformer/base'; +export interface RecvPacket { + type: string, // 仅recv + data: RecvPacketData +} + +export interface RecvPacketData { + seq: number + cmd: string + data: Buffer +} // 0 send 1 recv export interface NativePacketExportType { - InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean; - SendPacket?: (cmd: string, data: string, trace_id: string) => void; + initHook?: (send: string, recv: string) => boolean; } -export class NativePacketClient extends IPacketClient { +export class NativePacketClient { + protected readonly napcore: NapCoreContext; + protected readonly logger: PacketLogger; + protected readonly cb = new Map Promise | any>(); // hash-type callback + logStack: LogStack; + available: boolean = false; private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64']; private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} }; - private readonly sendEvent = new Map(); // seq - hash - private readonly timeEvent = new Map(); // hash - timeout constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) { - super(napCore, logger, logStack); + this.napcore = napCore; + this.logger = logger; + this.logStack = logStack; } check(): boolean { @@ -30,7 +43,7 @@ export class NativePacketClient extends IPacketClient { this.logStack.pushLogWarn(`NativePacketClient: 不支持的平台: ${platform}`); return false; } - const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node'); + const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/napi2native/napi2native.' + platform + '.node'); if (!fs.existsSync(moehoo_path)) { this.logStack.pushLogWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`); return false; @@ -40,36 +53,55 @@ export class NativePacketClient extends IPacketClient { async init(_pid: number, recv: string, send: string): Promise { const platform = process.platform + '.' + process.arch; - const isNewQQ = this.napcore.basicInfo.requireMinNTQQBuild("36580"); - const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + (isNewQQ ? '.new' : '') + '.node'); - process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); - - this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, _uin: string, cmd: string, seq: number, hex_data: string) => { - const hash = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex'); - if (type === 0 && this.cb.get(hash + 'recv')) { - //此时为send 提取seq - this.sendEvent.set(seq, hash); - setTimeout(() => { - this.sendEvent.delete(seq); - this.timeEvent.delete(hash); - }, +(this.timeEvent.get(hash) ?? 20000)); - //正式send完成 无recv v - //均无异常 v - } - if (type === 1 && this.sendEvent.get(seq)) { - const hash = this.sendEvent.get(seq); - const callback = this.cb.get(hash + 'recv'); - callback?.({ seq, cmd, hex_data }); - } - }, this.napcore.config.o3HookMode == 1); - this.available = true; + const isNewQQ = this.napcore.basicInfo.requireMinNTQQBuild("40824"); + if (isNewQQ) { + const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/napi2native/napi2native.' + platform + '.node'); + process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); + this.MoeHooExport?.exports.initHook?.(send, recv); + this.available = true; + } } - sendCommandImpl(cmd: string, data: string, hash: string, timeout: number): void { - this.timeEvent.set(hash, setTimeout(() => { - this.timeEvent.delete(hash);//考虑情况为正式send都没进 - }, timeout)); - this.MoeHooExport.exports.SendPacket?.(cmd, data, hash); - this.cb.get(hash + 'send')?.({ seq: 0, cmd, hex_data: '' }); + async sendPacket( + cmd: string, + data: PacketBuf, + rsp = false, + timeout = 5000 + ): Promise { + if (!rsp) { + this.napcore + .sendSsoCmdReqByContend(cmd, data) + .catch(err => + this.logger.error( + `[PacketClient] sendPacket 无响应命令发送失败 cmd=${cmd} err=${err}` + ) + ); + return { seq: 0, cmd, data: Buffer.alloc(0) }; + } + + const sendPromise = this.napcore + .sendSsoCmdReqByContend(cmd, data) + .then(ret => ({ + seq: 0, + cmd, + data: (ret as { rspbuffer: Buffer }).rspbuffer + })); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => + reject( + new Error( + `[PacketClient] sendPacket 超时 cmd=${cmd} timeout=${timeout}ms` + ) + ), + timeout + ); + }); + + return Promise.race([sendPromise, timeoutPromise]); + } + async sendOidbPacket(pkt: OidbPacket, rsp = false, timeout = 5000): Promise { + return await this.sendPacket(pkt.cmd, pkt.data, rsp, timeout); } } diff --git a/src/core/packet/context/clientContext.ts b/src/core/packet/context/clientContext.ts index 585e4a77..69f3ae1b 100644 --- a/src/core/packet/context/clientContext.ts +++ b/src/core/packet/context/clientContext.ts @@ -1,17 +1,8 @@ -import { IPacketClient } from '@/core/packet/client/baseClient'; import { NativePacketClient } from '@/core/packet/client/nativeClient'; import { OidbPacket } from '@/core/packet/transformer/base'; import { PacketLogger } from '@/core/packet/context/loggerContext'; import { NapCoreContext } from '@/core/packet/context/napCoreContext'; -type clientPriorityType = { - [key: number]: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => IPacketClient; -} - -const clientPriority: clientPriorityType = { - 10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack) -}; - export class LogStack { private stack: string[] = []; private readonly logger: PacketLogger; @@ -52,7 +43,7 @@ export class PacketClientContext { private readonly napCore: NapCoreContext; private readonly logger: PacketLogger; private readonly logStack: LogStack; - private readonly _client: IPacketClient; + private readonly _client: NativePacketClient; constructor(napCore: NapCoreContext, logger: PacketLogger) { this.napCore = napCore; @@ -75,48 +66,15 @@ export class PacketClientContext { async sendOidbPacket(pkt: OidbPacket, rsp?: T, timeout?: number): Promise { const raw = await this._client.sendOidbPacket(pkt, rsp, timeout); - return (rsp ? Buffer.from(raw.hex_data, 'hex') : undefined) as T extends true ? Buffer : void; + return raw.data as T extends true ? Buffer : void; } - private newClient(): IPacketClient { - const prefer = this.napCore.config.packetBackend; - let client: IPacketClient | null; - switch (prefer) { - case 'native': - this.logger.info('使用指定的 NativePacketClient 作为后端'); - client = new NativePacketClient(this.napCore, this.logger, this.logStack); - break; - case 'auto': - case undefined: - client = this.judgeClient(); - break; - default: - this.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`); - client = null; - } - if (!client?.check()) { - throw new Error('[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!'); - } - if (!client) { - throw new Error('[Core] [Packet] 后端异常,NapCat.Packet将不会加载!'); + private newClient(): NativePacketClient { + this.logger.info('使用 NativePacketClient 作为后端'); + const client = new NativePacketClient(this.napCore, this.logger, this.logStack); + if (!client.check()) { + throw new Error('[Core] [Packet] NativePacketClient 不可用,NapCat.Packet将不会加载!'); } return client; } - - private judgeClient(): IPacketClient { - const sortedClients = Object.entries(clientPriority) - .map(([priority, clientFactory]) => { - const client = clientFactory(this.napCore, this.logger, this.logStack); - const score = +priority * +client.check(); - return { client, score }; - }) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score); - const selectedClient = sortedClients[0]?.client; - if (!selectedClient) { - throw new Error('[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!'); - } - this.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`); - return selectedClient; - } } diff --git a/src/core/packet/context/napCoreContext.ts b/src/core/packet/context/napCoreContext.ts index ff7faa75..00e3f67e 100644 --- a/src/core/packet/context/napCoreContext.ts +++ b/src/core/packet/context/napCoreContext.ts @@ -34,5 +34,5 @@ export class NapCoreContext { return this.core.configLoader.configData; } - sendSsoCmdReqByContend = (cmd: string, trace_id: string) => this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id); + sendSsoCmdReqByContend = (cmd: string, data: Buffer) => this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, data); } diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts index e3ab7de6..00c4143b 100644 --- a/src/core/packet/context/operationContext.ts +++ b/src/core/packet/context/operationContext.ts @@ -122,28 +122,28 @@ export class PacketOperationContext { return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } - async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType,timeout: number = 20000) { + async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadPtt.build(selfUid, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); 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, timeout: number = 20000) { + async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadVideo.build(selfUid, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); 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, timeout: number = 20000) { + async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadGroupImage.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadImage.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } - async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType, timeout: number = 20000) { + async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadGroupPtt.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadImage.parse(resp); @@ -243,14 +243,14 @@ export class PacketOperationContext { return res.rename.retCode; } - async GetGroupFileUrl(groupUin: number, fileUUID: string,timeout: number = 20000) { + async GetGroupFileUrl(groupUin: number, fileUUID: string, timeout?: number) { const req = trans.DownloadGroupFile.build(groupUin, fileUUID); const resp = await this.context.client.sendOidbPacket(req, true, timeout); 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, timeout: number = 20000) { + async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string, timeout?: number) { const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadPrivateFile.parse(resp); diff --git a/src/core/packet/handler/client.ts b/src/core/packet/handler/client.ts new file mode 100644 index 00000000..8e19fc0a --- /dev/null +++ b/src/core/packet/handler/client.ts @@ -0,0 +1,214 @@ +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import { constants } from 'node:os'; +import { LogWrapper } from '@/common/log'; +import offset from '@/core/external/packet.json'; +interface OffsetType { + [key: string]: { + recv: string; + send: string; + }; +} + +const typedOffset: OffsetType = offset; +// 0 send 1 recv +export interface NativePacketExportType { + initHook?: (send: string, recv: string, callback: (type: PacketType, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean; +} + +export type PacketType = 0 | 1; // 0: send, 1: recv +export type PacketCallback = (data: { type: PacketType, uin: string, cmd: string, seq: number, hex_data: string }) => void; + +interface ListenerEntry { + callback: PacketCallback; + once: boolean; +} + +export class NativePacketHandler { + private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64']; + private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} }; + protected readonly logger: LogWrapper; + + // 统一的监听器存储 - key: 'all' | 'type:0' | 'type:1' | 'cmd:xxx' | 'exact:type:cmd' + private readonly listeners: Map> = new Map(); + + + constructor({ logger }: { logger: LogWrapper }) { + this.logger = logger; + } + + /** + * 添加监听器的通用方法 + */ + private addListener(key: string, callback: PacketCallback, once: boolean = false): () => void { + if (!this.listeners.has(key)) { + this.listeners.set(key, new Set()); + } + const entry: ListenerEntry = { callback, once }; + this.listeners.get(key)!.add(entry); + return () => this.removeListener(key, callback); + } + + /** + * 移除监听器的通用方法 + */ + private removeListener(key: string, callback: PacketCallback): boolean { + const entries = this.listeners.get(key); + if (!entries) return false; + + for (const entry of entries) { + if (entry.callback === callback) { + return entries.delete(entry); + } + } + return false; + } + + // ===== 永久监听器 ===== + + /** 监听所有数据包 */ + onAll(callback: PacketCallback): () => void { + return this.addListener('all', callback); + } + + /** 监听特定类型的数据包 (0: send, 1: recv) */ + onType(type: PacketType, callback: PacketCallback): () => void { + return this.addListener(`type:${type}`, callback); + } + + /** 监听所有发送的数据包 */ + onSend(callback: PacketCallback): () => void { + return this.onType(0, callback); + } + + /** 监听所有接收的数据包 */ + onRecv(callback: PacketCallback): () => void { + return this.onType(1, callback); + } + + /** 监听特定cmd的数据包(不限type) */ + onCmd(cmd: string, callback: PacketCallback): () => void { + return this.addListener(`cmd:${cmd}`, callback); + } + + /** 监听特定type和cmd的数据包(精确匹配) */ + onExact(type: PacketType, cmd: string, callback: PacketCallback): () => void { + return this.addListener(`exact:${type}:${cmd}`, callback); + } + + // ===== 一次性监听器 ===== + + /** 一次性监听所有数据包 */ + onceAll(callback: PacketCallback): () => void { + return this.addListener('all', callback, true); + } + + /** 一次性监听特定类型的数据包 */ + onceType(type: PacketType, callback: PacketCallback): () => void { + return this.addListener(`type:${type}`, callback, true); + } + + /** 一次性监听所有发送的数据包 */ + onceSend(callback: PacketCallback): () => void { + return this.onceType(0, callback); + } + + /** 一次性监听所有接收的数据包 */ + onceRecv(callback: PacketCallback): () => void { + return this.onceType(1, callback); + } + + /** 一次性监听特定cmd的数据包 */ + onceCmd(cmd: string, callback: PacketCallback): () => void { + return this.addListener(`cmd:${cmd}`, callback, true); + } + + /** 一次性监听特定type和cmd的数据包 */ + onceExact(type: PacketType, cmd: string, callback: PacketCallback): () => void { + return this.addListener(`exact:${type}:${cmd}`, callback, true); + } + + // ===== 移除监听器 ===== + + /** 移除特定的全局监听器 */ + off(key: string, callback: PacketCallback): boolean { + return this.removeListener(key, callback); + } + + /** 移除特定key下的所有监听器 */ + offAll(key: string): void { + this.listeners.delete(key); + } + + /** 移除所有监听器 */ + removeAllListeners(): void { + this.listeners.clear(); + } + + /** + * 触发监听器 - 按优先级触发: 精确匹配 > cmd匹配 > type匹配 > 全局 + */ + private emitPacket(type: PacketType, uin: string, cmd: string, seq: number, hex_data: string): void { + const keys = [ + `exact:${type}:${cmd}`, // 精确匹配 + `cmd:${cmd}`, // cmd匹配 + `type:${type}`, // type匹配 + 'all' // 全局 + ]; + + for (const key of keys) { + const entries = this.listeners.get(key); + if (!entries) continue; + + const toRemove: ListenerEntry[] = []; + for (const entry of entries) { + try { + entry.callback({ type, uin, cmd, seq, hex_data }); + if (entry.once) { + toRemove.push(entry); + } + } catch (error) { + this.logger.logError('监听器回调执行出错:', error); + } + } + + // 移除一次性监听器 + for (const entry of toRemove) { + entries.delete(entry); + } + } + } + + async init(version: string): Promise { + const version_arch = version + '-' + process.arch; + try { + const send = typedOffset[version_arch]?.send; + const recv = typedOffset[version_arch]?.recv; + if (!send || !recv) { + this.logger.logWarn(`NativePacketClient: 未找到对应版本的偏移数据: ${version_arch}`); + return false; + } + const platform = process.platform + '.' + process.arch; + if (!this.supportedPlatforms.includes(platform)) { + this.logger.logWarn(`NativePacketClient: 不支持的平台: ${platform}`); + return false; + } + const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/packet/MoeHoo.' + platform + '.node'); + + process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); + if (!fs.existsSync(moehoo_path)) { + this.logger.logWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`); + return false; + } + this.MoeHooExport.exports.initHook?.(send, recv, (type: PacketType, uin: string, cmd: string, seq: number, hex_data: string) => { + this.emitPacket(type, uin, cmd, seq, hex_data); + }, true); + return true; + } + catch (error) { + this.logger.logError('NativePacketClient 初始化出错:', error); + return false; + } + } +} diff --git a/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts b/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts index 135c86d4..ab5e203a 100644 --- a/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts +++ b/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts @@ -1,6 +1,6 @@ import * as proto from '@/core/packet/transformer/proto'; import { NapProtoMsg } from '@napneko/nap-proto-core'; -import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base'; +import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/core/packet/transformer/base'; import { MiniAppReqParams } from '@/core/packet/entities/miniApp'; class GetMiniAppAdaptShareInfo extends PacketTransformer { @@ -41,7 +41,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer { - return Buffer.from(str).toString('hex') as PacketHexStr; +export const PacketBufBuilder = (str: Uint8Array): PacketBuf => { + return Buffer.from(str) as PacketBuf; }; export interface OidbPacket { cmd: string; - data: PacketHexStr + data: PacketBuf } export abstract class PacketTransformer { diff --git a/src/core/packet/transformer/highway/FetchSessionKey.ts b/src/core/packet/transformer/highway/FetchSessionKey.ts index 263326f5..6aef75f2 100644 --- a/src/core/packet/transformer/highway/FetchSessionKey.ts +++ b/src/core/packet/transformer/highway/FetchSessionKey.ts @@ -1,6 +1,6 @@ import * as proto from '@/core/packet/transformer/proto'; import { NapProtoMsg } from '@napneko/nap-proto-core'; -import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base'; +import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/core/packet/transformer/base'; class FetchSessionKey extends PacketTransformer { constructor() { @@ -25,7 +25,7 @@ class FetchSessionKey extends PacketTransformer { constructor() { @@ -25,7 +25,7 @@ class DownloadForwardMsg extends PacketTransformer }); return { cmd: 'trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg', - data: PacketHexStrBuilder(req) + data: PacketBufBuilder(req) }; } diff --git a/src/core/packet/transformer/message/FetchC2CMessage.ts b/src/core/packet/transformer/message/FetchC2CMessage.ts index 92785163..1fc7adf9 100644 --- a/src/core/packet/transformer/message/FetchC2CMessage.ts +++ b/src/core/packet/transformer/message/FetchC2CMessage.ts @@ -1,6 +1,6 @@ import * as proto from '@/core/packet/transformer/proto'; import { NapProtoMsg } from '@napneko/nap-proto-core'; -import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base'; +import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/core/packet/transformer/base'; class FetchC2CMessage extends PacketTransformer { constructor() { @@ -15,7 +15,7 @@ class FetchC2CMessage extends PacketTransformer { constructor() { @@ -18,7 +18,7 @@ class FetchGroupMessage extends PacketTransformer { @@ -39,7 +39,7 @@ class UploadForwardMsg extends PacketTransformer { ); return { cmd: 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', - data: PacketHexStrBuilder(req) + data: PacketBufBuilder(req) }; } diff --git a/src/core/packet/transformer/oidb/oidbBase.ts b/src/core/packet/transformer/oidb/oidbBase.ts index 632a4b10..4e9e092c 100644 --- a/src/core/packet/transformer/oidb/oidbBase.ts +++ b/src/core/packet/transformer/oidb/oidbBase.ts @@ -1,6 +1,6 @@ import * as proto from '@/core/packet/transformer/proto'; import { NapProtoMsg } from '@napneko/nap-proto-core'; -import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base'; +import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/core/packet/transformer/base'; class OidbBase extends PacketTransformer { constructor() { @@ -16,7 +16,7 @@ class OidbBase extends PacketTransformer { }); return { cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`, - data: PacketHexStrBuilder(data), + data: PacketBufBuilder(data), }; } diff --git a/src/core/services/NodeIKernelMsgService.ts b/src/core/services/NodeIKernelMsgService.ts index 34dd8e0c..4d3c6a7c 100644 --- a/src/core/services/NodeIKernelMsgService.ts +++ b/src/core/services/NodeIKernelMsgService.ts @@ -585,7 +585,7 @@ export interface NodeIKernelMsgService { prepareTempChat(args: unknown): unknown; - sendSsoCmdReqByContend(cmd: string, param: string): Promise; + sendSsoCmdReqByContend(cmd: string, param: unknown): Promise; getTempChatInfo(ChatType: number, Uid: string): Promise; diff --git a/src/framework/napcat.ts b/src/framework/napcat.ts index 820f3587..1f85a65d 100644 --- a/src/framework/napcat.ts +++ b/src/framework/napcat.ts @@ -9,8 +9,8 @@ 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'; +import { NativePacketHandler } from '@/core/packet/handler/client'; //Framework ES入口文件 export async function getWebUiUrl() { @@ -38,15 +38,15 @@ export async function NCoreInitFramework( const logger = new LogWrapper(pathWrapper.logsPath); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); - 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 nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 + nativePacketHandler.onAll((packet) => { + console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data); + }); + await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion()); + // 在 init 之后注册监听器 + + // 初始化 FFmpeg 服务 + await FFmpegService.init(pathWrapper.binaryPath, logger); //直到登录成功后,执行下一步 // const selfInfo = { // uid: 'u_FUSS0_x06S_9Tf4na_WpUg', @@ -72,7 +72,7 @@ export async function NCoreInitFramework( // 过早进入会导致addKernelMsgListener等Listener添加失败 // await sleep(2500); // 初始化 NapCatFramework - const loaderObject = new NapCatFramework(wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper); + const loaderObject = new NapCatFramework(wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler); await loaderObject.core.initCore(); //启动WebUi @@ -93,8 +93,10 @@ export class NapCatFramework { selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: NapCatPathWrapper, + packetHandler: NativePacketHandler, ) { this.context = { + packetHandler, workingEnv: NapCatCoreWorkingEnv.Framework, wrapper, session, diff --git a/src/native/ffmpeg/ffmpegAddon.darwin.arm64.node b/src/native/ffmpeg/ffmpegAddon.darwin.arm64.node new file mode 100644 index 00000000..f8b69143 Binary files /dev/null and b/src/native/ffmpeg/ffmpegAddon.darwin.arm64.node differ diff --git a/src/native/ffmpeg/ffmpegAddon.linux.arm64.node b/src/native/ffmpeg/ffmpegAddon.linux.arm64.node new file mode 100644 index 00000000..b11e66a2 Binary files /dev/null and b/src/native/ffmpeg/ffmpegAddon.linux.arm64.node differ diff --git a/src/native/ffmpeg/ffmpegAddon.linux.x64.node b/src/native/ffmpeg/ffmpegAddon.linux.x64.node new file mode 100644 index 00000000..a1916fdd Binary files /dev/null and b/src/native/ffmpeg/ffmpegAddon.linux.x64.node differ diff --git a/src/native/ffmpeg/ffmpegAddon.win32.x64.node b/src/native/ffmpeg/ffmpegAddon.win32.x64.node new file mode 100644 index 00000000..06054c75 Binary files /dev/null and b/src/native/ffmpeg/ffmpegAddon.win32.x64.node differ diff --git a/src/native/napi2native/napi2native.darwin.arm64.node b/src/native/napi2native/napi2native.darwin.arm64.node new file mode 100644 index 00000000..390a350a Binary files /dev/null and b/src/native/napi2native/napi2native.darwin.arm64.node differ diff --git a/src/native/napi2native/napi2native.linux.arm64.node b/src/native/napi2native/napi2native.linux.arm64.node new file mode 100644 index 00000000..5d724c4d Binary files /dev/null and b/src/native/napi2native/napi2native.linux.arm64.node differ diff --git a/src/native/napi2native/napi2native.linux.x64.node b/src/native/napi2native/napi2native.linux.x64.node new file mode 100644 index 00000000..727fff60 Binary files /dev/null and b/src/native/napi2native/napi2native.linux.x64.node differ diff --git a/src/native/napi2native/napi2native.win32.x64.node b/src/native/napi2native/napi2native.win32.x64.node new file mode 100644 index 00000000..efa0c479 Binary files /dev/null and b/src/native/napi2native/napi2native.win32.x64.node differ diff --git a/src/native/packet/MoeHoo.darwin.arm64.new.node b/src/native/packet/MoeHoo.darwin.arm64.new.node deleted file mode 100644 index 6c2c3e40..00000000 Binary files a/src/native/packet/MoeHoo.darwin.arm64.new.node and /dev/null differ diff --git a/src/native/packet/MoeHoo.darwin.arm64.node b/src/native/packet/MoeHoo.darwin.arm64.node index 829749c4..15fad35d 100644 Binary files a/src/native/packet/MoeHoo.darwin.arm64.node and b/src/native/packet/MoeHoo.darwin.arm64.node differ diff --git a/src/native/packet/MoeHoo.darwin.x64.node b/src/native/packet/MoeHoo.darwin.x64.node deleted file mode 100644 index 66cfff98..00000000 Binary files a/src/native/packet/MoeHoo.darwin.x64.node and /dev/null differ diff --git a/src/native/packet/MoeHoo.linux.arm64.new.node b/src/native/packet/MoeHoo.linux.arm64.new.node deleted file mode 100644 index 64e54db0..00000000 Binary files a/src/native/packet/MoeHoo.linux.arm64.new.node and /dev/null differ diff --git a/src/native/packet/MoeHoo.linux.arm64.node b/src/native/packet/MoeHoo.linux.arm64.node index 4f248425..982f74b7 100644 Binary files a/src/native/packet/MoeHoo.linux.arm64.node and b/src/native/packet/MoeHoo.linux.arm64.node differ diff --git a/src/native/packet/MoeHoo.linux.x64.new.node b/src/native/packet/MoeHoo.linux.x64.new.node deleted file mode 100644 index ec8a577c..00000000 Binary files a/src/native/packet/MoeHoo.linux.x64.new.node and /dev/null differ diff --git a/src/native/packet/MoeHoo.linux.x64.node b/src/native/packet/MoeHoo.linux.x64.node index 98287ac2..27ec1dd4 100644 Binary files a/src/native/packet/MoeHoo.linux.x64.node and b/src/native/packet/MoeHoo.linux.x64.node differ diff --git a/src/native/packet/MoeHoo.win32.x64.new.node b/src/native/packet/MoeHoo.win32.x64.new.node deleted file mode 100644 index d70d3cae..00000000 Binary files a/src/native/packet/MoeHoo.win32.x64.new.node and /dev/null differ diff --git a/src/native/packet/MoeHoo.win32.x64.node b/src/native/packet/MoeHoo.win32.x64.node index 0c5db04e..20733409 100644 Binary files a/src/native/packet/MoeHoo.win32.x64.node and b/src/native/packet/MoeHoo.win32.x64.node differ diff --git a/src/onebot/action/extends/GetUnidirectionalFriendList.ts b/src/onebot/action/extends/GetUnidirectionalFriendList.ts index 96de0f7d..7963568b 100644 --- a/src/onebot/action/extends/GetUnidirectionalFriendList.ts +++ b/src/onebot/action/extends/GetUnidirectionalFriendList.ts @@ -1,4 +1,4 @@ -import { PacketHexStr } from '@/core/packet/transformer/base'; +import { PacketBuf } from '@/core/packet/transformer/base'; import { OneBotAction } from '@/onebot/action/OneBotAction'; import { ActionName } from '@/onebot/action/router'; import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf'; @@ -39,8 +39,8 @@ export class GetUnidirectionalFriendList extends OneBotAction { bytes_cookies: "" }; const packed_data = await this.pack_data(JSON.stringify(req_json)); - const data = Buffer.from(packed_data).toString('hex'); - const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketHexStr }; + const data = Buffer.from(packed_data); + const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketBuf }; const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true); const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data); const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list; diff --git a/src/onebot/action/extends/SendPacket.ts b/src/onebot/action/extends/SendPacket.ts index 479b10f7..2191c23a 100644 --- a/src/onebot/action/extends/SendPacket.ts +++ b/src/onebot/action/extends/SendPacket.ts @@ -1,4 +1,4 @@ -import { PacketHexStr } from '@/core/packet/transformer/base'; +import { PacketBuf } from '@/core/packet/transformer/base'; import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus'; import { ActionName } from '@/onebot/action/router'; import { Static, Type } from '@sinclair/typebox'; @@ -16,7 +16,7 @@ export class SendPacket extends GetPacketStatusDepends { selfInfo.nick = user.nick; diff --git a/src/pty/windowsPtyAgent.ts b/src/pty/windowsPtyAgent.ts index de30a61f..7ffbceb4 100644 --- a/src/pty/windowsPtyAgent.ts +++ b/src/pty/windowsPtyAgent.ts @@ -67,11 +67,11 @@ export class WindowsPtyAgent { } if (this._useConpty) { if (!conptyNative) { - conptyNative = require_dlopen('./pty/' + process.platform + '.' + process.arch + '/conpty.node'); + conptyNative = require_dlopen('./native/pty/' + process.platform + '.' + process.arch + '/conpty.node'); } } else { if (!winptyNative) { - winptyNative = require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node'); + winptyNative = require_dlopen('./native/pty/' + process.platform + '.' + process.arch + '/pty.node'); } } this._ptyNative = this._useConpty ? conptyNative : winptyNative; diff --git a/src/shell/base.ts b/src/shell/base.ts index 708ac2a7..1d0238fe 100644 --- a/src/shell/base.ts +++ b/src/shell/base.ts @@ -31,9 +31,9 @@ import { WebUiDataRuntime } from '@/webui/src/helper/Data'; import { napCatVersion } from '@/common/version'; import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener'; import { sleep } from '@/common/helper'; -import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg'; import { FFmpegService } from '@/common/ffmpeg'; import { connectToNamedPipe } from '@/shell/pipe'; +import { NativePacketHandler } from '@/core/packet/handler/client'; // NapCat Shell App ES 入口文件 async function handleUncaughtExceptions(logger: LogWrapper) { process.on('uncaughtException', (err) => { @@ -313,18 +313,19 @@ export async function NCoreInitShell() { const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); handleUncaughtExceptions(logger); + + // 初始化 FFmpeg 服务 + await FFmpegService.init(pathWrapper.binaryPath, logger); + await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); - 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 basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); + const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 + + nativePacketHandler.onAll((packet) => { + console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data); + }); + await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion()); const o3Service = wrapper.NodeIO3MiscService.get(); o3Service.addO3MiscListener(new NodeIO3MiscListener()); @@ -391,6 +392,7 @@ export async function NCoreInitShell() { selfInfo, basicInfoWrapper, pathWrapper, + nativePacketHandler ).InitNapCat(); } @@ -407,8 +409,10 @@ export class NapCatShell { selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: NapCatPathWrapper, + packetHandler: NativePacketHandler, ) { this.context = { + packetHandler, workingEnv: NapCatCoreWorkingEnv.Shell, wrapper, session, diff --git a/src/webui/index.ts b/src/webui/index.ts index 7c3388f3..730961f1 100644 --- a/src/webui/index.ts +++ b/src/webui/index.ts @@ -93,21 +93,18 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp if (config.token === 'napcat' || !config.token) { const randomToken = getRandomToken(8); await WebUiConfig.UpdateWebUIConfig({ token: randomToken }); - logger.log(`[NapCat] [WebUi] 🔐 检测到默认密码,已自动更新为安全密码`); + logger.log(`[NapCat] [WebUi] 检测到默认密码,已自动更新为安全密码`); // 存储token到全局变量,等待QQ登录成功后发送 setPendingTokenToSend(randomToken); - logger.log(`[NapCat] [WebUi] 📤 新密码将在QQ登录成功后发送给用户`); + logger.log(`[NapCat] [WebUi] 新密码将在QQ登录成功后发送给用户`); // 重新获取更新后的配置 config = await WebUiConfig.GetWebUIConfig(); - } else { - logger.log(`[NapCat] [WebUi] ✅ 当前使用安全密码`); } // 存储启动时的初始token用于鉴权 setInitialWebUiToken(config.token); - logger.log(`[NapCat] [WebUi] 🔑 已缓存启动时的token用于鉴权,运行时手动修改配置文件密码将不会生效`); // 检查是否禁用WebUI if (config.disableWebUI) { @@ -216,7 +213,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp // ------------启动服务------------ server.listen(port, host, async () => { let searchParams = { token: token }; - logger.log(`[NapCat] [WebUi] 🔑 token=${token}`); + logger.log(`[NapCat] [WebUi] WebUi Token: ${token}`); logger.log( `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}` ); diff --git a/vite.config.ts b/vite.config.ts index 65e662d2..86add854 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,8 +31,7 @@ const UniversalBaseConfigPlugin: PluginOption[] = [ targets: [ { src: './manifest.json', dest: 'dist' }, { src: './src/core/external/napcat.json', dest: 'dist/config/' }, - { src: './src/native/packet', dest: 'dist/moehoo', flatten: false }, - { src: './src/native/pty', dest: 'dist/pty', flatten: false }, + { src: './src/native/', dest: 'dist/native', flatten: false }, { src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false }, { src: './src/framework/liteloader.cjs', dest: 'dist' }, { src: './src/framework/napcat.cjs', dest: 'dist' }, @@ -57,10 +56,9 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [ // }), cp({ targets: [ + { src: './src/native/', dest: 'dist/native', flatten: false }, { src: './manifest.json', dest: 'dist' }, { src: './src/core/external/napcat.json', dest: 'dist/config/' }, - { src: './src/native/packet', dest: 'dist/moehoo', flatten: false }, - { src: './src/native/pty', dest: 'dist/pty', flatten: false }, { src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false }, { src: './src/framework/liteloader.cjs', dest: 'dist' }, { src: './src/framework/napcat.cjs', dest: 'dist' }, @@ -82,8 +80,7 @@ const ShellBaseConfigPlugin: PluginOption[] = [ // }), cp({ targets: [ - { src: './src/native/packet', dest: 'dist/moehoo', flatten: false }, - { src: './src/native/pty', dest: 'dist/pty', flatten: false }, + { src: './src/native/', dest: 'dist/native', flatten: false }, { src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false }, { src: './src/core/external/napcat.json', dest: 'dist/config/' }, { src: './package.json', dest: 'dist' },