feat: ffmpeg enhance for native node addon

This commit is contained in:
手瓜一十雪 2025-10-30 09:06:48 +08:00
parent 9a35ee9cd1
commit 803b1a6c77
10 changed files with 695 additions and 181 deletions

View 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;
}
}

View 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>;
}

View File

@ -0,0 +1,137 @@
/**
* 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;
if (platformName === 'win32' && archName === 'x64') {
addonFileName = 'ffmpegAddon.win.x64.node';
} else if (platformName === 'linux' && archName === 'x64') {
addonFileName = 'ffmpegAddon.linux.x64.node';
} else if (platformName === 'linux' && archName === 'arm64') {
addonFileName = 'ffmpegAddon.linux.arm64.node';
} else if (platformName === 'darwin' && archName === 'arm64') {
addonFileName = 'ffmpegAddon.darwin.arm64.node';
} else {
throw new Error(`Unsupported platform: ${platformName} ${archName}`);
}
return path.join(binaryPath, "./nodeffmpeg/", addonFileName);
}
/**
* 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, 'bmp24');
// 将缩略图写入文件
await writeFile(thumbnailPath, info.image);
}
}

View 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}`);
}
}
}

View File

@ -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; // 默认时长
}
}
}

View File

@ -4,7 +4,7 @@ import { Type, Static } from '@sinclair/typebox';
import { AnySchema } from 'ajv';
export const NapcatConfigSchema = Type.Object({
fileLog: Type.Boolean({ default: true }),
fileLog: Type.Boolean({ default: false }),
consoleLog: Type.Boolean({ default: true }),
fileLogLevel: Type.String({ default: 'debug' }),
consoleLogLevel: Type.String({ default: 'info' }),

View File

@ -9,7 +9,6 @@ import { NodeIKernelLoginService } from '@/core/services';
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui';
import { NapCatOneBot11Adapter } from '@/onebot';
import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg';
import { FFmpegService } from '@/common/ffmpeg';
//Framework ES入口文件
@ -38,15 +37,9 @@ 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);
});
}
// 初始化 FFmpeg 服务
await FFmpegService.init(pathWrapper.binaryPath, logger);
//直到登录成功后,执行下一步
// const selfInfo = {
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',

View File

@ -31,7 +31,6 @@ 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';
// NapCat Shell App ES 入口文件
@ -313,16 +312,11 @@ 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());

View File

@ -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)}`
);

View File

@ -82,6 +82,7 @@ const ShellBaseConfigPlugin: PluginOption[] = [
// }),
cp({
targets: [
{ src: './src/native/ffmpeg', dest: 'dist/nodeffmpeg', flatten: false },
{ 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 },