From 974b10b7b56a7a560ee427347c4d7d01890aeb78 Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Sat, 24 Jan 2026 02:30:06 +0800 Subject: [PATCH] feat: support thumbnail for flash-transfer --- packages/napcat-core/apis/flash.ts | 46 +++++- packages/napcat-core/data/flash.ts | 26 ++-- packages/napcat-core/index.ts | 1 + packages/napcat-core/wrapper.ts | 15 +- .../action/file/flash/CreateFlashTask.ts | 135 +++++++++++++++++- 5 files changed, 206 insertions(+), 17 deletions(-) diff --git a/packages/napcat-core/apis/flash.ts b/packages/napcat-core/apis/flash.ts index 2d865161..3e5a182d 100644 --- a/packages/napcat-core/apis/flash.ts +++ b/packages/napcat-core/apis/flash.ts @@ -19,8 +19,10 @@ export class NTQQFlashApi { /** * 发起闪传上传任务 * @param fileListToUpload 上传文件绝对路径的列表,可以是文件夹!! + * @param thumbnailPath + * @param filesetName */ - async createFlashTransferUploadTask (fileListToUpload: string[]): Promise < GeneralCallResult & { + async createFlashTransferUploadTask (fileListToUpload: string[], thumbnailPath: string, filesetName: string): Promise < GeneralCallResult & { createFlashTransferResult: createFlashTransferResult; seq: number; } > { @@ -29,15 +31,34 @@ export class NTQQFlashApi { const timestamp : number = Date.now(); const selfInfo = this.core.selfInfo; + console.log(thumbnailPath); + const fileUploadArg = { screen: 1, // 1 + name: filesetName, uploaders: [{ uin: selfInfo.uin, uid: selfInfo.uid, sendEntrance: '', nickname: selfInfo.nick, }], + coverPath: thumbnailPath, paths: fileListToUpload, + excludePaths: [], + expireLeftTime: 0, + isNeedDelDeviceInfo: false, + isNeedDelLocation: false, + coverOriginalInfos: [ + { + path: fileListToUpload[0] || '', + thumbnailPath, + }, + ], + uploadSceneType: 10, + detectPrivacyInfoResult: { + exists: false, + allDetectResults: new Map(), + }, }; const uploadResult = await flashService.createFlashTransferUploadTask(timestamp, fileUploadArg); @@ -261,4 +282,27 @@ export class NTQQFlashApi { }; } } + + async createFileThumbnail (filePath: string): Promise { + const msgService = this.context.session.getMsgService(); + const savePath = msgService.getFileThumbSavePathForSend(750, true); + + const result = await this.core.util.createThumbnailImage( + 'flashtransfer', + filePath, + savePath, + { + width: 520, + height: 520, + }, + 'jpeg', + null + ); + if (result.result === 0) { + this.context.logger.log('获取缩略图成功!!'); + result.targetPath = savePath; + return result; + } + return result; + } } diff --git a/packages/napcat-core/data/flash.ts b/packages/napcat-core/data/flash.ts index 56abc817..525b4bb7 100644 --- a/packages/napcat-core/data/flash.ts +++ b/packages/napcat-core/data/flash.ts @@ -202,21 +202,25 @@ export interface createFlashTransferResult { } export interface StartFlashTaskRequests { - screen?: number; // 1 PC-QQ + screen: number; // 1 PC-QQ + name?: string; uploaders: UploaderInfo[]; permission?: {}; coverPath?: string; paths: string[]; // 文件的绝对路径,可以是文件夹 - // excludePaths: []; - // expireLeftTime: 0, - // isNeedDelDeviceInfo: boolean, - // isNeedDelLocation: boolean, - // coverOriginalInfos: [], - // uploadSceneType: 10, // 不知道怎么枚举 先硬编码吧 - // detectPrivacyInfoResult: { - // exists: boolean, - // allDetectResults: {} - // } + excludePaths?: any[]; + expireLeftTime?: number, // 0 + isNeedDelDeviceInfo: boolean, + isNeedDelLocation: boolean, + coverOriginalInfos?: { + path: string, + thumbnailPath: string, + }[], + uploadSceneType: number, // 不知道怎么枚举 先硬编码吧 (PC QQ 10) + detectPrivacyInfoResult: { + exists: boolean, + allDetectResults: {} + } } export interface FileListInfoRequests { diff --git a/packages/napcat-core/index.ts b/packages/napcat-core/index.ts index e444ea9b..cc7ff526 100644 --- a/packages/napcat-core/index.ts +++ b/packages/napcat-core/index.ts @@ -74,6 +74,7 @@ export function loadQQWrapper (execPath: string | undefined, QQVersion: string): } const nativemodule: { exports: WrapperNodeApi; } = { exports: {} as WrapperNodeApi }; process.dlopen(nativemodule, wrapperNodePath); + process.env['NAPCAT_WRAPPER_PATH'] = wrapperNodePath; return nativemodule.exports; } export function getMajorPath (execPath: string, QQVersion: string): string { diff --git a/packages/napcat-core/wrapper.ts b/packages/napcat-core/wrapper.ts index c94bf42c..a0822954 100644 --- a/packages/napcat-core/wrapper.ts +++ b/packages/napcat-core/wrapper.ts @@ -1,5 +1,6 @@ import { NodeIDependsAdapter, NodeIDispatcherAdapter, NodeIGlobalAdapter } from './adapters'; import { + GeneralCallResult, NodeIKernelAvatarService, NodeIKernelBuddyService, NodeIKernelGroupService, @@ -27,7 +28,7 @@ import { NodeIKernelMSFService } from './services/NodeIKernelMSFService'; import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService'; import { NodeIKernelECDHService } from './services/NodeIKernelECDHService'; import { NodeIO3MiscService } from './services/NodeIO3MiscService'; -import { NodeIKernelFlashTransferService } from "./services/NodeIKernelFlashTransferService"; +import { NodeIKernelFlashTransferService } from './services/NodeIKernelFlashTransferService'; export interface NodeQQNTWrapperUtil { get(): NodeQQNTWrapperUtil; @@ -139,6 +140,18 @@ export interface NodeQQNTWrapperUtil { getNvidiaDriverVersion(): unknown; isNull(): unknown; + + createThumbnailImage( + serviceName: string, + filePath: string, + targetPath: string, + imgSize: { + width: number, + height: number + }, + fileFormat: string, + arg: unknown, // null + ): Promise < GeneralCallResult & { targetPath?: string } >; } export interface NodeIQQNTStartupSessionWrapper { create(): NodeIQQNTStartupSessionWrapper; diff --git a/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts b/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts index 7d2f811b..2246007d 100644 --- a/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts +++ b/packages/napcat-onebot/action/file/flash/CreateFlashTask.ts @@ -1,6 +1,71 @@ import { OneBotAction } from '@/napcat-onebot/action/OneBotAction'; import { ActionName } from '@/napcat-onebot/action/router'; -import { Static, Type } from '@sinclair/typebox'; +import { Static, Type, Optional } from '@sinclair/typebox'; +import path from 'node:path'; +import fs from 'node:fs'; + +const richMediaList = [ + '.mp4', '.mov', '.avi', '.wmv', '.mpeg', '.mpg', '.flv', '.mkv', + '.png', '.gif', '.jpg', '.jpeg', '.webp', '.bmp', +]; + +const aiList = ['.ai', '.eps']; +const apkList = ['.apk']; +const audioList = ['.mp3', '.wav', '.wma', '.aac', '.flac', '.ogg', '.m4a', '.mid', '.amr', '.m4r']; +const bakList = ['.bak', '.tmp', '.old', '.swp']; +const codeList = ['.js', '.ts', '.jsx', '.tsx', '.json', '.c', '.cpp', '.h', '.hpp', '.java', '.py', '.go', '.rs', '.php', '.html', '.css', '.sh', '.bat', '.cmd', '.xml', '.yaml', '.yml', '.sql', '.lua', '.rb']; +const dmgList = ['.dmg']; +const docList = ['.doc', '.docx', '.wps', '.dot', '.dotx', '.odt', '.rtf']; +const exeList = ['.exe', '.msi', '.com', '.scr', '.bin']; +const fontList = ['.ttf', '.otf', '.woff', '.woff2', '.ttc', '.fon']; +const imageList = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.tif', '.tiff']; +const ipaList = ['.ipa']; +const keynoteList = ['.key']; +const linkList = ['.lnk', '.url', '.webloc']; +const mindmapList = ['.xmind', '.mm', '.mindnode', '.emmx']; +const noteList = ['.enex', '.notes', '.one']; +const numbersList = ['.numbers']; +const pagesList = ['.pages']; +const pdfList = ['.pdf']; +const pkgList = ['.pkg']; +const pptList = ['.ppt', '.pptx', '.pps', '.ppsx', '.pot', '.odp']; +const psList = ['.psd']; +const rarList = ['.rar']; +const sketchList = ['.sketch']; +const txtList = ['.txt', '.md', '.log', '.ini', '.conf', '.cfg', '.info']; +const videoList = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.mpeg', '.mpg', '.3gp', '.rmvb']; +const xlsList = ['.xls', '.xlsx', '.csv', '.et', '.xlt', '.ods']; +const zipList = ['.zip', '.7z', '.tar', '.gz', '.bz2', '.iso', '.cab', '.jar']; + +const extensionMap = { + ai: aiList, + apk: apkList, + audio: audioList, + bak: bakList, + code: codeList, + dmg: dmgList, + doc: docList, + exe: exeList, + font: fontList, + image: imageList, + ipa: ipaList, + keynote: keynoteList, + link: linkList, + mindmap: mindmapList, + note: noteList, + numbers: numbersList, + pages: pagesList, + pdf: pdfList, + pkg: pkgList, + ppt: pptList, + ps: psList, + rar: rarList, + sketch: sketchList, + txt: txtList, + video: videoList, + xls: xlsList, + zip: zipList, +}; // 不全部使用json因为:一个文件解析Form-data会变字符串!!! 但是api文档就写List const SchemaData = Type.Object({ @@ -8,6 +73,8 @@ const SchemaData = Type.Object({ Type.Array(Type.String()), Type.String(), ]), + name: Optional(Type.String()), + thumb_path: Optional(Type.String()), }); type Payload = Static; @@ -16,9 +83,69 @@ export class CreateFlashTask extends OneBotAction { override payloadSchema = SchemaData; async _handle (payload: Payload) { - // todo fileset的名字和缩略图还没实现!! - const fileList = Array.isArray(payload.files) ? payload.files : [payload.files]; + let iconName: string; + const qqPath = process.env['NAPCAT_WRAPPER_PATH'] || ''; - return await this.core.apis.FlashApi.createFlashTransferUploadTask(fileList); + const fileList = Array.isArray(payload.files) ? payload.files : [payload.files]; + let thumbPath: string = ''; + + if (fileList.length === 1) { + // 我是真没hook到那种合并的缩略图是哪个方法产生的,暂时不实现(怀疑是js直接canvas渲染的!!) + const filePath = fileList[0]; + if (filePath === undefined) { + return {}; + } + const ext = path.extname(filePath).toLowerCase(); + + if (richMediaList.includes(ext)) { + try { + const res = await this.core.apis.FlashApi.createFileThumbnail(filePath); + if (res && typeof res === 'object' && 'result' in res && res.result === 0) { + thumbPath = res.targetPath as string; + } + } catch (_e) { + } + } else { + let isDir = false; + + try { + const stat = await fs.promises.stat(filePath); + isDir = stat.isDirectory(); + } catch { + } + + if (isDir) { + iconName = 'folder'; + } else { + iconName = Object.keys(extensionMap).find(key => extensionMap[key].includes(ext)) || 'unknown'; + } + + // const __filename = fileURLToPath(import.meta.url); + // thumbPath = path.join(path.dirname(filePath), 'StaticThumbnail', `${iconName}.png`); // Gemini??? 害我找半天错??? + if (qqPath !== undefined) { + const basicPath = path.dirname(qqPath); + thumbPath = path.join(basicPath, 'resource', 'fileIcon', `${iconName}.png`); + } + } + } else { + iconName = 'multi_files'; + if (qqPath !== undefined) { + const basicPath = path.dirname(qqPath); + thumbPath = path.join(basicPath, 'resource', 'fileIcon', `${iconName}.png`); + } + } + + function toPlatformPath (inputPath: string) { + const unifiedPath = inputPath.replace(/[\\/]/g, path.sep); + return path.normalize(unifiedPath); + } + + let normalPath: string; + if (payload.thumb_path !== undefined) { + normalPath = path.normalize(payload.thumb_path); + } else { + normalPath = toPlatformPath(thumbPath); + } + return await this.core.apis.FlashApi.createFlashTransferUploadTask(fileList, normalPath, payload.name || ''); } }