mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-20 13:40:10 +08:00
feat: ffmpeg enhance for native node addon
This commit is contained in:
parent
9a35ee9cd1
commit
803b1a6c77
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>;
|
||||||
|
}
|
||||||
137
src/common/ffmpeg-addon-adapter.ts
Normal file
137
src/common/ffmpeg-addon-adapter.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
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 { statSync, existsSync, writeFileSync } from 'fs';
|
||||||
import path, { dirname } from 'path';
|
import path from 'path';
|
||||||
import { execFile } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import type { VideoInfo } from './video';
|
import type { VideoInfo } from './video';
|
||||||
import { fileTypeFromFile } from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { platform } from 'node:os';
|
import { platform } from 'node:os';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
import { imageSizeFallBack } from '@/image-size';
|
import { FFmpegAdapterFactory } from './ffmpeg-adapter-factory';
|
||||||
const currentPath = dirname(fileURLToPath(import.meta.url));
|
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
const getFFmpegPath = (tool: string): string => {
|
const getFFmpegPath = (tool: string, binaryPath?: string): string => {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32' && binaryPath) {
|
||||||
const exeName = `${tool}.exe`;
|
const exeName = `${tool}.exe`;
|
||||||
const isLocalExeExists = existsSync(path.join(currentPath, 'ffmpeg', exeName));
|
const localPath = path.join(binaryPath, 'ffmpeg', exeName);
|
||||||
return isLocalExeExists ? path.join(currentPath, 'ffmpeg', exeName) : exeName;
|
const isLocalExeExists = existsSync(localPath);
|
||||||
|
return isLocalExeExists ? localPath : exeName;
|
||||||
}
|
}
|
||||||
return tool;
|
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 {
|
export class FFmpegService {
|
||||||
// 确保目标目录存在
|
private static adapter: IFFmpegAdapter | null = null;
|
||||||
public static setFfmpegPath(ffmpegPath: string,logger:LogWrapper): void {
|
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') {
|
if (platform() === 'win32') {
|
||||||
FFMPEG_CMD = path.join(ffmpegPath, 'ffmpeg.exe');
|
FFMPEG_CMD = path.join(ffmpegPath, 'ffmpeg.exe');
|
||||||
FFPROBE_CMD = path.join(ffmpegPath, 'ffprobe.exe');
|
FFPROBE_CMD = path.join(ffmpegPath, 'ffprobe.exe');
|
||||||
logger.log('[Check] ffmpeg:', FFMPEG_CMD);
|
logger.log('[Check] ffmpeg:', FFMPEG_CMD);
|
||||||
logger.log('[Check] ffprobe:', FFPROBE_CMD);
|
logger.log('[Check] ffprobe:', FFPROBE_CMD);
|
||||||
}
|
|
||||||
}
|
// 更新适配器路径
|
||||||
private static ensureDirExists(filePath: string): void {
|
await FFmpegAdapterFactory.updateFFmpegPath(logger, FFMPEG_CMD, FFPROBE_CMD);
|
||||||
const dir = dirname(filePath);
|
|
||||||
if (!existsSync(dir)) {
|
|
||||||
mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取视频缩略图
|
||||||
|
*/
|
||||||
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
|
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
|
||||||
try {
|
const adapter = await this.getAdapter();
|
||||||
this.ensureDirExists(thumbnailPath);
|
await adapter.extractThumbnail(videoPath, 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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换音频文件
|
||||||
|
*/
|
||||||
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
|
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||||
try {
|
const adapter = await this.getAdapter();
|
||||||
this.ensureDirExists(outputFile);
|
await adapter.convertFile(inputFile, outputFile, format);
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 PCM 格式
|
||||||
|
*/
|
||||||
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
|
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
|
||||||
try {
|
const adapter = await this.getAdapter();
|
||||||
this.ensureDirExists(pcmPath);
|
return adapter.convertToPCM(filePath, 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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取视频信息
|
||||||
|
*/
|
||||||
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
|
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
|
||||||
|
const adapter = await this.getAdapter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 并行执行获取文件信息和提取缩略图
|
// 获取文件大小
|
||||||
const [fileInfo, duration] = await Promise.all([
|
const fileSize = statSync(videoPath).size;
|
||||||
this.getFileInfo(videoPath, thumbnailPath),
|
|
||||||
this.getVideoDuration(videoPath)
|
// 使用适配器获取视频信息
|
||||||
]);
|
const videoInfo = await adapter.getVideoInfo(videoPath);
|
||||||
|
|
||||||
|
// 如果提供了缩略图路径且适配器返回了缩略图,保存到指定路径
|
||||||
|
if (thumbnailPath && videoInfo.thumbnail) {
|
||||||
|
writeFileSync(thumbnailPath, videoInfo.thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
const result: VideoInfo = {
|
const result: VideoInfo = {
|
||||||
width: fileInfo.width,
|
width: videoInfo.width,
|
||||||
height: fileInfo.height,
|
height: videoInfo.height,
|
||||||
time: duration,
|
time: videoInfo.duration,
|
||||||
format: fileInfo.format,
|
format: videoInfo.format,
|
||||||
size: fileInfo.size,
|
size: fileSize,
|
||||||
filePath: videoPath
|
filePath: videoPath
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
// 降级处理:返回默认值
|
||||||
}
|
const fileType = await fileTypeFromFile(videoPath).catch(() => null);
|
||||||
}
|
const fileSize = statSync(videoPath).size;
|
||||||
|
|
||||||
private static async getFileInfo(videoPath: string, thumbnailPath: string): Promise<{
|
|
||||||
format: string,
|
|
||||||
size: number,
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
}> {
|
|
||||||
|
|
||||||
// 获取文件大小和类型
|
|
||||||
const [fileType, fileSize] = await Promise.all([
|
|
||||||
fileTypeFromFile(videoPath).catch(() => {
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
Promise.resolve(statSync(videoPath).size)
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.extractThumbnail(videoPath, thumbnailPath);
|
|
||||||
// 获取图片尺寸
|
|
||||||
const dimensions = await imageSizeFallBack(thumbnailPath);
|
|
||||||
|
|
||||||
return {
|
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,
|
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; // 默认时长
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@ import { Type, Static } from '@sinclair/typebox';
|
|||||||
import { AnySchema } from 'ajv';
|
import { AnySchema } from 'ajv';
|
||||||
|
|
||||||
export const NapcatConfigSchema = Type.Object({
|
export const NapcatConfigSchema = Type.Object({
|
||||||
fileLog: Type.Boolean({ default: true }),
|
fileLog: Type.Boolean({ default: false }),
|
||||||
consoleLog: Type.Boolean({ default: true }),
|
consoleLog: Type.Boolean({ default: true }),
|
||||||
fileLogLevel: Type.String({ default: 'debug' }),
|
fileLogLevel: Type.String({ default: 'debug' }),
|
||||||
consoleLogLevel: Type.String({ default: 'info' }),
|
consoleLogLevel: Type.String({ default: 'info' }),
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { NodeIKernelLoginService } from '@/core/services';
|
|||||||
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
|
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
|
||||||
import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui';
|
import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui';
|
||||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||||
import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg';
|
|
||||||
import { FFmpegService } from '@/common/ffmpeg';
|
import { FFmpegService } from '@/common/ffmpeg';
|
||||||
|
|
||||||
//Framework ES入口文件
|
//Framework ES入口文件
|
||||||
@ -38,15 +37,9 @@ export async function NCoreInitFramework(
|
|||||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
||||||
if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
|
|
||||||
downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
|
// 初始化 FFmpeg 服务
|
||||||
if (reset && path) {
|
await FFmpegService.init(pathWrapper.binaryPath, logger);
|
||||||
FFmpegService.setFfmpegPath(path, logger);
|
|
||||||
}
|
|
||||||
}).catch(e => {
|
|
||||||
logger.logError('[Ffmpeg] Error:', e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//直到登录成功后,执行下一步
|
//直到登录成功后,执行下一步
|
||||||
// const selfInfo = {
|
// const selfInfo = {
|
||||||
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',
|
// uid: 'u_FUSS0_x06S_9Tf4na_WpUg',
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import { WebUiDataRuntime } from '@/webui/src/helper/Data';
|
|||||||
import { napCatVersion } from '@/common/version';
|
import { napCatVersion } from '@/common/version';
|
||||||
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
|
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
|
||||||
import { sleep } from '@/common/helper';
|
import { sleep } from '@/common/helper';
|
||||||
import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg';
|
|
||||||
import { FFmpegService } from '@/common/ffmpeg';
|
import { FFmpegService } from '@/common/ffmpeg';
|
||||||
import { connectToNamedPipe } from '@/shell/pipe';
|
import { connectToNamedPipe } from '@/shell/pipe';
|
||||||
// NapCat Shell App ES 入口文件
|
// NapCat Shell App ES 入口文件
|
||||||
@ -313,16 +312,11 @@ export async function NCoreInitShell() {
|
|||||||
const pathWrapper = new NapCatPathWrapper();
|
const pathWrapper = new NapCatPathWrapper();
|
||||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||||
handleUncaughtExceptions(logger);
|
handleUncaughtExceptions(logger);
|
||||||
|
|
||||||
|
// 初始化 FFmpeg 服务
|
||||||
|
await FFmpegService.init(pathWrapper.binaryPath, logger);
|
||||||
|
|
||||||
await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
|
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 basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion());
|
||||||
|
|
||||||
|
|||||||
@ -93,21 +93,18 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
if (config.token === 'napcat' || !config.token) {
|
if (config.token === 'napcat' || !config.token) {
|
||||||
const randomToken = getRandomToken(8);
|
const randomToken = getRandomToken(8);
|
||||||
await WebUiConfig.UpdateWebUIConfig({ token: randomToken });
|
await WebUiConfig.UpdateWebUIConfig({ token: randomToken });
|
||||||
logger.log(`[NapCat] [WebUi] 🔐 检测到默认密码,已自动更新为安全密码`);
|
logger.log(`[NapCat] [WebUi] 检测到默认密码,已自动更新为安全密码`);
|
||||||
|
|
||||||
// 存储token到全局变量,等待QQ登录成功后发送
|
// 存储token到全局变量,等待QQ登录成功后发送
|
||||||
setPendingTokenToSend(randomToken);
|
setPendingTokenToSend(randomToken);
|
||||||
logger.log(`[NapCat] [WebUi] 📤 新密码将在QQ登录成功后发送给用户`);
|
logger.log(`[NapCat] [WebUi] 新密码将在QQ登录成功后发送给用户`);
|
||||||
|
|
||||||
// 重新获取更新后的配置
|
// 重新获取更新后的配置
|
||||||
config = await WebUiConfig.GetWebUIConfig();
|
config = await WebUiConfig.GetWebUIConfig();
|
||||||
} else {
|
|
||||||
logger.log(`[NapCat] [WebUi] ✅ 当前使用安全密码`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储启动时的初始token用于鉴权
|
// 存储启动时的初始token用于鉴权
|
||||||
setInitialWebUiToken(config.token);
|
setInitialWebUiToken(config.token);
|
||||||
logger.log(`[NapCat] [WebUi] 🔑 已缓存启动时的token用于鉴权,运行时手动修改配置文件密码将不会生效`);
|
|
||||||
|
|
||||||
// 检查是否禁用WebUI
|
// 检查是否禁用WebUI
|
||||||
if (config.disableWebUI) {
|
if (config.disableWebUI) {
|
||||||
@ -216,7 +213,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
// ------------启动服务------------
|
// ------------启动服务------------
|
||||||
server.listen(port, host, async () => {
|
server.listen(port, host, async () => {
|
||||||
let searchParams = { token: token };
|
let searchParams = { token: token };
|
||||||
logger.log(`[NapCat] [WebUi] 🔑 token=${token}`);
|
logger.log(`[NapCat] [WebUi] WebUi Token: ${token}`);
|
||||||
logger.log(
|
logger.log(
|
||||||
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
|
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -82,6 +82,7 @@ const ShellBaseConfigPlugin: PluginOption[] = [
|
|||||||
// }),
|
// }),
|
||||||
cp({
|
cp({
|
||||||
targets: [
|
targets: [
|
||||||
|
{ src: './src/native/ffmpeg', dest: 'dist/nodeffmpeg', flatten: false },
|
||||||
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },
|
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },
|
||||||
{ src: './src/native/pty', dest: 'dist/pty', flatten: false },
|
{ src: './src/native/pty', dest: 'dist/pty', flatten: false },
|
||||||
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
|
{ src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user