mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-03 08:39:04 +08:00
feat: 4.9.0 New Capability (#1331)
* fix: get_group_info ownerUin is "0" * Add native module loading and improve logging Loaded a native module in NTQQGroupApi and added test calls to sendSsoCmdReqByContend with different parameter types. Changed fileLog default to true in config. Enhanced NativePacketClient with detailed send/receive logging. Updated NodeIKernelMsgService to accept unknown type for sendSsoCmdReqByContend param. Added process PID logging in napcat shell. * feat: new napcat-4.9.0-beta * feat: Add FFmpeg native addon and TypeScript definitions Introduced FFmpeg Node.js native addon binaries for multiple platforms (darwin-arm64, linux-arm64, linux-x64, win-x64) and added TypeScript type definitions for the addon interface, including video info extraction, duration detection, audio conversion, and PCM decoding. * Remove baseClient and simplify packet client selection Deleted baseClient.ts and moved its logic into nativeClient.ts, making NativePacketClient a standalone class. Refactored PacketClientContext to always use NativePacketClient, removing support for multiple packet backends and related selection logic. Updated binary for napi2native.win32.x64.node. * Remove debug log for process PID in napcat.ts Eliminated an unnecessary console.log statement that printed the process PID. This cleans up the output and removes leftover debugging code. * fix: getQQBuildStr * fix: 简化代码 * refactor: 大幅度调整send * feat: ffmpeg enhance for native node addon * Remove baseClient.ts from packet client module Deleted the src/core/packet/client/baseClient.ts file, which contained the PacketClient class and related interfaces. This may be part of a refactor or cleanup to remove unused or redundant code. * Remove 'bmp24' argument from getVideoInfo call Updated the extractThumbnail method to call addon.getVideoInfo without the 'bmp24' argument, aligning with the updated addon API. * refactor: 重构目录删除旧支持 * feat: raw包能力增强完成 * refactor: 规范化 * feat: packet能力增强 * feat: 9.9.22-40824 & 9.9.22-40768 * Refactor addon path resolution and rename Windows addon Simplifies the logic for resolving the ffmpeg addon path by dynamically constructing the filename from process.platform and process.arch. Also renames the Windows x64 addon file to ffmpegAddon.win32.x64.node for consistency. * Add mappings for 3.2.20 versions in napi2native.json Added send and recv address mappings for 3.2.20-x64 and 3.2.20-arm64 builds to support additional versions in napi2native.json. --------- Co-authored-by: Clansty <i@gao4.pw>
This commit is contained in:
parent
18d0f11320
commit
e37fa59b8c
131
src/common/ffmpeg-adapter-factory.ts
Normal file
131
src/common/ffmpeg-adapter-factory.ts
Normal file
@ -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<IFFmpegAdapter> | 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<IFFmpegAdapter> {
|
||||
// 如果已经初始化,直接返回
|
||||
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<IFFmpegAdapter> {
|
||||
|
||||
// 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<void> {
|
||||
// 如果当前使用的是 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;
|
||||
}
|
||||
}
|
||||
68
src/common/ffmpeg-adapter-interface.ts
Normal file
68
src/common/ffmpeg-adapter-interface.ts
Normal file
@ -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<boolean>;
|
||||
|
||||
/**
|
||||
* 获取视频信息(包含缩略图)
|
||||
* @param videoPath 视频文件路径
|
||||
* @returns 视频信息
|
||||
*/
|
||||
getVideoInfo(videoPath: string): Promise<VideoInfoResult>;
|
||||
|
||||
/**
|
||||
* 获取音视频文件时长
|
||||
* @param filePath 文件路径
|
||||
* @returns 时长(秒)
|
||||
*/
|
||||
getDuration(filePath: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 转换音频为 PCM 格式
|
||||
* @param filePath 输入文件路径
|
||||
* @param pcmPath 输出 PCM 文件路径
|
||||
* @returns PCM 数据 Buffer
|
||||
*/
|
||||
convertToPCM(filePath: string, pcmPath: string): Promise<Buffer>;
|
||||
|
||||
/**
|
||||
* 转换音频文件
|
||||
* @param inputFile 输入文件路径
|
||||
* @param outputFile 输出文件路径
|
||||
* @param format 目标格式 ('amr' | 'silk' 等)
|
||||
*/
|
||||
convertFile(inputFile: string, outputFile: string, format: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 提取视频缩略图
|
||||
* @param videoPath 视频文件路径
|
||||
* @param thumbnailPath 缩略图输出路径
|
||||
*/
|
||||
extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void>;
|
||||
}
|
||||
129
src/common/ffmpeg-addon-adapter.ts
Normal file
129
src/common/ffmpeg-addon-adapter.ts
Normal file
@ -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<boolean> {
|
||||
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<VideoInfoResult> {
|
||||
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<number> {
|
||||
const addon = this.ensureAddon();
|
||||
return addon.getDuration(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM
|
||||
*/
|
||||
async convertToPCM(filePath: string, pcmPath: string): Promise<Buffer> {
|
||||
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<void> {
|
||||
const addon = this.ensureAddon();
|
||||
|
||||
if (format === 'silk' || format === 'ntsilk') {
|
||||
// 使用 Addon 的 NTSILK 转换
|
||||
await addon.convertToNTSilkTct(inputFile, outputFile);
|
||||
} else {
|
||||
throw new Error(`Format '${format}' is not supported by FFmpeg Addon`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取缩略图
|
||||
*/
|
||||
async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
|
||||
const addon = this.ensureAddon();
|
||||
const info = await addon.getVideoInfo(videoPath);
|
||||
|
||||
// 将缩略图写入文件
|
||||
await writeFile(thumbnailPath, info.image);
|
||||
}
|
||||
}
|
||||
71
src/common/ffmpeg-addon.ts
Normal file
71
src/common/ffmpeg-addon.ts
Normal file
@ -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<VideoInfo>;
|
||||
|
||||
/**
|
||||
* Get duration of audio or video file in seconds
|
||||
*/
|
||||
getDuration(filePath: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* Convert audio file to NTSILK format (WeChat voice message format)
|
||||
*/
|
||||
convertToNTSilkTct(inputPath: string, outputPath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Decode audio file to raw PCM data
|
||||
*/
|
||||
decodeAudioToPCM(filePath: string): Promise<AudioPCMResult>;
|
||||
}
|
||||
244
src/common/ffmpeg-exec-adapter.ts
Normal file
244
src/common/ffmpeg-exec-adapter.ts
Normal file
@ -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<boolean> {
|
||||
// 首先检查当前路径
|
||||
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<VideoInfoResult> {
|
||||
// 获取文件大小和类型
|
||||
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<number> {
|
||||
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<Buffer> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<void> {
|
||||
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<IFFmpegAdapter> {
|
||||
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<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 });
|
||||
|
||||
// 更新适配器路径
|
||||
await FFmpegAdapterFactory.updateFFmpegPath(logger, FFMPEG_CMD, FFPROBE_CMD);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取视频缩略图
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.extractThumbnail(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}`);
|
||||
}
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.convertFile(inputFile, outputFile, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM 格式
|
||||
*/
|
||||
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 adapter = await this.getAdapter();
|
||||
return adapter.convertToPCM(filePath, pcmPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频信息
|
||||
*/
|
||||
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
|
||||
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<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; // 默认时长
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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';
|
||||
|
||||
2
src/core/external/appid.json
vendored
2
src/core/external/appid.json
vendored
@ -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"
|
||||
},
|
||||
|
||||
38
src/core/external/napi2native.json
vendored
Normal file
38
src/core/external/napi2native.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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<string, (json: RecvPacketData) => Promise<any> | 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<void>;
|
||||
|
||||
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<RecvPacketData> {
|
||||
return new Promise<RecvPacketData>((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<RecvPacketData> {
|
||||
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<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp, timeout);
|
||||
}
|
||||
}
|
||||
@ -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<string, (json: RecvPacketData) => Promise<any> | 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<number, string>(); // seq - hash
|
||||
private readonly timeEvent = new Map<string, NodeJS.Timeout>(); // 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<void> {
|
||||
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<RecvPacketData> {
|
||||
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<RecvPacketData>((_, 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<RecvPacketData> {
|
||||
return await this.sendPacket(pkt.cmd, pkt.data, rsp, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<T extends boolean = false>(pkt: OidbPacket, rsp?: T, timeout?: number): Promise<T extends true ? Buffer : void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<typeof IndexNode>,timeout: number = 20000) {
|
||||
async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, 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<typeof IndexNode>, timeout: number = 20000) {
|
||||
async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, 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<typeof IndexNode>, timeout: number = 20000) {
|
||||
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, 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<typeof IndexNode>, timeout: number = 20000) {
|
||||
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, 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);
|
||||
|
||||
214
src/core/packet/handler/client.ts
Normal file
214
src/core/packet/handler/client.ts
Normal file
@ -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<string, Set<ListenerEntry>> = 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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<typeof proto.MiniAppAdaptShareInfoResp> {
|
||||
@ -41,7 +41,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
|
||||
});
|
||||
return {
|
||||
cmd: 'LightAppSvc.mini_app_share.AdaptShareInfo',
|
||||
data: PacketHexStrBuilder(data)
|
||||
data: PacketBufBuilder(data)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { NapProtoDecodeStructType } from '@napneko/nap-proto-core';
|
||||
import { PacketMsgBuilder } from '@/core/packet/message/builder';
|
||||
|
||||
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
||||
export type PacketBuf = Buffer & { readonly hexNya: unique symbol };
|
||||
|
||||
export const PacketHexStrBuilder = (str: Uint8Array): PacketHexStr => {
|
||||
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<T> {
|
||||
|
||||
@ -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<typeof proto.HttpConn0x6ff_501Response> {
|
||||
constructor() {
|
||||
@ -25,7 +25,7 @@ class FetchSessionKey extends PacketTransformer<typeof proto.HttpConn0x6ff_501Re
|
||||
});
|
||||
return {
|
||||
cmd: 'HttpConn.0x6ff_501',
|
||||
data: PacketHexStrBuilder(req)
|
||||
data: PacketBufBuilder(req)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 DownloadForwardMsg extends PacketTransformer<typeof proto.RecvLongMsgResp> {
|
||||
constructor() {
|
||||
@ -25,7 +25,7 @@ class DownloadForwardMsg extends PacketTransformer<typeof proto.RecvLongMsgResp>
|
||||
});
|
||||
return {
|
||||
cmd: 'trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg',
|
||||
data: PacketHexStrBuilder(req)
|
||||
data: PacketBufBuilder(req)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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<typeof proto.SsoGetC2cMsgResponse> {
|
||||
constructor() {
|
||||
@ -15,7 +15,7 @@ class FetchC2CMessage extends PacketTransformer<typeof proto.SsoGetC2cMsgRespons
|
||||
});
|
||||
return {
|
||||
cmd: 'trpc.msg.register_proxy.RegisterProxy.SsoGetC2cMsg',
|
||||
data: PacketHexStrBuilder(req)
|
||||
data: PacketBufBuilder(req)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 FetchGroupMessage extends PacketTransformer<typeof proto.SsoGetGroupMsgResponse> {
|
||||
constructor() {
|
||||
@ -18,7 +18,7 @@ class FetchGroupMessage extends PacketTransformer<typeof proto.SsoGetGroupMsgRes
|
||||
});
|
||||
return {
|
||||
cmd: 'trpc.msg.register_proxy.RegisterProxy.SsoGetGroupMsg',
|
||||
data: PacketHexStrBuilder(req)
|
||||
data: PacketBufBuilder(req)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import zlib from 'node:zlib';
|
||||
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 { PacketMsg } from '@/core/packet/message/message';
|
||||
|
||||
class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
|
||||
@ -39,7 +39,7 @@ class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
|
||||
);
|
||||
return {
|
||||
cmd: 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg',
|
||||
data: PacketHexStrBuilder(req)
|
||||
data: PacketBufBuilder(req)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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<typeof proto.OidbSvcTrpcTcpBase> {
|
||||
constructor() {
|
||||
@ -16,7 +16,7 @@ class OidbBase extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
|
||||
});
|
||||
return {
|
||||
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
|
||||
data: PacketHexStrBuilder(data),
|
||||
data: PacketBufBuilder(data),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -585,7 +585,7 @@ export interface NodeIKernelMsgService {
|
||||
|
||||
prepareTempChat(args: unknown): unknown;
|
||||
|
||||
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>;
|
||||
sendSsoCmdReqByContend(cmd: string, param: unknown): Promise<unknown>;
|
||||
|
||||
getTempChatInfo(ChatType: number, Uid: string): Promise<TmpChatInfoApi>;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
BIN
src/native/ffmpeg/ffmpegAddon.darwin.arm64.node
Normal file
BIN
src/native/ffmpeg/ffmpegAddon.darwin.arm64.node
Normal file
Binary file not shown.
BIN
src/native/ffmpeg/ffmpegAddon.linux.arm64.node
Normal file
BIN
src/native/ffmpeg/ffmpegAddon.linux.arm64.node
Normal file
Binary file not shown.
BIN
src/native/ffmpeg/ffmpegAddon.linux.x64.node
Normal file
BIN
src/native/ffmpeg/ffmpegAddon.linux.x64.node
Normal file
Binary file not shown.
BIN
src/native/ffmpeg/ffmpegAddon.win32.x64.node
Normal file
BIN
src/native/ffmpeg/ffmpegAddon.win32.x64.node
Normal file
Binary file not shown.
BIN
src/native/napi2native/napi2native.darwin.arm64.node
Normal file
BIN
src/native/napi2native/napi2native.darwin.arm64.node
Normal file
Binary file not shown.
BIN
src/native/napi2native/napi2native.linux.arm64.node
Normal file
BIN
src/native/napi2native/napi2native.linux.arm64.node
Normal file
Binary file not shown.
BIN
src/native/napi2native/napi2native.linux.x64.node
Normal file
BIN
src/native/napi2native/napi2native.linux.x64.node
Normal file
Binary file not shown.
BIN
src/native/napi2native/napi2native.win32.x64.node
Normal file
BIN
src/native/napi2native/napi2native.win32.x64.node
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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<void, Friend[]> {
|
||||
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;
|
||||
|
||||
@ -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<Payload, string | undefin
|
||||
override actionName = ActionName.SendPacket;
|
||||
async _handle(payload: Payload) {
|
||||
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
|
||||
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: payload.data as PacketHexStr }, rsp);
|
||||
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: Buffer.from(payload.data, 'hex') as PacketBuf }, rsp);
|
||||
return typeof data === 'object' ? data.toString('hex') : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +102,6 @@ export class NapCatOneBot11Adapter {
|
||||
async InitOneBot () {
|
||||
const selfInfo = this.core.selfInfo;
|
||||
const ob11Config = this.configLoader.configData;
|
||||
|
||||
this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid, false)
|
||||
.then(async (user) => {
|
||||
selfInfo.nick = user.nick;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)}`
|
||||
);
|
||||
|
||||
@ -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' },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user