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
242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
import { InstanceContext, SnowCatCore } from '@/snowcat-core';
|
|
import { Peer } from '@/snowcat-core/types';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { GeneralCallResultStatus } from '@/snowcat-core/services/common';
|
|
import { sleep } from '@/snowcat-common/src/helper';
|
|
|
|
const normalizePath = (p: string) => path.normalize(p).toLowerCase();
|
|
|
|
export class NTQQOnlineApi {
|
|
context: InstanceContext;
|
|
core: SnowCatCore;
|
|
|
|
constructor (context: InstanceContext, core: SnowCatCore) {
|
|
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(`[SnowCat] 文件不存在: ${filePath}`);
|
|
}
|
|
const actualFileName = fileName || path.basename(filePath);
|
|
const fileSize = fs.statSync(filePath).size.toString();
|
|
|
|
const fileElementToSend = [{
|
|
elementType: 23,
|
|
elementId: '',
|
|
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: '[SnowCat] 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,
|
|
fileSize: '',
|
|
},
|
|
};
|
|
|
|
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: '[SnowCat] 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);
|
|
}
|
|
}
|