Compare commits

...

5 Commits

Author SHA1 Message Date
手瓜一十雪
2daddbb030 Refactor message API types and add elementId to file element
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run
Removed unnecessary type casting in NTQQMsgApi, added missing elementId property to fileElement in NTQQOnlineApi, and updated NodeIKernelMsgService to use SendMessageElement for sendMsg. Also standardized method signatures and formatting for improved type safety and consistency.
2026-01-22 17:59:11 +08:00
手瓜一十雪
6ec5bbeddf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2026-01-22 17:45:13 +08:00
H3CoF6
75236dd50c
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
2026-01-22 17:44:09 +08:00
手瓜一十雪
01958d47a4 Refactor type annotations and router initialization
Standardized type annotations for interfaces in user.ts and improved type safety in webapi.ts. Updated all Express router initializations to explicitly declare the Router type. Added missing RequestHandler typings in uploader modules for better type checking.
2026-01-22 17:35:54 +08:00
手瓜一十雪
772f07c58b Refactor DebugAdapter to extend IOB11NetworkAdapter
Refactored DebugAdapter to inherit from IOB11NetworkAdapter, improving integration with the OneBot network manager. Enhanced WebSocket client management, error handling, and adapter lifecycle. Updated API and WebSocket handlers for better type safety and reliability. This change prepares the debug adapter for more robust and maintainable debugging sessions.
2026-01-22 16:22:18 +08:00
50 changed files with 2509 additions and 691 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

@ -0,0 +1,240 @@
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,
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: '[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

@ -13,6 +13,17 @@ import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { qunAlbumControl } from '../data/webapi';
import { createAlbumCommentRequest, createAlbumFeedPublish, createAlbumMediaFeed } from '../data/album';
export interface SetNoticeRetSuccess {
ec: number;
em: string;
id: number;
ltsm: number;
new_fid: string;
read_only: number;
role: number;
srv_code: number;
}
export class NTQQWebApi {
context: InstanceContext;
core: NapCatCore;
@ -25,12 +36,12 @@ export class NTQQWebApi {
async shareDigest (groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const url = `https://qun.qq.com/cgi-bin/group_digest/share_digest?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
group_code: groupCode,
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()}`;
bkn: this.getBknFromCookie(cookieObject),
group_code: groupCode,
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()}`;
try {
return RequestUtil.HttpGetText(url, 'GET', '', { Cookie: this.cookieToString(cookieObject) });
} catch {
@ -52,11 +63,11 @@ export class NTQQWebApi {
async getGroupEssenceMsg (GroupCode: string, page_start: number = 0, page_limit: number = 50) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
}).toString()}`;
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
}).toString()}`;
try {
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
url,
@ -76,16 +87,16 @@ export class NTQQWebApi {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
@ -101,16 +112,16 @@ export class NTQQWebApi {
// 遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
retList.push(ret);
}
@ -153,16 +164,7 @@ export class NTQQWebApi {
imgWidth: number = 540,
imgHeight: number = 300
) {
interface SetNoticeRetSuccess {
ec: number;
em: string;
id: number;
ltsm: number;
new_fid: string;
read_only: number;
role: number;
srv_code: number;
}
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
@ -178,18 +180,18 @@ export class NTQQWebApi {
imgHeight: imgHeight.toString(),
};
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings,
...(picId === '' ? {} : externalParam),
}).toString()}`,
'POST',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
return ret;
} catch {
@ -201,20 +203,20 @@ export class NTQQWebApi {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
try {
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
return ret?.ec === 0 ? ret : undefined;
} catch {
@ -222,17 +224,17 @@ export class NTQQWebApi {
}
}
private async getDataInternal (cookieObject: { [key: string]: string }, groupCode: string, type: number) {
private async getDataInternal (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
let resJson;
try {
const res = await RequestUtil.HttpGetText(
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ Cookie: this.cookieToString(cookieObject) }
);
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
if (match?.[1]) {
@ -245,7 +247,7 @@ export class NTQQWebApi {
}
}
private async getHonorList (cookieObject: { [key: string]: string }, groupCode: string, type: number) {
private async getHonorList (cookieObject: { [key: string]: string; }, groupCode: string, type: number) {
const data = await this.getDataInternal(cookieObject, groupCode, type);
if (!data) {
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
@ -304,11 +306,11 @@ export class NTQQWebApi {
return HonorInfo;
}
private cookieToString (cookieObject: { [key: string]: string }) {
private cookieToString (cookieObject: { [key: string]: string; }) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
}
public getBknFromCookie (cookieObject: { [key: string]: string }) {
public getBknFromCookie (cookieObject: { [key: string]: string; }) {
const sKey = cookieObject['skey'] as string;
let hash = 5381;
@ -361,7 +363,7 @@ export class NTQQWebApi {
uin,
getMemberRole: '0',
});
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string }> } }>(api + params.toString(), 'GET', '', {
const response = await RequestUtil.HttpGetJson<{ data: { album: Array<{ id: string, title: string; }>; }; }>(api + params.toString(), 'GET', '', {
Cookie: cookies,
});
return response.data.album;
@ -384,7 +386,7 @@ export class NTQQWebApi {
sAlbumID,
});
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
const post = await RequestUtil.HttpGetJson<{ data: { session: string }, ret: number, msg: string }>(api, 'POST', body, {
const post = await RequestUtil.HttpGetJson<{ data: { session: string; }, ret: number, msg: string; }>(api, 'POST', body, {
Cookie: cookie,
'Content-Type': 'application/json',
});
@ -430,7 +432,7 @@ export class NTQQWebApi {
throw new Error(`HTTP error! status: ${response.status}`);
}
const post = await response.json() as { ret: number, msg: string }; if (post.ret !== 0) {
const post = await response.json() as { ret: number, msg: string; }; if (post.ret !== 0) {
throw new Error(`分片 ${seq} 上传失败: ${post.msg}`);
}
offset += chunk.length;
@ -475,10 +477,10 @@ export class NTQQWebApi {
const client_key = Date.now() * 1000;
return await this.context.session.getAlbumService().doQunComment(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: [],
},
map_info: [],
map_bytes_info: [],
map_user_account: [],
},
qunId,
2,
createAlbumMediaFeed(uin, albumId, lloc),
@ -509,13 +511,13 @@ export class NTQQWebApi {
const uin = this.core.selfInfo.uin || '10001';
return await this.context.session.getAlbumService().doQunLike(
random_seq, {
map_info: [],
map_bytes_info: [],
map_user_account: [],
}, {
id,
status: 1,
},
map_info: [],
map_bytes_info: [],
map_user_account: [],
}, {
id,
status: 1,
},
createAlbumFeedPublish(qunId, uin, albumId, lloc)
);
}

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;
}

File diff suppressed because it is too large Load Diff

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

@ -57,24 +57,24 @@ export interface BaseInfo {
}
// 音乐信息
interface MusicInfo {
export interface MusicInfo {
buf: string;
}
// 视频业务信息
interface VideoBizInfo {
export interface VideoBizInfo {
cid: string;
tvUrl: string;
synchType: string;
}
// 视频信息
interface VideoInfo {
export interface VideoInfo {
name: string;
}
// 扩展在线业务信息
interface ExtOnlineBusinessInfo {
export interface ExtOnlineBusinessInfo {
buf: string;
customStatus: unknown;
videoBizInfo: VideoBizInfo;
@ -82,12 +82,12 @@ interface ExtOnlineBusinessInfo {
}
// 扩展缓冲区
interface ExtBuffer {
export interface ExtBuffer {
buf: string;
}
// 用户状态
interface UserStatus {
export interface UserStatus {
uid: string;
uin: string;
status: number;
@ -109,14 +109,14 @@ interface UserStatus {
}
// 特权图标
interface PrivilegeIcon {
export interface PrivilegeIcon {
jumpUrl: string;
openIconList: unknown[];
closeIconList: unknown[];
}
// 增值服务信息
interface VasInfo {
export interface VasInfo {
vipFlag: boolean;
yearVipFlag: boolean;
svipFlag: boolean;
@ -149,7 +149,7 @@ interface VasInfo {
}
// 关系标志
interface RelationFlags {
export interface RelationFlags {
topTime: string;
isBlock: boolean;
isMsgDisturb: boolean;
@ -167,7 +167,7 @@ interface RelationFlags {
}
// 通用扩展信息
interface CommonExt {
export interface CommonExt {
constellation: number;
shengXiao: number;
kBloodType: number;
@ -193,14 +193,14 @@ export enum BuddyListReqType {
}
// 图片信息
interface Pic {
export interface Pic {
picId: string;
picTime: number;
picUrlMap: Record<string, string>;
}
// 照片墙
interface PhotoWall {
export interface PhotoWall {
picList: Pic[];
}
@ -247,7 +247,7 @@ export interface ModifyProfileParams {
nick: string;
longNick: string;
sex: NTSex;
birthday: { birthday_year: string, birthday_month: string, birthday_day: string };
birthday: { birthday_year: string, birthday_month: string, birthday_day: string; };
location: unknown;
}

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 {

View File

@ -1,231 +1,227 @@
import { Router, Request, Response } from 'express';
import { WebSocket, WebSocketServer } from 'ws';
import { WebSocket, WebSocketServer, RawData } from 'ws';
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
import { IncomingMessage } from 'http';
import { OB11Response } from '@/napcat-onebot/action/OneBotAction';
import { ActionName } from '@/napcat-onebot/action/router';
import { OB11LifeCycleEvent, LifeCycleSubType } from '@/napcat-onebot/event/meta/OB11LifeCycleEvent';
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import { WebsocketServerConfig } from '@/napcat-onebot/config/config';
import { ActionMap } from '@/napcat-onebot/action';
import { NapCatCore } from '@/napcat-core/index';
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/napcat-onebot/network/index';
import json5 from 'json5';
const router = Router();
type ActionNameType = typeof ActionName[keyof typeof ActionName];
const router: Router = Router();
const DEFAULT_ADAPTER_NAME = 'debug-primary';
/**
*
* OneBot NetworkManager WebSocket
*/
class DebugAdapter {
name: string;
isEnable: boolean = true;
// 安全令牌
class DebugAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
readonly token: string;
// 添加 config 属性,模拟 PluginConfig 结构
config: {
enable: boolean;
name: string;
messagePostFormat?: string;
reportSelfMessage?: boolean;
debug?: boolean;
token?: string;
heartInterval?: number;
};
wsClients: Set<WebSocket> = new Set();
wsClients: WebSocket[] = [];
wsClientWithEvent: WebSocket[] = [];
lastActivityTime: number = Date.now();
inactivityTimer: NodeJS.Timeout | null = null;
readonly INACTIVITY_TIMEOUT = 5 * 60 * 1000; // 5分钟不活跃
constructor (sessionId: string) {
this.name = `debug-${sessionId}`;
// 生成简单的随机 token
this.token = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
override get isActive (): boolean {
return this.isEnable && this.wsClientWithEvent.length > 0;
}
this.config = {
constructor (sessionId: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
const config: WebsocketServerConfig = {
enable: true,
name: this.name,
name: `debug-${sessionId}`,
host: '127.0.0.1',
port: 0,
messagePostFormat: 'array',
reportSelfMessage: true,
token: '',
enableForcePushEvent: true,
debug: true,
token: this.token,
heartInterval: 30000
heartInterval: 0
};
super(`debug-${sessionId}`, config, core, obContext, actions);
this.token = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
this.isEnable = false;
this.startInactivityCheck();
}
// 实现 IOB11NetworkAdapter 接口所需的抽象方法
async open (): Promise<void> { }
async close (): Promise<void> { this.cleanup(); }
async reload (_config: any): Promise<any> { return 0; }
/**
* OneBot - WebSocket ()
*/
async onEvent (event: any) {
this.updateActivity();
const payload = JSON.stringify(event);
if (this.wsClients.size === 0) {
async open (): Promise<void> {
if (this.isEnable) {
this.logger.logError('[Debug] Cannot open an already opened adapter');
return;
}
this.wsClients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(payload);
} catch (error) {
console.error('[Debug] 发送事件到 WebSocket 失败:', error);
}
}
});
this.logger.log('[Debug] Adapter opened:', this.name);
this.isEnable = true;
}
/**
* OneBot API (HTTP 使)
*/
async callApi (actionName: string, params: any): Promise<any> {
this.updateActivity();
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (!oneBotContext) {
throw new Error('OneBot 未初始化');
}
const action = oneBotContext.actions.get(actionName);
if (!action) {
throw new Error(`不支持的 API: ${actionName}`);
}
return await action.handle(params, this.name, {
name: this.name,
enable: true,
messagePostFormat: 'array',
reportSelfMessage: true,
debug: true,
});
}
/**
* WebSocket (OneBot )
*/
async handleWsMessage (ws: WebSocket, message: string | Buffer) {
this.updateActivity();
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any; } = { action: ActionName.Unknown, params: {} };
let echo;
try {
receiveData = JSON.parse(message.toString());
echo = receiveData.echo;
} catch {
this.sendWsResponse(ws, OB11Response.error('json解析失败,请检查数据格式', 1400, echo));
async close (): Promise<void> {
if (!this.isEnable) {
return;
}
this.logger.log('[Debug] Adapter closing:', this.name);
this.isEnable = false;
receiveData.params = (receiveData?.params) ? receiveData.params : {};
// 兼容 WebUI 之前可能的一些非标准格式 (如果用户是旧前端)
// 但既然用户说要"原始流",我们优先支持标准格式
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (!oneBotContext) {
this.sendWsResponse(ws, OB11Response.error('OneBot 未初始化', 1404, echo));
return;
}
const action = oneBotContext.actions.get(receiveData.action as any);
if (!action) {
this.sendWsResponse(ws, OB11Response.error('不支持的API ' + receiveData.action, 1404, echo));
return;
}
try {
const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
send: async (data: object) => {
this.sendWsResponse(ws, OB11Response.ok(data, echo ?? '', true));
},
});
this.sendWsResponse(ws, retdata);
} catch (e: any) {
this.sendWsResponse(ws, OB11Response.error(e.message || '内部错误', 1200, echo));
}
}
sendWsResponse (ws: WebSocket, data: any) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
/**
* WebSocket
*/
addWsClient (ws: WebSocket) {
this.wsClients.add(ws);
this.updateActivity();
// 发送生命周期事件 (Connect)
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (oneBotContext && oneBotContext.core) {
try {
const event = new OB11LifeCycleEvent(oneBotContext.core, LifeCycleSubType.CONNECT);
ws.send(JSON.stringify(event));
} catch (e) {
console.error('[Debug] 发送生命周期事件失败', e);
}
}
}
/**
* WebSocket
*/
removeWsClient (ws: WebSocket) {
this.wsClients.delete(ws);
}
updateActivity () {
this.lastActivityTime = Date.now();
}
startInactivityCheck () {
this.inactivityTimer = setInterval(() => {
const inactive = Date.now() - this.lastActivityTime;
// 如果没有 WebSocket 连接且超时,则自动清理
if (inactive > this.INACTIVITY_TIMEOUT && this.wsClients.size === 0) {
console.log(`[Debug] Adapter ${this.name} 不活跃,自动关闭`);
this.cleanup();
}
}, 30000);
}
cleanup () {
// 停止不活跃检查定时器
if (this.inactivityTimer) {
clearInterval(this.inactivityTimer);
this.inactivityTimer = null;
}
// 关闭所有 WebSocket 连接
// 关闭所有 WebSocket 连接并移除事件监听器
this.wsClients.forEach((client) => {
try {
client.removeAllListeners();
client.close();
} catch (error) {
// ignore
this.logger.logError('[Debug] 关闭 WebSocket 失败:', error);
}
});
this.wsClients.clear();
// 从 OneBot NetworkManager 移除
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (oneBotContext) {
oneBotContext.networkManager.adapters.delete(this.name);
}
// 从管理器中移除
debugAdapterManager.removeAdapter(this.name);
this.wsClients = [];
this.wsClientWithEvent = [];
}
async reload (_config: unknown): Promise<OB11NetworkReloadType> {
return OB11NetworkReloadType.NetWorkReload;
}
async onEvent<T extends OB11EmitEventContent> (event: T): Promise<void> {
this.updateActivity();
const payload = JSON.stringify(event);
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
try {
wsClient.send(payload);
} catch (error) {
this.logger.logError('[Debug] 发送事件失败:', error);
}
}
});
}
async callApi (actionName: ActionNameType, params: Record<string, unknown>): Promise<unknown> {
this.updateActivity();
const action = this.actions.get(actionName as Parameters<typeof this.actions.get>[0]);
if (!action) {
throw new Error(`不支持的 API: ${actionName}`);
}
type ActionHandler = { handle: (params: unknown, ...args: unknown[]) => Promise<unknown>; };
return await (action as ActionHandler).handle(params, this.name, this.config);
}
private async handleMessage (wsClient: WebSocket, message: RawData): Promise<void> {
this.updateActivity();
let receiveData: { action: ActionNameType, params?: Record<string, unknown>, echo?: unknown; } = {
action: ActionName.Unknown,
params: {}
};
let echo: unknown = undefined;
try {
receiveData = json5.parse(message.toString());
echo = receiveData.echo;
} catch {
this.sendToClient(wsClient, OB11Response.error('json解析失败,请检查数据格式', 1400, echo));
return;
}
receiveData.params = receiveData?.params || {};
const action = this.actions.get(receiveData.action as Parameters<typeof this.actions.get>[0]);
if (!action) {
this.logger.logError('[Debug] 不支持的API:', receiveData.action);
this.sendToClient(wsClient, OB11Response.error('不支持的API ' + receiveData.action, 1404, echo));
return;
}
try {
type ActionHandler = { websocketHandle: (params: unknown, ...args: unknown[]) => Promise<unknown>; };
const retdata = await (action as ActionHandler).websocketHandle(receiveData.params, echo ?? '', this.name, this.config, {
send: async (data: object) => {
this.sendToClient(wsClient, OB11Response.ok(data, echo ?? '', true));
},
});
this.sendToClient(wsClient, retdata);
} catch (e: unknown) {
const error = e as Error;
this.logger.logError('[Debug] 处理消息失败:', error);
this.sendToClient(wsClient, OB11Response.error(error.message || '内部错误', 1200, echo));
}
}
private sendToClient (wsClient: WebSocket, data: unknown): void {
if (wsClient.readyState === WebSocket.OPEN) {
try {
wsClient.send(JSON.stringify(data));
} catch (error) {
this.logger.logError('[Debug] 发送消息失败:', error);
}
}
}
async addWsClient (ws: WebSocket): Promise<void> {
this.wsClientWithEvent.push(ws);
this.wsClients.push(ws);
this.updateActivity();
// 发送连接事件
this.sendToClient(ws, new OB11LifeCycleEvent(this.core, LifeCycleSubType.CONNECT));
ws.on('error', (err) => this.logger.log('[Debug] WebSocket Error:', err.message));
ws.on('message', (message) => {
this.handleMessage(ws, message).catch((e: unknown) => {
this.logger.logError('[Debug] handleMessage error:', e);
});
});
ws.on('ping', () => ws.pong());
ws.once('close', () => this.removeWsClient(ws));
}
private removeWsClient (ws: WebSocket): void {
const normalIndex = this.wsClients.indexOf(ws);
if (normalIndex !== -1) {
this.wsClients.splice(normalIndex, 1);
}
const eventIndex = this.wsClientWithEvent.indexOf(ws);
if (eventIndex !== -1) {
this.wsClientWithEvent.splice(eventIndex, 1);
}
}
updateActivity (): void {
this.lastActivityTime = Date.now();
}
startInactivityCheck (): void {
this.inactivityTimer = setInterval(() => {
const inactive = Date.now() - this.lastActivityTime;
if (inactive > this.INACTIVITY_TIMEOUT && this.wsClients.length === 0) {
this.logger.log(`[Debug] Adapter ${this.name} 不活跃,自动关闭`);
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (oneBotContext) {
// 先从管理器移除,避免重复销毁
debugAdapterManager.removeAdapter(this.name);
// 使用 NetworkManager 标准流程关闭
oneBotContext.networkManager.closeSomeAdapters([this]).catch((e: unknown) => {
this.logger.logError('[Debug] 自动关闭适配器失败:', e);
});
}
}
}, 30000);
}
/**
* Token
*/
validateToken (inputToken: string): boolean {
return this.token === inputToken;
}
@ -244,17 +240,20 @@ class DebugAdapterManager {
return this.currentAdapter;
}
// 获取 OneBot 上下文
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (!oneBotContext) {
throw new Error('OneBot 未初始化,无法创建调试适配器');
}
// 创建新实例
const adapter = new DebugAdapter('primary');
const adapter = new DebugAdapter('primary', oneBotContext.core, oneBotContext, oneBotContext.actions);
this.currentAdapter = adapter;
// 注册到 OneBot NetworkManager
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (oneBotContext) {
oneBotContext.networkManager.adapters.set(adapter.name, adapter as any);
} else {
console.warn('[Debug] OneBot 未初始化,无法注册适配器');
}
// 使用 NetworkManager 标准流程注册并打开适配器
oneBotContext.networkManager.registerAdapterAndOpen(adapter).catch((e: unknown) => {
console.error('[Debug] 注册适配器失败:', e);
});
return adapter;
}
@ -286,8 +285,9 @@ router.post('/create', async (_req: Request, res: Response) => {
token: adapter.token,
message: '调试适配器已就绪',
});
} catch (error: any) {
sendError(res, error.message);
} catch (error: unknown) {
const err = error as Error;
sendError(res, err.message);
}
});
@ -310,10 +310,11 @@ const handleCallApi = async (req: Request, res: Response) => {
}
const { action, params } = req.body;
const result = await adapter.callApi(action, params || {});
const result = await adapter.callApi(action as ActionNameType, params || {});
sendSuccess(res, result);
} catch (error: any) {
sendError(res, error.message);
} catch (error: unknown) {
const err = error as Error;
sendError(res, err.message);
}
};
@ -329,10 +330,25 @@ router.post('/close/:adapterName', async (req: Request, res: Response) => {
if (!adapterName) {
return sendError(res, '缺少 adapterName 参数');
}
const adapter = debugAdapterManager.getAdapter(adapterName);
if (!adapter) {
return sendError(res, '调试适配器不存在');
}
// 先从管理器移除,避免重复销毁
debugAdapterManager.removeAdapter(adapterName);
// 使用 NetworkManager 标准流程关闭适配器
const oneBotContext = WebUiDataRuntime.getOneBotContext();
if (oneBotContext) {
await oneBotContext.networkManager.closeSomeAdapters([adapter]);
}
sendSuccess(res, { message: '调试适配器已关闭' });
} catch (error: any) {
sendError(res, error.message);
} catch (error: unknown) {
const err = error as Error;
sendError(res, err.message);
}
});
@ -340,7 +356,7 @@ router.post('/close/:adapterName', async (req: Request, res: Response) => {
* WebSocket
* : /api/Debug/ws?adapterName=xxx&token=xxx
*/
export function handleDebugWebSocket (request: IncomingMessage, socket: any, head: any) {
export function handleDebugWebSocket (request: IncomingMessage, socket: unknown, head: unknown) {
const url = new URL(request.url || '', `http://${request.headers.host}`);
let adapterName = url.searchParams.get('adapterName');
const token = url.searchParams.get('token') || url.searchParams.get('access_token');
@ -353,8 +369,8 @@ export function handleDebugWebSocket (request: IncomingMessage, socket: any, hea
// Debug session should provide token
if (!token) {
console.log('[Debug] WebSocket 连接被拒绝: 缺少 Token');
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 401 Unauthorized\r\n\r\n');
(socket as { destroy: () => void; }).destroy();
return;
}
@ -362,43 +378,37 @@ export function handleDebugWebSocket (request: IncomingMessage, socket: any, hea
// 如果是默认 adapter 且不存在,尝试创建
if (!adapter && adapterName === DEFAULT_ADAPTER_NAME) {
adapter = debugAdapterManager.getOrCreateAdapter();
try {
adapter = debugAdapterManager.getOrCreateAdapter();
} catch (error) {
console.log('[Debug] WebSocket 连接被拒绝: 无法创建适配器', error);
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
(socket as { destroy: () => void; }).destroy();
return;
}
}
if (!adapter) {
console.log('[Debug] WebSocket 连接被拒绝: 适配器不存在');
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.destroy();
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 404 Not Found\r\n\r\n');
(socket as { destroy: () => void; }).destroy();
return;
}
if (!adapter.validateToken(token)) {
console.log('[Debug] WebSocket 连接被拒绝: Token 无效');
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
socket.destroy();
(socket as { write: (data: string) => void; destroy: () => void; }).write('HTTP/1.1 403 Forbidden\r\n\r\n');
(socket as { destroy: () => void; }).destroy();
return;
}
// 创建 WebSocket 服务器
const wsServer = new WebSocketServer({ noServer: true });
wsServer.handleUpgrade(request, socket, head, (ws) => {
adapter.addWsClient(ws);
ws.on('message', async (data) => {
try {
await adapter.handleWsMessage(ws, data as any);
} catch (error: any) {
console.error('[Debug] handleWsMessage error', error);
}
});
ws.on('close', () => {
adapter.removeWsClient(ws);
});
ws.on('error', () => {
adapter.removeWsClient(ws);
wsServer.handleUpgrade(request, socket as never, head as Buffer, (ws) => {
adapter.addWsClient(ws).catch((e: unknown) => {
console.error('[Debug] 添加 WebSocket 客户端失败:', e);
ws.close();
});
});
}

View File

@ -3,7 +3,7 @@ import { GetThemeConfigHandler, GetNapCatVersion, QQVersionHandler, SetThemeConf
import { StatusRealTimeHandler } from '@/napcat-webui-backend/src/api/Status';
import { GetProxyHandler } from '../api/Proxy';
const router = Router();
const router: Router = Router();
// router: 获取nc的package.json信息
router.get('/QQVersion', QQVersionHandler);
router.get('/GetNapCatVersion', GetNapCatVersion);

View File

@ -18,7 +18,7 @@ import {
DeleteWebUIFontHandler, // 添加上传处理器
} from '../api/File';
const router = Router();
const router: Router = Router();
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1分钟内

View File

@ -8,7 +8,7 @@ import {
CloseTerminalHandler,
} from '../api/Log';
const router = Router();
const router: Router = Router();
// 日志相关路由
router.get('/GetLog', LogHandler);

View File

@ -2,7 +2,7 @@ import { Router } from 'express';
import { OB11GetConfigHandler, OB11SetConfigHandler } from '@/napcat-webui-backend/src/api/OB11Config';
const router = Router();
const router: Router = Router();
// router:读取配置
router.post('/GetConfig', OB11GetConfigHandler);
// router:写入配置

View File

@ -1,7 +1,7 @@
import { Router } from 'express';
import { GetPluginListHandler, ReloadPluginHandler, SetPluginStatusHandler, UninstallPluginHandler } from '@/napcat-webui-backend/src/api/Plugin';
const router = Router();
const router: Router = Router();
router.get('/List', GetPluginListHandler);
router.post('/Reload', ReloadPluginHandler);

View File

@ -1,7 +1,7 @@
import { Router } from 'express';
import { RestartProcessHandler } from '../api/Process';
const router = Router();
const router: Router = Router();
// POST /api/Process/Restart - 重启进程
router.post('/Restart', RestartProcessHandler);

View File

@ -12,7 +12,7 @@ import {
QQRefreshQRcodeHandler,
} from '@/napcat-webui-backend/src/api/QQLogin';
const router = Router();
const router: Router = Router();
// router:获取快速登录列表
router.all('/GetQuickLoginList', QQGetQuickLoginListHandler);
// router:获取快速登录列表(新)

View File

@ -5,7 +5,7 @@
import { Router } from 'express';
import { UpdateNapCatHandler } from '@/napcat-webui-backend/src/api/UpdateNapCat';
const router = Router();
const router: Router = Router();
// POST /api/UpdateNapCat/update - 更新NapCat
router.post('/update', UpdateNapCatHandler);

View File

@ -8,7 +8,7 @@ import {
UpdateWebUIConfigHandler,
} from '@/napcat-webui-backend/src/api/WebUIConfig';
const router = Router();
const router: Router = Router();
// 获取WebUI基础配置
router.get('/GetConfig', GetWebUIConfigHandler);

View File

@ -11,7 +11,7 @@ import {
VerifyPasskeyAuthenticationHandler,
} from '@/napcat-webui-backend/src/api/Auth';
const router = Router();
const router: Router = Router();
// router:登录
router.post('/login', LoginHandler);
// router:检查登录状态

View File

@ -19,7 +19,7 @@ import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
import { ProcessRouter } from './Process';
import { PluginRouter } from './Plugin';
const router = Router();
const router: Router = Router();
// 鉴权中间件
router.use(auth);

View File

@ -1,5 +1,5 @@
import multer from 'multer';
import { Request, Response } from 'express';
import { Request, Response, RequestHandler } from 'express';
import fs from 'fs';
import path from 'path';
import { randomUUID } from 'crypto';
@ -65,7 +65,7 @@ export const createDiskStorage = (uploadPath: string) => {
});
};
export const createDiskUpload = (uploadPath: string) => {
export const createDiskUpload = (uploadPath: string): RequestHandler => {
const upload = multer({
storage: createDiskStorage(uploadPath),
limits: {

View File

@ -1,7 +1,7 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import type { Request, Response } from 'express';
import type { Request, Response, RequestHandler } from 'express';
import { WebUiConfig } from '@/napcat-webui-backend/index';
// 支持的字体格式
@ -42,7 +42,7 @@ export const webUIFontStorage = multer.diskStorage({
},
});
export const webUIFontUpload = multer({
export const webUIFontUpload: RequestHandler = multer({
storage: webUIFontStorage,
fileFilter: (_, file, cb) => {
// 验证文件类型