From 8df54d5cd31bb18fb4b4f9ac1ea67e922da30acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sat, 15 Nov 2025 11:17:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8E=BBcore=E8=80=A6=E5=90=88onebot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved file element creation methods (for file, picture, video, and ptt) from napcat-core/apis/file.ts to a new OneBotFileApi class in napcat-onebot/api/file.ts. Updated package.json dependencies to remove unused packages and fix workspace references. This improves separation of concerns and modularity between core and onebot-specific logic. --- packages/napcat-core/apis/file.ts | 170 --------------------------- packages/napcat-core/package.json | 5 +- packages/napcat-onebot/api/file.ts | 182 +++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 174 deletions(-) create mode 100644 packages/napcat-onebot/api/file.ts diff --git a/packages/napcat-core/apis/file.ts b/packages/napcat-core/apis/file.ts index f0df201a..4817e55a 100644 --- a/packages/napcat-core/apis/file.ts +++ b/packages/napcat-core/apis/file.ts @@ -5,12 +5,7 @@ import { IMAGE_HTTP_HOST_NT, Peer, PicElement, - PicSubType, RawMessage, - SendFileElement, - SendPicElement, - SendPttElement, - SendVideoElement, } from '@/napcat-core/types'; import path from 'path'; import fs from 'fs'; @@ -19,16 +14,9 @@ import { InstanceContext, NapCatCore, SearchResultItem } from '@/napcat-core/ind import { fileTypeFromFile } from 'file-type'; import { RkeyManager } from '@/napcat-core/helper/rkey'; import { calculateFileMD5 } from 'napcat-common/src/file'; -import pathLib from 'node:path'; -import { defaultVideoThumbB64 } from 'napcat-common/src/video'; -import { encodeSilk } from 'napcat-common/src/audio'; -import { SendMessageContext } from 'napcat-onebot/api/msg'; -import { getFileTypeForSendType } from '../helper/msg'; -import { FFmpegService } from 'napcat-common/src/ffmpeg'; import { rkeyDataType } from '../types/file'; import { NapProtoMsg } from 'napcat-protobuf'; import { FileId } from '../packet/transformer/proto/misc/fileid'; -import { imageSizeFallBack } from 'napcat-image-size'; export class NTQQFileApi { context: InstanceContext; @@ -181,164 +169,6 @@ export class NTQQFileApi { }; } - async createValidSendFileElement (context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = ''): Promise { - const { - fileName: _fileName, - path, - fileSize, - } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE); - if (fileSize === 0) { - throw new Error('文件异常,大小为0'); - } - 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): Promise { - 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 = ''): Promise { - 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.context.logger.logError('获取文件类型失败', e); - } - 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 thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`); - try { - videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath); - if (!fs.existsSync(thumbPath)) { - this.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在')); - throw new Error('获取视频缩略图失败'); - } - } catch (e) { - this.context.logger.logError('获取视频信息失败', e); - fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64')); - } - if (_diyThumbPath) { - try { - await this.copyFile(_diyThumbPath, thumbPath); - } catch (e) { - this.context.logger.logError('复制自定义缩略图失败', e); - } - } - context.deleteAfterSentFiles.push(thumbPath); - const thumbSize = (await fsPromises.stat(thumbPath)).size; - const thumbMd5 = await calculateFileMD5(thumbPath); - context.deleteAfterSentFiles.push(thumbPath); - - const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`; - return { - elementType: ElementType.VIDEO, - elementId: '', - videoElement: { - fileName: uploadName, - filePath: path, - videoMd5: md5, - thumbMd5, - fileTime: videoInfo.time, - thumbPath: new Map([[0, thumbPath]]), - thumbSize, - thumbWidth: videoInfo.width, - thumbHeight: videoInfo.height, - fileSize: fileSize.toString(), - }, - }; - } - - async createValidSendPttElement (_context: SendMessageContext, pttPath: string): Promise { - const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, 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.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, - }, - }, - }; - } async downloadFileForModelId (peer: Peer, modelId: string, unknown: string, timeout = 1000 * 60 * 2) { const [, fileTransNotifyInfo] = await this.core.eventWrapper.callNormalEventV2( diff --git a/packages/napcat-core/package.json b/packages/napcat-core/package.json index 9dc60db6..110a527b 100644 --- a/packages/napcat-core/package.json +++ b/packages/napcat-core/package.json @@ -14,14 +14,11 @@ }, "dependencies": { "@protobuf-ts/runtime": "^2.11.1", - "napcat-protobuf": "workspace:*", "ajv": "^8.13.0", "@sinclair/typebox": "^0.34.38", "file-type": "^21.0.0", - "napcat-image-size": "workspace:*", - "napcat-core": "workspace:*", "napcat-common": "workspace:*", - "napcat-onebot": "workspace:*" + "napcat-protobuf": "workspace:*" }, "devDependencies": { "@types/node": "^22.0.1" diff --git a/packages/napcat-onebot/api/file.ts b/packages/napcat-onebot/api/file.ts new file mode 100644 index 00000000..1c9ed71b --- /dev/null +++ b/packages/napcat-onebot/api/file.ts @@ -0,0 +1,182 @@ + + +import { NapCatOneBot11Adapter } from '@/napcat-onebot/index'; +import { encodeSilk } from 'napcat-common/src/audio'; +import { FFmpegService } from 'napcat-common/src/ffmpeg'; +import { calculateFileMD5 } from 'napcat-common/src/file'; +import { defaultVideoThumbB64 } from 'napcat-common/src/video'; +import { ElementType, NapCatCore, PicElement, PicSubType, SendFileElement, SendPicElement, SendPttElement, SendVideoElement } from 'napcat-core'; +import { getFileTypeForSendType } from 'napcat-core/helper/msg'; +import { imageSizeFallBack } from 'napcat-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'; +export class OneBotFileApi { + obContext: NapCatOneBot11Adapter; + core: NapCatCore; + constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) { + this.obContext = obContext; + this.core = core; + } + async createValidSendFileElement(context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = ''): Promise { + const { + fileName: _fileName, + path, + fileSize, + } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE); + if (fileSize === 0) { + throw new Error('文件异常,大小为0'); + } + 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): Promise { + 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 = ''): Promise { + 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 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 thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`); + try { + videoInfo = await FFmpegService.getVideoInfo(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); + context.deleteAfterSentFiles.push(thumbPath); + + const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`; + return { + elementType: ElementType.VIDEO, + elementId: '', + videoElement: { + fileName: uploadName, + filePath: path, + videoMd5: md5, + thumbMd5, + fileTime: videoInfo.time, + thumbPath: new Map([[0, thumbPath]]), + thumbSize, + thumbWidth: videoInfo.width, + thumbHeight: videoInfo.height, + fileSize: fileSize.toString(), + }, + }; + } + + async createValidSendPttElement(_context: SendMessageContext, pttPath: string): Promise { + const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, 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, + }, + }, + }; + } + +}