Feat/Implement QQ Online File/Folder and Flash Transfer support (#1541)

* feat: implement QQ online file transfer and flash transfer support

* fix: change OnlineFile OB11Message data

* fix: add fileSize and isDir to OB11MessageOnlineFile

* fix: resolve typescript strict mode errors
This commit is contained in:
H3CoF6 2026-01-22 17:44:09 +08:00 committed by GitHub
parent 772f07c58b
commit 75236dd50c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1831 additions and 23 deletions

View File

@ -0,0 +1,264 @@
import { GeneralCallResult, InstanceContext, NapCatCore } from '@/napcat-core';
import {
createFlashTransferResult,
FileListResponse,
FlashFileSetInfo,
SendStatus,
} from '@/napcat-core/data/flash';
import { Peer } from '@/napcat-core/types';
export class NTQQFlashApi {
context: InstanceContext;
core: NapCatCore;
constructor (context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
/**
*
* @param fileListToUpload
*/
async createFlashTransferUploadTask (fileListToUpload: string[]): Promise < GeneralCallResult & {
createFlashTransferResult: createFlashTransferResult;
seq: number;
} > {
const flashService = this.context.session.getFlashTransferService();
const timestamp : number = Date.now();
const selfInfo = this.core.selfInfo;
const fileUploadArg = {
screen: 1, // 1
uploaders: [{
uin: selfInfo.uin,
uid: selfInfo.uid,
sendEntrance: '',
nickname: selfInfo.nick,
}],
paths: fileListToUpload,
};
const uploadResult = await flashService.createFlashTransferUploadTask(timestamp, fileUploadArg);
if (uploadResult.result === 0) {
this.context.logger.log('[Flash] 发起闪传任务成功');
return uploadResult;
} else {
this.context.logger.logError('[Flash] 发起闪传上传任务失败!!');
return uploadResult;
}
}
/**
*
* @param fileSetId
*/
async downloadFileSetBySetId (fileSetId: string): Promise < GeneralCallResult & {
extraInfo: unknown
} > {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.startFileSetDownload(fileSetId, 1, { isIncludeCompressInnerFiles: false }); // 为了方便,暂时硬编码
if (result.result === 0) {
this.context.logger.log('[Flash] 成功开始下载文件集');
} else {
this.context.logger.logError('[Flash] 尝试下载文件集失败!');
}
return result;
}
/**
*
* @param fileSetId
*/
async getShareLinkBySetId (fileSetId: string): Promise < GeneralCallResult & {
shareLink: string;
expireTimestamp: string;
}> {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.getShareLinkReq(fileSetId);
if (result.result === 0) {
this.context.logger.log('[Flash] 获取闪传外链分享成功:', result.shareLink);
} else {
this.context.logger.logError('[Flash] 获取闪传外链失败!!');
}
return result;
}
/**
* id
* @param shareCode
*/
async fromShareLinkFindSetId (shareCode: string): Promise < GeneralCallResult & {
fileSetId: string;
} > {
const flashService = this.context.session.getFlashTransferService();
const result = await flashService.getFileSetIdByCode(shareCode);
if (result.result === 0) {
this.context.logger.log('[Flash] 获取shareCode的文件集Id成功');
} else {
this.context.logger.logError('[Flash] 获取文件集ID失败');
}
return result;
}
/**
* fileSet的文件结构信息
* == GeneralCallResult!!! ==
* @param fileSetId
*/
async getFileListBySetId (fileSetId: string): Promise < FileListResponse > {
const flashService = this.context.session.getFlashTransferService();
const requestArg = {
seq: 0,
fileSetId,
isUseCache: false,
sceneType: 1, // 硬编码
reqInfos: [
{
count: 18, // 18 ??
paginationInfo: {},
parentId: '',
reqIndexPath: '',
reqDepth: 1,
filterCondition: {
fileCategory: 0,
filterType: 0,
},
sortConditions: [
{
sortField: 0,
sortOrder: 0,
},
],
isNeedPhysicalInfoReady: false,
},
],
};
const result = await flashService.getFileList(requestArg);
if (result.rsp.result === 0) {
this.context.logger.log('[Flash] 获取fileSet文件信息成功');
return result.rsp;
} else {
this.context.logger.logError(`[Flash] 获取文件信息失败ErrMsg: ${result.rsp.errMs}`);
return result.rsp;
}
}
/**
*
* @param fileSetId
*/
async getFileSetIndoBySetId (fileSetId: string): Promise < GeneralCallResult & {
seq: number;
isCache: boolean;
fileSet: FlashFileSetInfo;
} > {
const flashService = this.context.session.getFlashTransferService();
const requestArg = {
fileSetId,
};
const result = await flashService.getFileSet(requestArg);
if (result.result === 0) {
this.context.logger.log('[Flash] 获取闪传文件集信息成功!');
} else {
this.context.logger.logError('[Flash] 获取闪传文件信息失败!!');
}
return result;
}
/**
* /
* @param fileSetId
* @param peer
*/
async sendFlashMessage (fileSetId: string, peer:Peer): Promise < {
errCode: number,
errMsg: string,
rsp: {
sendStatus: SendStatus[]
}
} > {
const flashService = this.context.session.getFlashTransferService();
const target = {
destUid: peer.peerUid,
destType: peer.chatType,
// destUin: peer.peerUin,
};
const requestsArg = {
fileSetId,
targets: [target],
};
const result = await flashService.sendFlashTransferMsg(requestsArg);
if (result.errCode === 0) {
this.context.logger.log('[Flash] 消息发送成功');
} else {
this.context.logger.logError(`[Flash] 消息发送失败!!原因:${result.errMsg}`);
}
return result;
}
/**
* URL
* @param fileSetId
* @param options
*/
async getFileTransUrl (fileSetId: string, options: { fileName?: string; fileIndex?: number }): Promise < GeneralCallResult & {
transferUrl: string;
} > {
const flashService = this.context.session.getFlashTransferService();
const result = await this.getFileListBySetId(fileSetId);
const { fileName, fileIndex } = options;
let targetFile: any;
let file: any;
const allFolder = result.fileLists;
// eslint-disable-next-line no-labels
searchLoop: for (const folder of allFolder) {
const fileList = folder.fileList;
for (let i = 0; i < fileList.length; i++) {
file = fileList[i];
if (fileName !== undefined && file.name === fileName) {
targetFile = file;
// eslint-disable-next-line no-labels
break searchLoop;
}
if (fileIndex !== undefined && i === fileIndex) {
targetFile = file;
// eslint-disable-next-line no-labels
break searchLoop;
}
}
}
if (targetFile === undefined) {
this.context.logger.logError('[Flash] 未找到对应文件!!');
return {
result: -1,
errMsg: '未找到对应文件',
transferUrl: '',
};
} else {
this.context.logger.log('[Flash] 找到对应文件,准备尝试获取传输链接');
const res = await flashService.startFileTransferUrl(targetFile);
return {
result: 0,
errMsg: '',
transferUrl: res.url,
};
}
}
}

View File

@ -7,3 +7,5 @@ export * from './webapi';
export * from './system';
export * from './packet';
export * from './file';
export * from './online';
export * from './flash';

View File

@ -245,7 +245,7 @@ export class NTQQMsgApi {
[
'0',
peer,
msgElements,
msgElements as any,
new Map(),
],
(ret) => ret.result === 0,

View File

@ -0,0 +1,239 @@
import { InstanceContext, NapCatCore } from '@/napcat-core';
import { Peer } from '@/napcat-core/types';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { GeneralCallResultStatus } from '@/napcat-core/services/common';
import { sleep } from '@/napcat-common/src/helper';
const normalizePath = (p: string) => path.normalize(p).toLowerCase();
export class NTQQOnlineApi {
context: InstanceContext;
core: NapCatCore;
constructor (context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
/**
* node返回the fuck wrapper.node
* @param peer
* @param filePath
* @param fileName
*/
async sendOnlineFile (peer: Peer, filePath: string, fileName: string): Promise<any> {
if (!fs.existsSync(filePath)) {
throw new Error(`[NapCat] 文件不存在: ${filePath}`);
}
const actualFileName = fileName || path.basename(filePath);
const fileSize = fs.statSync(filePath).size.toString();
const fileElementToSend = [{
elementType: 23,
fileElement: {
fileName: actualFileName,
filePath,
fileSize,
},
}];
const msgService = this.context.session.getMsgService();
const startTime = Math.floor(Date.now() / 1000) - 2; // 容错时间窗口
msgService.sendMsg('0', peer, fileElementToSend, new Map()).catch((_e: any) => {
});
const maxRetries = 10;
let retryCount = 0;
while (retryCount < maxRetries) {
await sleep(1000);
retryCount++;
try {
const msgListResult = await msgService.getOnlineFileMsgs(peer);
const msgs = msgListResult?.msgList || [];
const foundMsg = msgs.find((msg: any) => {
if (parseInt(msg.msgTime) < startTime) return false;
const validElement = msg.elements.find((el: any) => {
if (el.elementType !== 23 || !el.fileElement) return false;
const isNameMatch = el.fileElement.fileName === actualFileName;
const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(filePath);
return isNameMatch && isPathMatch;
});
return !!validElement;
});
if (foundMsg) {
const targetElement = foundMsg.elements.find((el: any) => el.elementType === 23);
this.context.logger.log('[OnlineFile] 在线文件发送成功!');
return {
result: GeneralCallResultStatus.OK,
errMsg: '',
msgId: foundMsg.msgId,
elementId: targetElement?.elementId || '',
};
}
} catch (_e) {
}
}
this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!');
return {
result: GeneralCallResultStatus.ERROR,
errMsg: '[NapCat] Send Online File Timeout: Message not found in history.',
};
}
/**
* 线
* @param peer
* @param folderPath
* @param folderName
*/
async sendOnlineFolder (peer: Peer, folderPath: string, folderName?: string): Promise<any> {
const actualFolderName = folderName || path.basename(folderPath);
if (!fs.existsSync(folderPath)) {
return { result: GeneralCallResultStatus.ERROR, errMsg: `Folder not found: ${folderPath}` };
}
if (!fs.statSync(folderPath).isDirectory()) {
return { result: GeneralCallResultStatus.ERROR, errMsg: `Path is not a directory: ${folderPath}` };
}
const folderElementItem = {
elementType: 30,
elementId: '',
fileElement: {
fileName: actualFolderName,
filePath: folderPath,
},
} as any;
const msgService = this.context.session.getMsgService();
const startTime = Math.floor(Date.now() / 1000) - 2;
msgService.sendMsg('0', peer, [folderElementItem], new Map()).catch((_e: any) => {
});
const maxRetries = 10;
let retryCount = 0;
while (retryCount < maxRetries) {
await sleep(1000);
retryCount++;
try {
const msgListResult = await msgService.getOnlineFileMsgs(peer);
const msgs = msgListResult?.msgList || [];
const foundMsg = msgs.find((msg: any) => {
if (parseInt(msg.msgTime) < startTime) return false;
const validElement = msg.elements.find((el: any) => {
if (el.elementType !== 30 || !el.fileElement) return false;
const isNameMatch = el.fileElement.fileName === actualFolderName;
const isPathMatch = normalizePath(el.fileElement.filePath) === normalizePath(folderPath);
return isNameMatch && isPathMatch;
});
return !!validElement;
});
if (foundMsg) {
const targetElement = foundMsg.elements.find((el: any) => el.elementType === 30);
this.context.logger.log('[OnlineFile] 在线文件夹发送成功!');
return {
result: GeneralCallResultStatus.OK,
errMsg: '',
msgId: foundMsg.msgId,
elementId: targetElement?.elementId || '',
};
}
} catch (_e) {
}
}
this.context.logger.logError('[OnlineFile] 在线文件发送失败!!!');
return {
result: GeneralCallResultStatus.ERROR,
errMsg: '[NapCat] Send Online Folder Timeout: Message not found in history.',
};
}
/**
* 线
* @param peer
*/
async getOnlineFileMsg (peer: Peer) : Promise<any> {
const msgService = this.context.session.getMsgService();
return await msgService.getOnlineFileMsgs(peer);
}
/**
* 线
* @param peer
* @param msgId
*/
async cancelMyOnlineFileMsg (peer: Peer, msgId: string) : Promise<void> {
const msgService = this.context.session.getMsgService();
await msgService.cancelSendMsg(peer, msgId);
}
/**
* 线
* @param peer
* @param msgId
* @param elementId
*/
async refuseOnlineFileMsg (peer: Peer, msgId: string, elementId: string) : Promise<void> {
const msgService = this.context.session.getMsgService();
const arrToSend = {
msgId,
peerUid: peer.peerUid,
chatType: 1,
elementId,
downloadType: 1,
downSourceType: 1,
};
await msgService.refuseGetRichMediaElement(arrToSend);
}
/**
* 线/
* @param peer
* @param msgId
* @param elementId
* @constructor
*/
async receiveOnlineFileOrFolder (peer: Peer, msgId: string, elementId: string) : Promise<any> {
const msgService = this.context.session.getMsgService();
const arrToSend = {
msgId,
peerUid: peer.peerUid,
chatType: 1,
elementId,
downSourceType: 1,
downloadType: 1,
};
return await msgService.getRichMediaElement(arrToSend);
}
/**
* 线/线
* @param peer
* @param msgId
*/
async switchFileToOffline (peer: Peer, msgId: string) : Promise<void> {
const msgService = this.context.session.getMsgService();
await msgService.switchToOfflineSendMsg(peer, msgId);
}
}

View File

@ -0,0 +1,324 @@
export interface FlashBaseRequest {
fileSetId: string
}
export interface UploaderInfo {
uin: string,
nickname: string,
uid: string,
sendEntrance: string, // ""
}
export interface thumbnailInfo {
id: string,
url: {
spec: number,
uri: string,
}[],
localCachePath: string,
}
export interface SendTarget {
destType: number // 1私聊
destUin?: string,
destUid: string,
}
export interface SendTargetRequests {
fileSetId: string
targets: SendTarget[]
}
export interface DownloadStatusInfo {
result: number; // 0
fileSetId: string;
status: number;
info: {
curDownLoadFailFileNum: number,
curDownLoadedPauseFileNum: number,
curDownLoadedFileNum: number,
curRealDownLoadedFileNum: number,
curDownloadingFileNum: number,
totalDownLoadedFileNum: number,
curDownLoadedBytes: string, // "0"
totalDownLoadedBytes: string,
curSpeedBps: number,
avgSpeedBps: number,
maxSpeedBps: number,
remainDownLoadSeconds: number,
failFileIdList: [],
allFileIdList: [],
hasNormalFileDownloading: boolean,
onlyCompressInnerFileDownloading: boolean,
isAllFileAlreadyDownloaded: boolean,
saveFileSetDir: string,
allWaitingStatusTask: boolean,
downloadSceneType: number,
retryCount: number,
statisticInfo: {
downloadTaskId: string,
downloadFilesetName: string,
downloadFileTypeDistribution: string,
downloadFileSizeDistribution: string
},
albumStorageFailImageNum: number,
albumStorageFailVideoNum: number,
albumStorageFailFileIdList: [],
albumStorageSucImageNum: number,
albumStorageSucVideoNum: number,
albumStorageSucFileIdList: [],
albumStorageFileNum: number
}
}
export interface physicalInfo {
id: string,
url: string,
status: number, // 2 已下载
processing: string,
localPath: string,
width: 0,
height: 0,
time: number,
}
export interface downloadInfo {
status: number,
curDownLoadBytes: string,
totalFileBytes: string,
errorCode: number,
}
export interface uploadInfo {
uploadedBytes: string,
errorCode: number,
svrRrrCode: number,
errMsg: string,
isNeedDelDeviceInfo: boolean,
thumbnailUploadState: number
isSecondHit: boolean,
hasModifiedErr: boolean,
}
export interface folderUploadInfo {
totalUploadedFileSize: string
successCount: number
failedCount: number
}
export interface folderDownloadInfo {
totalDownloadedFileSize: string
totalFileSize: string
totalDownloadFileCount: number
successCount: number
failedCount: number
pausedCount: number
cancelCount: number
downloadingCount: number
partialDownloadCount: number
curLevelDownloadedFileCount: number
curLevelUnDownloadedFileCount: number
}
export interface compressFileFolderInfo {
downloadStatus: number
saveFileDirPath: string
totalFileCount: string
totalFileSize: string
}
export interface albumStorgeInfo {
status: number
localIdentifier: string
errorCode: number
timeCost: number
}
export interface FlashOneFileInfo {
fileSetId: string
cliFileId: string // client?? 或许可以换取url
compressedFileFolderId: string
archiveIndex: 0
indexPath: string
isDir: boolean // 文件或者文件夹!!
parentId: string
depth: number // 1
cliFileIndex: number
fileType: number // 枚举!! 已完成枚举!!
name: string
namePinyin: string
isCover: boolean
isCoverOriginal: boolean
fileSize: string
fileCount: number
thumbnail: thumbnailInfo
physical: physicalInfo
srvFileId: string // service?? 服务器上面的id吗
srvParentFileId: string
svrLastUpdateTimestamp: string
downloadInfo: downloadInfo
saveFilePath: string
search_relative_path: string
disk_relative_path: string
uploadInfo: uploadInfo
status: number
uploadStatus: number // 3已上传成功
downloadStatus: number // 0未下载
folderUploadInfo: folderUploadInfo
folderDownloadInfo: folderDownloadInfo
sha1: string
bookmark: string
compressFileFolderInfo: compressFileFolderInfo
uploadPauseReason: string
downloadPauseReason: string
filePhysicalSize: string
thumbnail_sha1: string | null
thumbnail_size: string | null
needAlbumStorage: boolean
albumStorageInfo: albumStorgeInfo
}
export interface fileListsInfo {
parentId: string,
depth: number, // 1
fileList: FlashOneFileInfo[],
paginationInfo: {}
isEnd: boolean,
isCache: boolean,
}
export interface FileListResponse {
seq: number,
result: number,
errMs: string,
fileLists: fileListsInfo[],
}
export interface createFlashTransferResult {
fileSetId: string,
shareLink: string,
expireTime: string,
expireLeftTime: string,
}
export interface StartFlashTaskRequests {
screen?: number; // 1 PC-QQ
uploaders: UploaderInfo[];
permission?: {};
coverPath?: string;
paths: string[]; // 文件的绝对路径,可以是文件夹
// excludePaths: [];
// expireLeftTime: 0,
// isNeedDelDeviceInfo: boolean,
// isNeedDelLocation: boolean,
// coverOriginalInfos: [],
// uploadSceneType: 10, // 不知道怎么枚举 先硬编码吧
// detectPrivacyInfoResult: {
// exists: boolean,
// allDetectResults: {}
// }
}
export interface FileListInfoRequests {
seq: number, // 0
fileSetId: string,
isUseCache: boolean,
sceneType: number, // 1
reqInfos: {
count: number, // 18 ?? 硬编码吧 不懂
paginationInfo: {},
parentId: string,
reqIndexPath: string,
reqDepth: number, // 1
filterCondition: {
fileCategory: number,
filterType: number,
}, // 0
sortConditions: {
sortField: number,
sortOrder: number,
}[],
isNeedPhysicalInfoReady: boolean
}[]
}
export interface FlashFileSetInfo {
fileSetId: string,
name: string,
namePinyin: string,
totalFileCount: number,
totalFileSize: number,
permission: {},
shareInfo: {
shareLink: string,
extractionCode: string,
},
cover: {
id: string,
urls: [
{
spec: number, // 2
url: string
}
],
localCachePath: string
},
uploaders: [
{
uin: string,
nickname: string,
uid: string,
sendEntrance: string
}
],
expireLeftTime: number,
aiClusteringStatus: {
firstClusteringList: [],
shouldPull: boolean
},
createTime: number,
expireTime: number,
firstLevelItemCount: 1,
svrLastUpdateTimestamp: 0,
taskId: string, // 同 fileSetId
uploadInfo: {
totalUploadedFileSize: number,
successCount: number,
failedCount: number
},
downloadInfo: {
totalDownloadedFileSize: 0,
totalFileSize: 0,
totalDownloadFileCount: 0,
successCount: 0,
failedCount: 0,
pausedCount: 0,
cancelCount: 0,
status: 0,
curLevelDownloadedFileCount: number,
curLevelUnDownloadedFileCount: 0
},
transferType: number,
isLocalCreate: true,
status: number, // todo 枚举全部状态
uploadStatus: number, // todo 同上
uploadPauseReason: 0,
downloadStatus: 0,
downloadPauseReason: 0,
saveFileSetDir: string,
uploadSceneType: 10,
downloadSceneType: 0, // 0 PC-QQ 103 web
retryCount: number,
isMergeShareUpload: 0,
isRemoveDeviceInfo: boolean,
isRemoveLocation: boolean
}
export interface SendStatus {
result: number,
msg: string,
target: {
destType: number,
destUid: string,
}
}

View File

@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/napcat-core/index';
import { ILogWrapper } from 'napcat-common/src/log-interface';
import EventEmitter from 'node:events';
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
@ -263,7 +264,13 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.fileElement) {
return `[文件 ${element.fileElement.fileName}]`;
if (element.fileElement.fileUuid) {
return `[文件 ${element.fileElement.fileName}]`;
} else if (element.elementType === ElementType.TOFURECORD) {
return `[在线文件 ${element.fileElement.fileName}]`;
} else if (element.elementType === ElementType.ONLINEFOLDER) {
return `[在线文件夹 ${element.fileElement.fileName}/]`;
}
}
if (element.videoElement) {
@ -287,7 +294,12 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
}
if (element.markdownElement) {
return '[Markdown 消息]';
// console.log(element.markdownElement);
if (element.markdownElement.mdSummary !== undefined && element.markdownElement.mdExtInfo !== undefined && element.markdownElement.mdExtInfo.flashTransferInfo) {
return element.markdownElement.mdSummary;
} else {
return '[Markdown 消息]';
}
}
if (element.multiForwardMsgElement) {
@ -296,6 +308,8 @@ function msgElementToText (element: MessageElement, msg: RawMessage, recursiveLe
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
} else if (element.elementType === ElementType.FILE) {
return '[文件发送中]';
}
return `[未实现 (ElementType = ${element.elementType})]`;

View File

@ -6,6 +6,8 @@ import {
NTQQSystemApi,
NTQQUserApi,
NTQQWebApi,
NTQQFlashApi,
NTQQOnlineApi,
} from '@/napcat-core/apis';
import { NTQQCollectionApi } from '@/napcat-core/apis/collection';
import {
@ -23,7 +25,7 @@ import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from 'napcat-common/src/system';
import { NTEventWrapper } from '@/napcat-core/helper/event';
import { KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
import { KickedOffLineInfo, RawMessage, SelfInfo, SelfStatusInfo } from '@/napcat-core/types';
import { NapCatConfigLoader, NapcatConfigSchema } from '@/napcat-core/helper/config';
import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/napcat-core/listeners';
@ -123,6 +125,8 @@ export class NapCatCore {
MsgApi: new NTQQMsgApi(this.context, this),
UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this),
FlashApi: new NTQQFlashApi(this.context, this),
OnlineApi: new NTQQOnlineApi(this.context, this),
};
container.bind(NapCatCore).toConstantValue(this);
container.bind(TypedEventEmitter).toConstantValue(this.event);
@ -178,6 +182,11 @@ export class NapCatCore {
async initNapCatCoreListeners () {
const msgListener = new NodeIKernelMsgListener();
// 在线文件/文件夹消息
msgListener.onRecvOnlineFileMsg = (msgs: RawMessage[]) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
};
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
const tips = `[KickedOffLine] [${Info.tipsTitle}] ${Info.tipsDesc}`;
@ -297,4 +306,6 @@ export interface StableNTApiWrapper {
MsgApi: NTQQMsgApi,
UserApi: NTQQUserApi,
GroupApi: NTQQGroupApi;
FlashApi: NTQQFlashApi,
OnlineApi: NTQQOnlineApi,
}

View File

@ -0,0 +1,302 @@
import { GeneralCallResult } from './common';
import {
SendStatus,
StartFlashTaskRequests,
createFlashTransferResult,
FlashBaseRequest,
FlashFileSetInfo,
FileListInfoRequests,
FileListResponse,
DownloadStatusInfo,
SendTargetRequests,
FlashOneFileInfo,
} from '../data/flash';
export interface NodeIKernelFlashTransferService {
/**
* /
* @param timestamp
* @param fileInfo
*/
createFlashTransferUploadTask(timestamp: number, fileInfo: StartFlashTaskRequests): Promise < GeneralCallResult & {
createFlashTransferResult: createFlashTransferResult;
seq: number;
} >; // 2 arg 重点 // 自动上传
createMergeShareTask(...args: unknown[]): unknown; // 2 arg
updateFlashTransfer(...args: unknown[]): unknown; // 2 arg
getFileSetList(...args: unknown[]): unknown; // 1 arg
getFileSetListCount(...args: unknown[]): unknown; // 1 arg
/**
* file set
* @param fileSetIdDict
*/
getFileSet(fileSetIdDict: FlashBaseRequest): Promise < GeneralCallResult & {
seq: number;
isCache: boolean;
fileSet: FlashFileSetInfo;
} >; // 1 arg
/**
* file set
* @param requestArgs
*/
getFileList(requestArgs: FileListInfoRequests): Promise < {
rsp: FileListResponse;
} > ; // 1 arg 这个方法QQ有bug 并没有,是我参数有问题
getDownloadedFileCount(...args: unknown[]): unknown; // 1 arg
getLocalFileList(...args: unknown[]): unknown; // 3 arg
batchRemoveUserFileSetHistory(...args: unknown[]): unknown; // 1 arg
/**
*
* @param fileSetId
*/
getShareLinkReq(fileSetId:string): Promise< GeneralCallResult & {
shareLink: string;
expireTimestamp: string;
}>;
/**
* fileSetId
* @param shareCode
*/
getFileSetIdByCode(shareCode: string): Promise < GeneralCallResult & {
fileSetId: string;
} > ; // 1 arg code == share code
batchRemoveFile(...args: unknown[]): unknown; // 1 arg
checkUploadPathValid(...args: unknown[]): unknown; // 1 arg
cleanFailedFiles(...args: unknown[]): unknown; // 2 arg
/**
*
*/
resumeAllUnfinishedTasks(): unknown; // 0 arg !!
addFileSetUploadListener(...args: unknown[]): unknown; // 1 arg
removeFileSetUploadListener(...args: unknown[]): unknown; // 1 arg
/**
*
* @param fileSetId
*/
startFileSetUpload(fileSetId: string): void; // 1 arg 并不是新建任务,应该是暂停后的启动
/**
*
* @param fileSetId
*/
stopFileSetUpload(fileSetId: string): void; // 1 arg stop 后start无效
/**
*
* @param fileSetId
*/
pauseFileSetUpload(fileSetId: string): void; // 1 arg 暂停上传
/**
*
* @param args
*/
resumeFileSetUpload(...args: unknown[]): unknown; // 1 arg 继续
pauseFileUpload(...args: unknown[]): unknown; // 1 arg
resumeFileUpload(...args: unknown[]): unknown; // 1 arg
stopFileUpload(...args: unknown[]): unknown; // 1 arg
asyncGetThumbnailPath(...args: unknown[]): unknown; // 2 arg
setDownLoadDefaultFileDir(...args: unknown[]): unknown; // 1 arg
setFileSetDownloadDir(...args: unknown[]): unknown; // 2 arg
getFileSetDownloadDir(...args: unknown[]): unknown; // 1 arg
setFlashTransferDir(...args: unknown[]): unknown; // 2 arg
addFileSetDownloadListener(...args: unknown[]): unknown; // 1 arg
removeFileSetDownloadListener(...args: unknown[]): unknown; // 1 arg
/**
* file set的函数
* @param fileSetId
* @param chatType //因为没有peer其实可以硬编码为1 (好友私聊)
* @param arg // 默认为false
*/
startFileSetDownload(fileSetId:string, chatType: number, arg: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} >; // 3 arg
stopFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} > ; // 2 arg 结束不可重启!!
pauseFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} > ; // 2 arg
resumeFileSetDownload(fileSetId: string, arg1: { isIncludeCompressInnerFiles: boolean }): Promise < GeneralCallResult & {
extraInfo: 0
} > ; // 2 arg
startFileListDownLoad(...args: unknown[]): unknown; // 4 arg // 大概率是选择set里面的部分文件进行下载没必要不想写
pauseFileListDownLoad(...args: unknown[]): unknown; // 2 arg
resumeFileListDownLoad(...args: unknown[]): unknown; // 2 arg
stopFileListDownLoad(...args: unknown[]): unknown; // 2 arg
startThumbnailListDownload(fileSetId: string): Promise < GeneralCallResult >; // 1 arg // 缩略图下载
stopThumbnailListDownload(fileSetId: string): Promise < GeneralCallResult >; // 1 arg
asyncRequestDownLoadStatus(fileSetId: string): Promise < DownloadStatusInfo >; // 1 arg
startFileTransferUrl(fileInfo: FlashOneFileInfo): Promise < {
ret: number,
url: string,
expireTimestampSeconds: string
} >; // 1 arg
startFileListDownLoadBySessionId(...args: unknown[]): unknown; // 2 arg
addFileSetSimpleStatusListener(...args: unknown[]): unknown; // 2 arg
addFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 2 arg
removeFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 2 arg
removeFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg
addDesktopFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg
addDesktopFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 1 arg
removeDesktopFileSetSimpleStatusMonitoring(...args: unknown[]): unknown; // 1 arg
removeDesktopFileSetSimpleStatusListener(...args: unknown[]): unknown; // 1 arg
addFileSetSimpleUploadInfoListener(...args: unknown[]): unknown; // 1 arg
addFileSetSimpleUploadInfoMonitoring(...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleUploadInfoMonitoring(...args: unknown[]): unknown; // 1 arg
removeFileSetSimpleUploadInfoListener(...args: unknown[]): unknown; // 1 arg
/**
*
* @param sendArgs
*/
sendFlashTransferMsg(sendArgs: SendTargetRequests): Promise < {
errCode: number,
errMsg: string,
rsp: {
sendStatus: SendStatus[]
}
} >; // 1 arg 估计是file set id
addFlashTransferTaskInfoListener(...args: unknown[]): unknown; // 1 arg
removeFlashTransferTaskInfoListener(...args: unknown[]): unknown; // 1 arg
retrieveLocalLastFailedSetTasksInfo(): unknown; // 0 arg
getFailedFileList(fileSetId: string): Promise < {
rsp: {
seq: number;
result: number;
errMs: string;
fileSetId: string;
fileList: []
}
} >; // 1 arg
getLocalFileListByStatuses(...args: unknown[]): unknown; // 1 arg
addTransferStateListener(...args: unknown[]): unknown; // 1 arg
removeTransferStateListener(...args: unknown[]): unknown; // 1 arg
getFileSetFirstClusteringList(...args: unknown[]): unknown; // 3 arg
getFileSetClusteringList(...args: unknown[]): unknown; // 1 arg
addFileSetClusteringListListener(...args: unknown[]): unknown; // 1 arg
removeFileSetClusteringListListener(...args: unknown[]): unknown; // 1 arg
getFileSetClusteringDetail(...args: unknown[]): unknown; // 1 arg
doAIOFlashTransferBubbleActionWithStatus(...args: unknown[]): unknown; // 4 arg
getFilesTransferProgress(...args: unknown[]): unknown; // 1 arg
pollFilesTransferProgress(...args: unknown[]): unknown; // 1 arg
cancelPollFilesTransferProgress(...args: unknown[]): unknown; // 1 arg
checkDownloadStatusBeforeLocalFileOper(...args: unknown[]): unknown; // 3 arg
getCompressedFileFolder(...args: unknown[]): unknown; // 1 arg
addFolderListener(...args: unknown[]): unknown; // 1 arg
removeFolderListener(...args: unknown[]): unknown;
addCompressedFileListener(...args: unknown[]): unknown;
removeCompressedFileListener(...args: unknown[]): unknown;
getFileCategoryList(...args: unknown[]): unknown;
addDeviceStatusListener(...args: unknown[]): unknown;
removeDeviceStatusListener(...args: unknown[]): unknown;
checkDeviceStatus(...args: unknown[]): unknown;
pauseAllTasks(...args: unknown[]): unknown; // 2 arg
resumePausedTasksAfterDeviceStatus(...args: unknown[]): unknown;
onSystemGoingToSleep(...args: unknown[]): unknown;
onSystemWokeUp(...args: unknown[]): unknown;
getFileMetas(...args: unknown[]): unknown;
addDownloadCntStatisticsListener(...args: unknown[]): unknown;
removeDownloadCntStatisticsListener(...args: unknown[]): unknown;
detectPrivacyInfoInPaths(...args: unknown[]): unknown;
getFileThumbnailUrl(...args: unknown[]): unknown;
handleDownloadFinishAfterSaveToAlbum(...args: unknown[]): unknown;
checkBatchFilesDownloadStatus(...args: unknown[]): unknown;
onCheckAlbumStorageStatusResult(...args: unknown[]): unknown;
addFileAlbumStorageListener(...args: unknown[]): unknown;
removeFileAlbumStorageListener(...args: unknown[]): unknown;
refreshFolderStatus(...args: unknown[]): unknown;
}

View File

@ -1,4 +1,4 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/napcat-core/types';
import { ElementType, MessageElement, Peer, RawMessage, FileElement } from '@/napcat-core/types';
import { NodeIKernelMsgListener } from '@/napcat-core/listeners/NodeIKernelMsgListener';
import { GeneralCallResult } from '@/napcat-core/services/common';
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/napcat-core/types/msg';
@ -10,7 +10,10 @@ export interface NodeIKernelMsgService {
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number;
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<unknown, unknown>): Promise<GeneralCallResult>;
sendMsg(msgId: string, peer: Peer, msgElements: {
elementType: number;
fileElement: { fileName: string; filePath: string; fileSize: string }
}[], map: Map<unknown, unknown>): Promise<GeneralCallResult>;
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>;
@ -70,7 +73,7 @@ export interface NodeIKernelMsgService {
addSendMsg(...args: unknown[]): unknown;
cancelSendMsg(...args: unknown[]): unknown;
cancelSendMsg(peer: Peer, msgId: string): Promise<void> ;
switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown;
@ -78,7 +81,7 @@ export interface NodeIKernelMsgService {
refuseReceiveOnlineFileMsg(peer: Peer, MsgId: string): unknown;
resendMsg(...args: unknown[]): unknown;
resendMsg(peer: Peer, msgId: string): Promise<void>;
recallMsg(...args: unknown[]): unknown;
@ -132,7 +135,20 @@ export interface NodeIKernelMsgService {
isMsgMatched(...args: unknown[]): unknown;
getOnlineFileMsgs(...args: unknown[]): unknown;
getOnlineFileMsgs(peer: Peer): Promise <GeneralCallResult & {
msgList: {
msgId: string;
msgRandom: string;
senderUid: string;
peerUid: string;
msgTime: string;
elements: {
elementType: number;
elementId: string;
fileElement: FileElement
}[];
}[] // 一大坨,懒得写
} >;
getAllOnlineFileMsgs(...args: unknown[]): unknown;
@ -418,11 +434,25 @@ export interface NodeIKernelMsgService {
refreshMsgAbstractsByGuildIds(...args: unknown[]): unknown;
getRichMediaElement(...args: unknown[]): unknown;
getRichMediaElement(arg: {
msgId: string,
peerUid: string,
chatType: number,
elementId: string,
downSourceType: number,
downloadType: number,
}): Promise<any>;
cancelGetRichMediaElement(...args: unknown[]): unknown;
refuseGetRichMediaElement(...args: unknown[]): unknown;
refuseGetRichMediaElement(args: {
msgId: string,
peerUid: string,
chatType: number,
elementId: string,
downloadType: number, // 1
downSourceType: number, // 1
}): Promise<void>;
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown;

View File

@ -1,5 +1,6 @@
export enum GeneralCallResultStatus {
OK = 0,
ERROR = -1,
}
export interface GeneralCallResult {

View File

@ -0,0 +1,21 @@
export enum fileType {
MP3 = 1,
VIDEO = 2,
DOC = 3,
ZIP = 4,
XLS = 6,
PPT = 7,
CODE = 8,
PDF = 9,
TXT = 10,
UNKNOW = 11,
FOLDER = 25,
IMG = 26,
}
export enum FileStatus {
UPLOADING = 0,
// DOWNLOADED = 1, ??? 不太清楚
OK = 2,
STOP = 3,
}

View File

@ -66,13 +66,14 @@ export enum ElementType {
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
TOFURECORD = 23, // tofu record?? 在线文件的id是这个
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
ONLINEFOLDER = 30, // 在线文件夹
RECOMMENDEDMSG = 43,
ACTIONBAR = 44,
}
@ -303,11 +304,40 @@ export enum NTVideoType {
VIDEO_FORMAT_WMV = 3,
}
/**
*
*/
export interface FlashTransferIcon {
spec: number;
url: string;
}
/**
*
*/
export interface FlashTransferInfo {
filesetId: string;
name: string;
fileSize: string;
thnumbnail: {
id: string;
urls: FlashTransferIcon[];
localCachePath: string;
}
}
/**
* Markdown元素接口
*/
export interface MarkdownElement {
content: string;
style?: {};
processMsg?: string;
mdSummary?: string;
mdExtType?: number;
mdExtInfo?: {
flashTransferInfo: FlashTransferInfo;
}
}
/**

View File

@ -27,6 +27,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";
export interface NodeQQNTWrapperUtil {
get(): NodeQQNTWrapperUtil;
@ -202,6 +203,8 @@ export interface NodeIQQNTWrapperSession {
getSearchService(): NodeIKernelSearchService;
getFlashTransferService(): NodeIKernelFlashTransferService;
getDirectSessionService(): unknown;
getRDeliveryService(): unknown;

View File

@ -0,0 +1,24 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
// 不全部使用json因为一个文件解析Form-data会变字符串 但是api文档就写List
const SchemaData = Type.Object({
files: Type.Union([
Type.Array(Type.String()),
Type.String(),
]),
});
type Payload = Static<typeof SchemaData>;
export class CreateFlashTask extends OneBotAction<Payload, unknown> {
override actionName = ActionName.CreateFlashTask;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
// todo fileset的名字和缩略图还没实现
const fileList = Array.isArray(payload.files) ? payload.files : [payload.files];
return await this.core.apis.FlashApi.createFlashTransferUploadTask(fileList);
}
}

View File

@ -0,0 +1,19 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
fileset_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class DownloadFileset extends OneBotAction<Payload, unknown> {
override actionName = ActionName.DownloadFileset;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
// 默认路径 / fileset_id /为下载路径
return await this.core.apis.FlashApi.downloadFileSetBySetId(payload.fileset_id);
}
}

View File

@ -0,0 +1,20 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
share_code: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class GetFilesetId extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetFilesetId;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
// 适配share_link 防止被传 Link无法解析
const code = payload.share_code.includes('=') ? payload.share_code.split('=').slice(1).join('=') : payload.share_code;
return await this.core.apis.FlashApi.fromShareLinkFindSetId(code);
}
}

View File

@ -0,0 +1,18 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
fileset_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class GetFilesetInfo extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetFilesetInfo;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
return await this.core.apis.FlashApi.getFileSetIndoBySetId(payload.fileset_id);
}
}

View File

@ -0,0 +1,18 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
fileset_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class GetFlashFileList extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetFlashFileList;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
return await this.core.apis.FlashApi.getFileListBySetId(payload.fileset_id);
}
}

View File

@ -0,0 +1,24 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
fileset_id: Type.String(),
file_name: Type.Optional(Type.String()),
file_index: Type.Optional(Type.Number()),
});
type Payload = Static<typeof SchemaData>;
export class GetFlashFileUrl extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetFlashFileUrl;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
// 文件的索引依旧从0开始
return await this.core.apis.FlashApi.getFileTransUrl(payload.fileset_id, {
fileName: payload.file_name,
fileIndex: payload.file_index,
});
}
}

View File

@ -0,0 +1,18 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
fileset_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class GetShareLink extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetShareLink;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
return await this.core.apis.FlashApi.getShareLinkBySetId(payload.fileset_id);
}
}

View File

@ -0,0 +1,39 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType, Peer } from 'napcat-core/types';
const SchemaData = Type.Object({
fileset_id: Type.String(),
user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
});
type Payload = Static<typeof SchemaData>;
export class SendFlashMsg extends OneBotAction<Payload, unknown> {
override actionName = ActionName.SendFlashMsg;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
let peer: Peer;
if (payload.group_id) {
peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
} else if (payload.user_id) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
// 可能需要更严格的判断
const isBuddy = await this.core.apis.FriendApi.isBuddy(uid);
peer = {
chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP,
peerUid: uid,
};
} else {
throw new Error('user_id or group_id is required');
}
return await this.core.apis.FlashApi.sendFlashMessage(payload.fileset_id, peer);
}
}

View File

@ -0,0 +1,26 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType } from 'napcat-core/types';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
msg_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class CancelOnlineFile extends OneBotAction<Payload, unknown> {
override actionName = ActionName.CancelOnlineFile;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
// 仅私聊
const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid };
return await this.core.apis.OnlineApi.cancelMyOnlineFileMsg(peer, payload.msg_id);
}
}

View File

@ -0,0 +1,25 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType } from 'napcat-core/types';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GetOnlineFileMessages extends OneBotAction<Payload, unknown> {
override actionName = ActionName.GetOnlineFileMessages;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
// 仅私聊
const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid };
return await this.core.apis.OnlineApi.getOnlineFileMsg(peer);
}
}

View File

@ -0,0 +1,27 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType } from 'napcat-core/types';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
msg_id: Type.String(),
element_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class ReceiveOnlineFile extends OneBotAction<Payload, unknown> {
override actionName = ActionName.ReceiveOnlineFile;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
// 默认下载路径
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid };
return await this.core.apis.OnlineApi.receiveOnlineFileOrFolder(peer, payload.msg_id, payload.element_id);
}
}

View File

@ -0,0 +1,26 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType } from 'napcat-core/types';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
msg_id: Type.String(),
element_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class RefuseOnlineFile extends OneBotAction<Payload, unknown> {
override actionName = ActionName.RefuseOnlineFile;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid };
return await this.core.apis.OnlineApi.refuseOnlineFileMsg(peer, payload.msg_id, payload.element_id);
}
}

View File

@ -0,0 +1,28 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType } from 'napcat-core/types';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
file_path: Type.String(),
file_name: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
export class SendOnlineFile extends OneBotAction<Payload, unknown> {
override actionName = ActionName.SendOnlineFile;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
// 仅私聊
const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid };
const fileName = payload.file_name || '';
return await this.core.apis.OnlineApi.sendOnlineFile(peer, payload.file_path, fileName);
}
}

View File

@ -0,0 +1,26 @@
import { OneBotAction } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { ChatType } from 'napcat-core/types';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
folder_path: Type.String(),
folder_name: Type.Optional(Type.String()),
});
type Payload = Static<typeof SchemaData>;
export class SendOnlineFolder extends OneBotAction<Payload, unknown> {
override actionName = ActionName.SendOnlineFolder;
override payloadSchema = SchemaData;
async _handle (payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('User not found');
const peer = { chatType: ChatType.KCHATTYPEC2C, peerUid: uid };
return await this.core.apis.OnlineApi.sendOnlineFolder(peer, payload.folder_path, payload.folder_name);
}
}

View File

@ -66,9 +66,9 @@ import { FetchCustomFace } from './extends/FetchCustomFace';
import GoCQHTTPUploadPrivateFile from './go-cqhttp/UploadPrivateFile';
import { FetchEmojiLike } from './extends/FetchEmojiLike';
import { NapCatCore } from 'napcat-core';
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
import type { NetworkAdapterConfig } from '../config/config';
import { OneBotAction } from './OneBotAction';
import { NapCatOneBot11Adapter } from '@/napcat-onebot';
import { SetInputStatus } from './extends/SetInputStatus';
import { GetCSRF } from './system/GetCSRF';
import { DelGroupNotice } from './group/DelGroupNotice';
@ -140,6 +140,20 @@ import { DownloadFileImageStream } from './stream/DownloadFileImageStream';
import { TestDownloadStream } from './stream/TestStreamDownload';
import { UploadFileStream } from './stream/UploadFileStream';
import { AutoRegisterRouter } from './auto-register';
import { CreateFlashTask } from './file/flash/CreateFlashTask';
import { SendFlashMsg } from './file/flash/SendFlashMsg';
import { GetFlashFileList } from './file/flash/GetFlashFileList';
import { GetFlashFileUrl } from './file/flash/GetFlashFileUrl';
import { GetShareLink } from './file/flash/GetShareLink';
import { GetFilesetInfo } from './file/flash/GetFilesetInfo';
import { DownloadFileset } from './file/flash/DownloadFileset';
import { GetOnlineFileMessages } from './file/online/GetOnlineFileMessages';
import { SendOnlineFile } from './file/online/SendOnlineFile';
import { SendOnlineFolder } from './file/online/SendOnlineFolder';
import { CancelOnlineFile } from './file/online/CancelOnlineFile';
import { ReceiveOnlineFile } from './file/online/ReceiveOnlineFile';
import { RefuseOnlineFile } from './file/online/RefuseOnlineFile';
import { GetFilesetId } from './file/flash/GetFilesetIdByCode';
export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [
@ -293,6 +307,20 @@ export function createActionMap (obContext: NapCatOneBot11Adapter, core: NapCatC
new CleanCache(obContext, core),
new GetGroupAddRequest(obContext, core),
new GetCollectionList(obContext, core),
new CreateFlashTask(obContext, core),
new GetFlashFileList(obContext, core),
new GetFlashFileUrl(obContext, core),
new SendFlashMsg(obContext, core),
new GetShareLink(obContext, core),
new GetFilesetInfo(obContext, core),
new GetOnlineFileMessages(obContext, core),
new SendOnlineFile(obContext, core),
new SendOnlineFolder(obContext, core),
new ReceiveOnlineFile(obContext, core),
new RefuseOnlineFile(obContext, core),
new CancelOnlineFile(obContext, core),
new DownloadFileset(obContext, core),
new GetFilesetId(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];

View File

@ -125,8 +125,8 @@ export const ActionName = {
// 以下为扩展napcat扩展
Unknown: 'unknown',
SetDiyOnlineStatus: 'set_diy_online_status',
SharePeer: 'ArkSharePeer',// @deprecated
ShareGroupEx: 'ArkShareGroup',// @deprecated
SharePeer: 'ArkSharePeer', // @deprecated
ShareGroupEx: 'ArkShareGroup', // @deprecated
// 标准化接口
SendGroupArkShare: 'send_group_ark_share',
SendArkShare: 'send_ark_share',
@ -185,4 +185,22 @@ export const ActionName = {
GetClientkey: 'get_clientkey',
SendPoke: 'send_poke',
// Flash (闪传) 扩展
CreateFlashTask: 'create_flash_task',
SendFlashMsg: 'send_flash_msg', // 因为不可能手动构造element所以不走sendMsg
GetShareLink: 'get_share_link',
DownloadFileset: 'download_fileset',
GetFilesetInfo: 'get_fileset_info',
GetFlashFileList: 'get_flash_file_list',
GetFlashFileUrl: 'get_flash_file_url',
GetFilesetId: 'get_fileset_id',
// Online File (在线文件) 扩展
SendOnlineFile: 'send_online_file',
SendOnlineFolder: 'send_online_folder',
GetOnlineFileMessages: 'get_online_file_msg',
ReceiveOnlineFile: 'receive_online_file',
RefuseOnlineFile: 'refuse_online_file',
CancelOnlineFile: 'cancel_online_file',
} as const;

View File

@ -42,11 +42,18 @@ import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../event/notice/OB11GroupDecreaseEvent';
import { GroupAdmin } from 'napcat-core/packet/transformer/proto/message/groupAdmin';
import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from 'napcat-core/packet/transformer/proto';
import {
GroupChange,
GroupChangeInfo,
GroupInvite,
PushMsgBody,
} from 'napcat-core/packet/transformer/proto';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { LRUCache } from 'napcat-common/src/lru-cache';
import { cleanTaskQueue } from 'napcat-common/src/clean-task';
import { registerResource } from 'napcat-common/src/health';
import { OB11OnlineFileReceiveEvent } from '@/napcat-onebot/event/notice/OB11OnlineFileReceiveEvent';
import { OB11OnlineFileSendEvent } from '@/napcat-onebot/event/notice/OB11OnlineFileSendEvent';
type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@ -143,6 +150,21 @@ export class OneBotMsgApi {
},
fileElement: async (element, msg, elementWrapper, { disableGetUrl }) => {
// 让在线文件/文件夹的消息单独出去否则无法正确处理UUID
if (+elementWrapper.elementType === 23 || +elementWrapper.elementType === 30) {
// 判断为在线文件/文件夹
return {
type: OB11MessageDataType.onlinefile,
data: {
msgId: msg.msgId,
elementId: elementWrapper.elementId,
fileName: element.fileName,
fileSize: element.fileSize,
isDir: (elementWrapper.elementType === 30),
},
};
}
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
@ -538,12 +560,22 @@ export class OneBotMsgApi {
},
markdownElement: async (element) => {
return {
type: OB11MessageDataType.markdown,
data: {
content: element.content,
},
};
// 让QQ闪传消息独立出去
if (element.mdExtInfo !== undefined && element.mdExtInfo.flashTransferInfo) {
return {
type: OB11MessageDataType.flashtransfer,
data: {
fileSetId: element.mdExtInfo.flashTransferInfo.filesetId,
},
};
} else {
return {
type: OB11MessageDataType.markdown,
data: {
content: element.content,
},
};
}
},
};
@ -880,6 +912,10 @@ export class OneBotMsgApi {
}
return undefined;
},
// 不需要支持发送
[OB11MessageDataType.onlinefile]: async () => undefined,
[OB11MessageDataType.flashtransfer]: async () => undefined,
};
constructor (obContext: NapCatOneBot11Adapter, core: NapCatCore) {
@ -1329,6 +1365,7 @@ export class OneBotMsgApi {
async parseSysMessage (msg: number[]) {
const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg));
// 邀请需要解grayTipElement
// console.log(SysMessage.body?.msgContent);
if (SysMessage.contentHead.type === 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
await this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString(), true);
@ -1484,6 +1521,63 @@ export class OneBotMsgApi {
);
} else if (SysMessage.contentHead.type === 528 && SysMessage.contentHead.subType === 39 && SysMessage.body?.msgContent) {
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
} else if (SysMessage.contentHead.type === 166 && SysMessage.contentHead.c2CCmd === 133 && SysMessage.body?.msgContent) {
this.core.context.logger.logDebug('在线文件通道断开');
// 可能原因: 对方取消 对方拒绝 对方转离线
// body不是proto只能手动提取可能是错的
// console.log(SysMessage.body?.msgContent);
const mainCmd = SysMessage.body.msgContent[15];
const subCmd = SysMessage.body.msgContent[17];
if (mainCmd === 101) {
// 在线文件
if (subCmd === 225) {
// 对方取消或转离线
this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}取消了在线文件的传输(或转离线)`);
return new OB11OnlineFileReceiveEvent(
this.core,
+SysMessage.responseHead.fromUin
);
} else if (subCmd === 230) {
// 对方拒绝接收
this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}拒绝了你的在线文件传输`);
return new OB11OnlineFileSendEvent(
this.core,
+SysMessage.responseHead.fromUin,
'refuse'
);
}
} else if (mainCmd === 136) {
if (subCmd === 225) {
// 对方取消或转离线
this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}取消了在线文件夹的传输(或转离线)`);
return new OB11OnlineFileReceiveEvent(
this.core,
+SysMessage.responseHead.fromUin
);
} else if (subCmd === 230) {
// 对方拒绝接收
this.core.context.logger.log(`好友:${SysMessage.responseHead.fromUin}拒绝了你的在线文件夹传输`);
return new OB11OnlineFileSendEvent(
this.core,
+SysMessage.responseHead.fromUin,
'refuse'
);
}
}
this.core.context.logger.logDebug('未知的系统消息事件:', mainCmd, subCmd);
return undefined;
} else if (SysMessage.contentHead.type === 166 && SysMessage.contentHead.c2CCmd === 131 && SysMessage.body?.msgContent) {
const mainCmd = SysMessage.body.msgContent[15];
if (mainCmd === 101) {
this.core.context.logger.log('在线文件传输成功!');
} else if (mainCmd === 136) {
this.core.context.logger.log('在线文件夹传输成功!');
}
return new OB11OnlineFileSendEvent(
this.core,
+SysMessage.responseHead.fromUin,
'receive'
);
}
// else if (SysMessage.contentHead.type == 732 && SysMessage.contentHead.subType == 16 && SysMessage.body?.msgContent) {
// let data_wrap = PBString(2);

View File

@ -0,0 +1,11 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
import { NapCatCore } from 'napcat-core';
export abstract class OB11OnlineFileNoticeEvent extends OB11BaseNoticeEvent {
peer_id: number;
protected constructor (core: NapCatCore, peer_id: number) {
super(core);
this.peer_id = peer_id;
}
}

View File

@ -0,0 +1,13 @@
import { OB11OnlineFileNoticeEvent } from './OB11OnlineFileNoticeEvent';
import { NapCatCore } from '@/napcat-core';
export class OB11OnlineFileReceiveEvent extends OB11OnlineFileNoticeEvent {
notice_type: string;
sub_type: string;
constructor (core: NapCatCore, peer_id: number) {
super(core, peer_id);
this.notice_type = 'online_file_receive';
this.sub_type = 'cancel';
}
}

View File

@ -0,0 +1,12 @@
import { OB11OnlineFileNoticeEvent } from './OB11OnlineFileNoticeEvent';
import { NapCatCore } from '@/napcat-core';
export class OB11OnlineFileSendEvent extends OB11OnlineFileNoticeEvent {
notice_type = 'online_file_send';
sub_type: 'receive' | 'refuse';
constructor (core: NapCatCore, peer_id: number, sub_type: 'receive' | 'refuse') {
super(core, peer_id);
this.sub_type = sub_type;
}
}

View File

@ -328,6 +328,38 @@ export class NapCatOneBot11Adapter {
);
}
};
/**
* 线listener
*/
msgListener.onRecvOnlineFileMsg = async (msg: RawMessage[]) => {
if (!this.networkManager.hasActiveAdapters()) {
return;
}
for (const m of msg) {
// this.context.logger.logMessage(m, this.core.selfInfo);
if (this.bootTime > parseInt(m.msgTime)) {
this.context.logger.logDebug(`在线文件消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`);
continue;
}
m.id = MessageUnique.createUniqueMsgId(
{
chatType: m.chatType,
peerUid: m.peerUid,
guildId: '',
},
m.msgId
);
await this.emitMsg(m).catch((e) =>
this.context.logger.logError('处理在线文件消息失败', e)
);
}
};
msgListener.onAddSendMsg = async (msg) => {
try {
if (msg.sendStatus === SendStatusType.KSEND_STATUS_SENDING) {

View File

@ -73,6 +73,8 @@ export enum OB11MessageDataType {
miniapp = 'miniapp', // json类
contact = 'contact',
location = 'location',
onlinefile = 'onlinefile', // 在线文件/文件夹
flashtransfer = 'flashtransfer', // QQ闪传
}
export interface OB11MessagePoke {
@ -254,6 +256,24 @@ export interface OB11MessageForward {
};
}
export interface OB11MessageOnlineFile {
type: OB11MessageDataType.onlinefile;
data: {
msgId: string;
elementId: string;
fileName: string;
fileSize: string;
isDir: boolean;
}
}
export interface OB11MessageFlashTransfer {
type: OB11MessageDataType.flashtransfer;
data: {
fileSetId: string;
}
}
// 消息数据类型定义
export type OB11MessageData =
OB11MessageText |
@ -261,7 +281,8 @@ export type OB11MessageData =
OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode | OB11MessageIdMusic | OB11MessageCustomMusic | OB11MessageJson |
OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContact | OB11MessagePoke;
OB11MessageDice | OB11MessageRPS | OB11MessageMarkdown | OB11MessageForward | OB11MessageContact |
OB11MessagePoke | OB11MessageOnlineFile | OB11MessageFlashTransfer;
// 发送消息接口定义
export interface OB11PostSendMsg {