SnowCat 雪猫底层全源码:NapCat 4.18.28 底座 + SnowLuma 能力移植 + V8 通道适配 9.9.37
26 包 monorepo(TS 全源码): - snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包 - snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线 - snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI) - snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports) - snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时 - 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏) - 工具件: 解包器/启动器/便携制作器/doctor GitHub 关联已全量指向 DandanLLab/SnowCatQQ
This commit is contained in:
commit
b9655a5ab1
1048 files changed
+130851
No files matched your search
@@ -0,0 +1,228 @@
|
||||
import { NapCatOneBot11Adapter } from '@/snowcat-onebot/index';
|
||||
import { encodeSilk } from '@/snowcat-core/helper/audio';
|
||||
import { FFmpegService } from '@/snowcat-core/helper/ffmpeg/ffmpeg';
|
||||
import { calculateFileMD5 } from 'snowcat-common/src/file';
|
||||
import { ElementType, SnowCatCore, NTVideoType, PicElement, PicSubType, SendFileElement, SendPicElement, SendPttElement, SendVideoElement } from 'snowcat-core';
|
||||
import { getFileTypeForSendType } from 'snowcat-core/helper/msg';
|
||||
import { imageSizeFallBack } from 'snowcat-image-size';
|
||||
import { SendMessageContext } from './msg';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import pathLib from 'node:path';
|
||||
import fsPromises from 'fs/promises';
|
||||
import fs from 'fs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { defaultVideoThumbB64 } from '@/snowcat-core/helper/ffmpeg/video';
|
||||
|
||||
const videoFormatByExtension: Partial<Record<string, NTVideoType>> = {
|
||||
afs: NTVideoType.VIDEO_FORMAT_AFS,
|
||||
avi: NTVideoType.VIDEO_FORMAT_AVI,
|
||||
mkv: NTVideoType.VIDEO_FORMAT_MKV,
|
||||
mod: NTVideoType.VIDEO_FORMAT_MOD,
|
||||
mov: NTVideoType.VIDEO_FORMAT_MOV,
|
||||
mp4: NTVideoType.VIDEO_FORMAT_MP4,
|
||||
mts: NTVideoType.VIDEO_FORMAT_MTS,
|
||||
rm: NTVideoType.VIDEO_FORMAT_RM,
|
||||
rmvb: NTVideoType.VIDEO_FORMAT_RMVB,
|
||||
ts: NTVideoType.VIDEO_FORMAT_TS,
|
||||
wmv: NTVideoType.VIDEO_FORMAT_WMV,
|
||||
};
|
||||
|
||||
export class OneBotFileApi {
|
||||
obContext: NapCatOneBot11Adapter;
|
||||
core: SnowCatCore;
|
||||
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
|
||||
this.obContext = obContext;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async createValidSendFileElement (context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = '', uploadGroupFile: boolean = false, _isLocal: boolean = false): Promise<SendFileElement> {
|
||||
const {
|
||||
fileName: _fileName,
|
||||
path,
|
||||
fileSize,
|
||||
} = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE, 0, uploadGroupFile);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (uploadGroupFile) {
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
fileElement: {
|
||||
fileName: fileName || _fileName,
|
||||
folderId,
|
||||
filePath: path,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPicElement (context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0, _isLocal: boolean = false): Promise<SendPicElement> {
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const imageSize = await imageSizeFallBack(picPath);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
picElement: {
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
picWidth: imageSize.width,
|
||||
picHeight: imageSize.height,
|
||||
fileName,
|
||||
sourcePath: path,
|
||||
original: true,
|
||||
picType: await getFileTypeForSendType(picPath),
|
||||
picSubType: subType,
|
||||
fileUuid: '',
|
||||
fileSubId: '',
|
||||
thumbFileSize: 0,
|
||||
summary,
|
||||
} as PicElement,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement (context: SendMessageContext, filePath: string, fileName: string = '', _diyThumbPath: string = '', isLocal: boolean = false): Promise<SendVideoElement> {
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
time: 15,
|
||||
format: 'mp4',
|
||||
size: 0,
|
||||
filePath,
|
||||
};
|
||||
let fileExt = 'mp4';
|
||||
try {
|
||||
const tempExt = (await fileTypeFromFile(filePath))?.ext;
|
||||
if (tempExt) fileExt = tempExt;
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('获取文件类型失败', e);
|
||||
}
|
||||
const currentExt = pathLib.extname(filePath).slice(1).toLowerCase();
|
||||
const needsExtCopy = currentExt !== fileExt.toLowerCase();
|
||||
if (needsExtCopy) {
|
||||
if (isLocal) {
|
||||
// 本地文件缺少正确扩展名:拷贝到临时目录,避免污染用户目录
|
||||
const tempFilePath = pathLib.join(this.core.SnowCatTempPath, `${randomUUID()}.${fileExt}`);
|
||||
fs.copyFileSync(filePath, tempFilePath);
|
||||
context.deleteAfterSentFiles.push(tempFilePath);
|
||||
filePath = tempFilePath;
|
||||
} else {
|
||||
// 临时文件缺少正确扩展名:在原位置加扩展名
|
||||
const newFilePath = `${filePath}.${fileExt}`;
|
||||
fs.copyFileSync(filePath, newFilePath);
|
||||
context.deleteAfterSentFiles.push(newFilePath);
|
||||
filePath = newFilePath;
|
||||
}
|
||||
}
|
||||
// 扩展名已匹配时:本地文件直接用原始路径(零拷贝),临时文件也无需额外操作
|
||||
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
|
||||
fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true });
|
||||
const usePacketVideoMetadata = context.usePacketVideoMetadata === true;
|
||||
const thumbExtension = usePacketVideoMetadata ? 'jpg' : 'png';
|
||||
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.${thumbExtension}`);
|
||||
try {
|
||||
if (usePacketVideoMetadata) {
|
||||
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
|
||||
} else {
|
||||
videoInfo = await FFmpegService.getVideoInfo(filePath, '');
|
||||
await FFmpegService.extractLegacyVideoThumbnail(filePath, thumbPath);
|
||||
}
|
||||
if (!fs.existsSync(thumbPath)) {
|
||||
this.core.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
|
||||
throw new Error('获取视频缩略图失败');
|
||||
}
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('获取视频信息失败', e);
|
||||
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||
}
|
||||
if (_diyThumbPath) {
|
||||
try {
|
||||
await this.core.apis.FileApi.copyFile(_diyThumbPath, thumbPath);
|
||||
} catch (e) {
|
||||
this.core.context.logger.logError('复制自定义缩略图失败', e);
|
||||
}
|
||||
}
|
||||
context.deleteAfterSentFiles.push(thumbPath);
|
||||
const thumbSize = (await fsPromises.stat(thumbPath)).size;
|
||||
const thumbMd5 = await calculateFileMD5(thumbPath);
|
||||
|
||||
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`;
|
||||
const videoMetadata = usePacketVideoMetadata
|
||||
? {
|
||||
fileTime: Math.max(0, Math.round(videoInfo.time)),
|
||||
fileFormat: videoFormatByExtension[fileExt.toLowerCase()] ?? NTVideoType.VIDEO_FORMAT_MP4,
|
||||
fileWidth: videoInfo.width,
|
||||
fileHeight: videoInfo.height,
|
||||
}
|
||||
: {
|
||||
fileTime: videoInfo.time,
|
||||
};
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
videoElement: {
|
||||
fileName: uploadName,
|
||||
filePath: path,
|
||||
videoMd5: md5,
|
||||
thumbMd5,
|
||||
...videoMetadata,
|
||||
thumbPath: new Map([[0, thumbPath]]),
|
||||
thumbSize,
|
||||
thumbWidth: videoInfo.width,
|
||||
thumbHeight: videoInfo.height,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPttElement (_context: SendMessageContext, pttPath: string): Promise<SendPttElement> {
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.SnowCatTempPath, this.core.context.logger);
|
||||
if (!silkPath) {
|
||||
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
||||
}
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(silkPath, ElementType.PTT);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath).then().catch((e) => this.core.context.logger.logError('删除临时文件失败', e));
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
elementId: '',
|
||||
pttElement: {
|
||||
fileName,
|
||||
filePath: path,
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
duration: duration ?? 1,
|
||||
formatType: 1,
|
||||
voiceType: 1,
|
||||
voiceChangeType: 0,
|
||||
canConvert2Text: true,
|
||||
waveAmplitudes: [
|
||||
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
|
||||
],
|
||||
fileSubId: '',
|
||||
playState: 1,
|
||||
autoConvertText: 0,
|
||||
storeID: 0,
|
||||
otherBusinessInfo: {
|
||||
aiVoiceType: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user