mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 00:00:26 +00:00
Compare commits
3 Commits
v4.8.120
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc14763f0 | ||
|
|
6a6a0e0539 | ||
|
|
b930eea84f |
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
@@ -7,7 +7,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
|||||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||||
:loop_read
|
:loop_read
|
||||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||||
set "RetString=%%~b"
|
set RetString=%%b
|
||||||
goto :napcat_boot
|
goto :napcat_boot
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ for %%a in ("%RetString%") do (
|
|||||||
set "pathWithoutUninstall=%%~dpa"
|
set "pathWithoutUninstall=%%~dpa"
|
||||||
)
|
)
|
||||||
|
|
||||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||||
|
|
||||||
if not exist "%QQpath%" (
|
if not exist "%QQpath%" (
|
||||||
echo provided QQ path is invalid
|
echo provided QQ path is invalid
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
|||||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||||
:loop_read
|
:loop_read
|
||||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||||
set "RetString=%%~b"
|
set RetString=%%b
|
||||||
goto :napcat_boot
|
goto :napcat_boot
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ for %%a in ("%RetString%") do (
|
|||||||
set "pathWithoutUninstall=%%~dpa"
|
set "pathWithoutUninstall=%%~dpa"
|
||||||
)
|
)
|
||||||
|
|
||||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||||
|
|
||||||
if not exist "%QQpath%" (
|
if not exist "%QQpath%" (
|
||||||
echo provided QQ path is invalid
|
echo provided QQ path is invalid
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
|||||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||||
:loop_read
|
:loop_read
|
||||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||||
set "RetString=%%~b"
|
set RetString=%%b
|
||||||
goto :napcat_boot
|
goto :napcat_boot
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ for %%a in ("%RetString%") do (
|
|||||||
set "pathWithoutUninstall=%%~dpa"
|
set "pathWithoutUninstall=%%~dpa"
|
||||||
)
|
)
|
||||||
|
|
||||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||||
|
|
||||||
if not exist "%QQPath%" (
|
if not exist "%QQPath%" (
|
||||||
echo provided QQ path is invalid
|
echo provided QQ path is invalid
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
|||||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||||
:loop_read
|
:loop_read
|
||||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||||
set "RetString=%%~b"
|
set RetString=%%b
|
||||||
goto :napcat_boot
|
goto :napcat_boot
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ for %%a in ("%RetString%") do (
|
|||||||
set "pathWithoutUninstall=%%~dpa"
|
set "pathWithoutUninstall=%%~dpa"
|
||||||
)
|
)
|
||||||
|
|
||||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||||
|
|
||||||
if not exist "%QQPath%" (
|
if not exist "%QQPath%" (
|
||||||
echo provided QQ path is invalid
|
echo provided QQ path is invalid
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "qq-chat",
|
"name": "qq-chat",
|
||||||
"verHash": "c50d6326",
|
"verHash": "cc326038",
|
||||||
"version": "9.9.22-40768",
|
"version": "9.9.21-39038",
|
||||||
"linuxVersion": "3.2.20-40768",
|
"linuxVersion": "3.2.19-39038",
|
||||||
"linuxVerHash": "ab90fdfa",
|
"linuxVerHash": "c773cdf7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "QQ",
|
"description": "QQ",
|
||||||
"productName": "QQ",
|
"productName": "QQ",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"qd": "externals/devtools/cli/index.js"
|
"qd": "externals/devtools/cli/index.js"
|
||||||
},
|
},
|
||||||
"main": "./loadNapCat.js",
|
"main": "./loadNapCat.js",
|
||||||
"buildVersion": "40768",
|
"buildVersion": "39038",
|
||||||
"isPureShell": true,
|
"isPureShell": true,
|
||||||
"isByteCodeShell": true,
|
"isByteCodeShell": true,
|
||||||
"platform": "win32",
|
"platform": "win32",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.8.119",
|
"version": "4.8.116",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ export default function FileTable({
|
|||||||
setPreviewImages([])
|
setPreviewImages([])
|
||||||
setPreviewIndex(0)
|
setPreviewIndex(0)
|
||||||
setShowImage(false)
|
setShowImage(false)
|
||||||
setPage(1)
|
|
||||||
}, [currentPath])
|
}, [currentPath])
|
||||||
|
|
||||||
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||||
|
|||||||
@@ -171,8 +171,7 @@ const GenericForm = <T extends keyof NetworkConfigType>({
|
|||||||
|
|
||||||
export default GenericForm
|
export default GenericForm
|
||||||
export function random_token(length: number) {
|
export function random_token(length: number) {
|
||||||
const chars =
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?'
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?'
|
|
||||||
let result = ''
|
let result = ''
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import CryptoJS from 'crypto-js'
|
|
||||||
import { EventSourcePolyfill } from 'event-source-polyfill'
|
import { EventSourcePolyfill } from 'event-source-polyfill'
|
||||||
|
|
||||||
import { LogLevel } from '@/const/enum'
|
import { LogLevel } from '@/const/enum'
|
||||||
|
|
||||||
import { serverRequest } from '@/utils/request'
|
import { serverRequest } from '@/utils/request'
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
export interface Log {
|
export interface Log {
|
||||||
level: LogLevel
|
level: LogLevel
|
||||||
message: string
|
message: string
|
||||||
@@ -18,7 +17,7 @@ export default class WebUIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async loginWithToken(token: string) {
|
public static async loginWithToken(token: string) {
|
||||||
const sha256 = CryptoJS.SHA256(token + '.napcat').toString()
|
const sha256 = CryptoJS.SHA256(token + '.napcat').toString();
|
||||||
const { data } = await serverRequest.post<ServerResponse<AuthResponse>>(
|
const { data } = await serverRequest.post<ServerResponse<AuthResponse>>(
|
||||||
'/auth/login',
|
'/auth/login',
|
||||||
{ hash: sha256 }
|
{ hash: sha256 }
|
||||||
|
|||||||
@@ -182,4 +182,4 @@ const ServerConfigCard = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ServerConfigCard
|
export default ServerConfigCard
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.8.119",
|
"version": "4.8.116",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||||
|
|||||||
@@ -163,10 +163,8 @@ export function getQQVersionConfigPath(exePath: string = ''): string | undefined
|
|||||||
|
|
||||||
export function calcQQLevel(level?: QQLevel) {
|
export function calcQQLevel(level?: QQLevel) {
|
||||||
if (!level) return 0;
|
if (!level) return 0;
|
||||||
//const { penguinNum, crownNum, sunNum, moonNum, starNum } = level;
|
const { crownNum, sunNum, moonNum, starNum } = level;
|
||||||
const { crownNum, sunNum, moonNum, starNum } = level
|
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
||||||
//没补类型
|
|
||||||
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyWithBigInt(obj: any) {
|
export function stringifyWithBigInt(obj: any) {
|
||||||
@@ -206,4 +204,4 @@ export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '4.8.119';
|
export const napCatVersion = '4.8.116';
|
||||||
|
|||||||
16
src/core/external/appid.json
vendored
16
src/core/external/appid.json
vendored
@@ -386,21 +386,5 @@
|
|||||||
"9.9.21-39038": {
|
"9.9.21-39038": {
|
||||||
"appid": 537313906,
|
"appid": 537313906,
|
||||||
"qua": "V1_WIN_NQ_9.9.21_39038_GW_B"
|
"qua": "V1_WIN_NQ_9.9.21_39038_GW_B"
|
||||||
},
|
|
||||||
"9.9.22-40362": {
|
|
||||||
"appid": 537314212,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.22_40362_GW_B"
|
|
||||||
},
|
|
||||||
"3.2.20-40768": {
|
|
||||||
"appid": 537319840,
|
|
||||||
"qua": "V1_LNX_NQ_3.2.20_40768_GW_B"
|
|
||||||
},
|
|
||||||
"9.9.22-40768": {
|
|
||||||
"appid": 537319804,
|
|
||||||
"qua": "V1_WIN_NQ_9.9.22_40768_GW_B"
|
|
||||||
},
|
|
||||||
"6.9.82-40768": {
|
|
||||||
"appid": 537319829,
|
|
||||||
"qua": "V1_MAC_NQ_6.9.82_40768_GW_B"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
22
src/core/external/offset.json
vendored
22
src/core/external/offset.json
vendored
@@ -507,28 +507,8 @@
|
|||||||
"send": "7B025C8",
|
"send": "7B025C8",
|
||||||
"recv": "7B05F58"
|
"recv": "7B05F58"
|
||||||
},
|
},
|
||||||
"9.9.21-39038-x64": {
|
"9.9.21-39038-x64": {
|
||||||
"send": "313FB58",
|
"send": "313FB58",
|
||||||
"recv": "31432FC"
|
"recv": "31432FC"
|
||||||
},
|
|
||||||
"9.9.22-40362-x64": {
|
|
||||||
"send": "31C0EB8",
|
|
||||||
"recv": "31C465C"
|
|
||||||
},
|
|
||||||
"3.2.20-40768-x64": {
|
|
||||||
"send": "B69CFE0",
|
|
||||||
"recv": "B6A0A60"
|
|
||||||
},
|
|
||||||
"9.9.22-40768-x64": {
|
|
||||||
"send": "31C1838",
|
|
||||||
"recv": "31C4FDC"
|
|
||||||
},
|
|
||||||
"3.2.20-40768-arm64": {
|
|
||||||
"send": "7D49B18",
|
|
||||||
"recv": "7D4D4A8"
|
|
||||||
},
|
|
||||||
"6.9.82-40768-arm64": {
|
|
||||||
"send": "202A198",
|
|
||||||
"recv": "202B718"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -4,10 +4,7 @@ import { MessageUnique } from '@/common/message-unique';
|
|||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const SchemaData = Type.Object({
|
||||||
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
message_id: Type.Union([Type.Number(), Type.String()]),
|
||||||
msg_seq: Type.Optional(Type.String()),
|
|
||||||
msg_random: Type.Optional(Type.String()),
|
|
||||||
group_id: Type.Optional(Type.String()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
@@ -16,20 +13,6 @@ export default class DelEssenceMsg extends OneBotAction<Payload, unknown> {
|
|||||||
override payloadSchema = SchemaData;
|
override payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload): Promise<unknown> {
|
async _handle(payload: Payload): Promise<unknown> {
|
||||||
// 如果直接提供了 msg_seq, msg_random, group_id,优先使用
|
|
||||||
if (payload.msg_seq && payload.msg_random && payload.group_id) {
|
|
||||||
return await this.core.apis.GroupApi.removeGroupEssenceBySeq(
|
|
||||||
payload.group_id,
|
|
||||||
payload.msg_random,
|
|
||||||
payload.msg_seq,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有 message_id,则必须提供 msg_seq, msg_random, group_id
|
|
||||||
if (!payload.message_id) {
|
|
||||||
throw new Error('必须提供 message_id 或者同时提供 msg_seq, msg_random, group_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
const msg = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
const data = this.core.apis.GroupApi.essenceLRU.getValue(+payload.message_id);
|
const data = this.core.apis.GroupApi.essenceLRU.getValue(+payload.message_id);
|
||||||
|
|||||||
@@ -132,8 +132,6 @@ import { SetGroupAlbumMediaLike } from './extends/SetGroupAlbumMediaLike';
|
|||||||
import { DelGroupAlbumMedia } from './extends/DelGroupAlbumMedia';
|
import { DelGroupAlbumMedia } from './extends/DelGroupAlbumMedia';
|
||||||
import { CleanStreamTempFile } from './stream/CleanStreamTempFile';
|
import { CleanStreamTempFile } from './stream/CleanStreamTempFile';
|
||||||
import { DownloadFileStream } from './stream/DownloadFileStream';
|
import { DownloadFileStream } from './stream/DownloadFileStream';
|
||||||
import { DownloadFileRecordStream } from './stream/DownloadFileRecordStream';
|
|
||||||
import { DownloadFileImageStream } from './stream/DownloadFileImageStream';
|
|
||||||
import { TestDownloadStream } from './stream/TestStreamDownload';
|
import { TestDownloadStream } from './stream/TestStreamDownload';
|
||||||
import { UploadFileStream } from './stream/UploadFileStream';
|
import { UploadFileStream } from './stream/UploadFileStream';
|
||||||
|
|
||||||
@@ -142,8 +140,6 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
|||||||
const actionHandlers = [
|
const actionHandlers = [
|
||||||
new CleanStreamTempFile(obContext, core),
|
new CleanStreamTempFile(obContext, core),
|
||||||
new DownloadFileStream(obContext, core),
|
new DownloadFileStream(obContext, core),
|
||||||
new DownloadFileRecordStream(obContext, core),
|
|
||||||
new DownloadFileImageStream(obContext, core),
|
|
||||||
new TestDownloadStream(obContext, core),
|
new TestDownloadStream(obContext, core),
|
||||||
new UploadFileStream(obContext, core),
|
new UploadFileStream(obContext, core),
|
||||||
new DelGroupAlbumMedia(obContext, core),
|
new DelGroupAlbumMedia(obContext, core),
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ export const ActionName = {
|
|||||||
TestDownloadStream: 'test_download_stream',
|
TestDownloadStream: 'test_download_stream',
|
||||||
UploadFileStream: 'upload_file_stream',
|
UploadFileStream: 'upload_file_stream',
|
||||||
DownloadFileStream: 'download_file_stream',
|
DownloadFileStream: 'download_file_stream',
|
||||||
DownloadFileRecordStream: 'download_file_record_stream',
|
|
||||||
DownloadFileImageStream: 'download_file_image_stream',
|
|
||||||
|
|
||||||
DelGroupAlbumMedia: 'del_group_album_media',
|
DelGroupAlbumMedia: 'del_group_album_media',
|
||||||
SetGroupAlbumMediaLike: 'set_group_album_media_like',
|
SetGroupAlbumMediaLike: 'set_group_album_media_like',
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { OneBotAction, OneBotRequestToolkit } from '@/onebot/action/OneBotAction';
|
|
||||||
import { StreamPacket, StreamStatus } from './StreamBasic';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
|
||||||
|
|
||||||
export interface ResolvedFileInfo {
|
|
||||||
downloadPath: string;
|
|
||||||
fileName: string;
|
|
||||||
fileSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DownloadResult {
|
|
||||||
// 文件信息
|
|
||||||
file_name?: string;
|
|
||||||
file_size?: number;
|
|
||||||
chunk_size?: number;
|
|
||||||
|
|
||||||
// 分片数据
|
|
||||||
index?: number;
|
|
||||||
data?: string;
|
|
||||||
size?: number;
|
|
||||||
progress?: number;
|
|
||||||
base64_size?: number;
|
|
||||||
|
|
||||||
// 完成信息
|
|
||||||
total_chunks?: number;
|
|
||||||
total_bytes?: number;
|
|
||||||
message?: string;
|
|
||||||
data_type?: 'file_info' | 'file_chunk' | 'file_complete';
|
|
||||||
|
|
||||||
// 可选扩展字段
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
out_format?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class BaseDownloadStream<PayloadType, ResultType> extends OneBotAction<PayloadType, StreamPacket<ResultType>> {
|
|
||||||
protected async resolveDownload(file?: string): Promise<ResolvedFileInfo> {
|
|
||||||
const target = file || '';
|
|
||||||
let downloadPath = '';
|
|
||||||
let fileName = '';
|
|
||||||
let fileSize = 0;
|
|
||||||
|
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(target);
|
|
||||||
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
|
|
||||||
const { peer, msgId, elementId } = contextMsgFile;
|
|
||||||
downloadPath = await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
|
|
||||||
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList
|
|
||||||
.find(msg => msg.msgId === msgId);
|
|
||||||
const mixElement = rawMessage?.elements.find(e => e.elementId === elementId);
|
|
||||||
const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement;
|
|
||||||
if (!mixElementInner) throw new Error('element not found');
|
|
||||||
fileSize = parseInt(mixElementInner.fileSize?.toString() ?? '0');
|
|
||||||
fileName = mixElementInner.fileName ?? '';
|
|
||||||
return { downloadPath, fileName, fileSize };
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextModelIdFile = FileNapCatOneBotUUID.decodeModelId(target);
|
|
||||||
if (contextModelIdFile && contextModelIdFile.modelId) {
|
|
||||||
const { peer, modelId } = contextModelIdFile;
|
|
||||||
downloadPath = await this.core.apis.FileApi.downloadFileForModelId(peer, modelId, '');
|
|
||||||
return { downloadPath, fileName, fileSize };
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchResult = (await this.core.apis.FileApi.searchForFile([target]));
|
|
||||||
if (searchResult) {
|
|
||||||
downloadPath = await this.core.apis.FileApi.downloadFileById(searchResult.id, parseInt(searchResult.fileSize));
|
|
||||||
fileSize = parseInt(searchResult.fileSize);
|
|
||||||
fileName = searchResult.fileName;
|
|
||||||
return { downloadPath, fileName, fileSize };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('file not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async streamFileChunks(req: OneBotRequestToolkit, streamPath: string, chunkSize: number, chunkDataType: string): Promise<{ totalChunks: number; totalBytes: number }>
|
|
||||||
{
|
|
||||||
const stats = await fs.promises.stat(streamPath);
|
|
||||||
const totalSize = stats.size;
|
|
||||||
const readStream = fs.createReadStream(streamPath, { highWaterMark: chunkSize });
|
|
||||||
let chunkIndex = 0;
|
|
||||||
let bytesRead = 0;
|
|
||||||
for await (const chunk of readStream) {
|
|
||||||
const base64Chunk = (chunk as Buffer).toString('base64');
|
|
||||||
bytesRead += (chunk as Buffer).length;
|
|
||||||
await req.send({
|
|
||||||
type: StreamStatus.Stream,
|
|
||||||
data_type: chunkDataType,
|
|
||||||
index: chunkIndex,
|
|
||||||
data: base64Chunk,
|
|
||||||
size: (chunk as Buffer).length,
|
|
||||||
progress: Math.round((bytesRead / totalSize) * 100),
|
|
||||||
base64_size: base64Chunk.length
|
|
||||||
} as unknown as StreamPacket<any>);
|
|
||||||
chunkIndex++;
|
|
||||||
}
|
|
||||||
return { totalChunks: chunkIndex, totalBytes: bytesRead };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
|
||||||
import { OneBotRequestToolkit } from '@/onebot/action/OneBotAction';
|
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
|
||||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
|
||||||
import { StreamPacket, StreamStatus } from './StreamBasic';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { imageSizeFallBack } from '@/image-size';
|
|
||||||
import { BaseDownloadStream, DownloadResult } from './BaseDownloadStream';
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
|
||||||
file: Type.Optional(Type.String()),
|
|
||||||
file_id: Type.Optional(Type.String()),
|
|
||||||
chunk_size: Type.Optional(Type.Number({ default: 64 * 1024 })) // 默认64KB分块
|
|
||||||
});
|
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
export class DownloadFileImageStream extends BaseDownloadStream<Payload, DownloadResult> {
|
|
||||||
override actionName = ActionName.DownloadFileImageStream;
|
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
override useStream = true;
|
|
||||||
|
|
||||||
async _handle(payload: Payload, _adaptername: string, _config: NetworkAdapterConfig, req: OneBotRequestToolkit): Promise<StreamPacket<DownloadResult>> {
|
|
||||||
try {
|
|
||||||
payload.file ||= payload.file_id || '';
|
|
||||||
const chunkSize = payload.chunk_size || 64 * 1024;
|
|
||||||
|
|
||||||
const { downloadPath, fileName, fileSize } = await this.resolveDownload(payload.file);
|
|
||||||
|
|
||||||
const stats = await fs.promises.stat(downloadPath);
|
|
||||||
const totalSize = fileSize || stats.size;
|
|
||||||
const { width, height } = await imageSizeFallBack(downloadPath);
|
|
||||||
|
|
||||||
// 发送文件信息(与 DownloadFileStream 对齐,但包含宽高)
|
|
||||||
await req.send({
|
|
||||||
type: StreamStatus.Stream,
|
|
||||||
data_type: 'file_info',
|
|
||||||
file_name: fileName,
|
|
||||||
file_size: totalSize,
|
|
||||||
chunk_size: chunkSize,
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
});
|
|
||||||
|
|
||||||
const { totalChunks, totalBytes } = await this.streamFileChunks(req, downloadPath, chunkSize, 'file_chunk');
|
|
||||||
|
|
||||||
// 返回完成状态(与 DownloadFileStream 对齐)
|
|
||||||
return {
|
|
||||||
type: StreamStatus.Response,
|
|
||||||
data_type: 'file_complete',
|
|
||||||
total_chunks: totalChunks,
|
|
||||||
total_bytes: totalBytes,
|
|
||||||
message: 'Download completed'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Download failed: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
|
|
||||||
import { ActionName } from '@/onebot/action/router';
|
|
||||||
import { OneBotRequestToolkit } from '@/onebot/action/OneBotAction';
|
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
|
||||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
|
||||||
import { StreamPacket, StreamStatus } from './StreamBasic';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { decode } from 'silk-wasm';
|
|
||||||
import { FFmpegService } from '@/common/ffmpeg';
|
|
||||||
import { BaseDownloadStream } from './BaseDownloadStream';
|
|
||||||
|
|
||||||
const out_format = ['mp3', 'amr', 'wma', 'm4a', 'spx', 'ogg', 'wav', 'flac'];
|
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
|
||||||
file: Type.Optional(Type.String()),
|
|
||||||
file_id: Type.Optional(Type.String()),
|
|
||||||
chunk_size: Type.Optional(Type.Number({ default: 64 * 1024 })), // 默认64KB分块
|
|
||||||
out_format: Type.Optional(Type.String())
|
|
||||||
});
|
|
||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
|
||||||
|
|
||||||
import { DownloadResult } from './BaseDownloadStream';
|
|
||||||
|
|
||||||
export class DownloadFileRecordStream extends BaseDownloadStream<Payload, DownloadResult> {
|
|
||||||
override actionName = ActionName.DownloadFileRecordStream;
|
|
||||||
override payloadSchema = SchemaData;
|
|
||||||
override useStream = true;
|
|
||||||
|
|
||||||
async _handle(payload: Payload, _adaptername: string, _config: NetworkAdapterConfig, req: OneBotRequestToolkit): Promise<StreamPacket<DownloadResult>> {
|
|
||||||
try {
|
|
||||||
payload.file ||= payload.file_id || '';
|
|
||||||
const chunkSize = payload.chunk_size || 64 * 1024;
|
|
||||||
|
|
||||||
const { downloadPath, fileName, fileSize } = await this.resolveDownload(payload.file);
|
|
||||||
|
|
||||||
// 处理输出格式转换
|
|
||||||
let streamPath = downloadPath;
|
|
||||||
if (payload.out_format && typeof payload.out_format === 'string') {
|
|
||||||
if (!out_format.includes(payload.out_format)) {
|
|
||||||
throw new Error('转换失败 out_format 字段可能格式不正确');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pcmFile = `${downloadPath}.pcm`;
|
|
||||||
const outputFile = `${downloadPath}.${payload.out_format}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 如果已存在目标文件则跳过转换
|
|
||||||
await fs.promises.access(outputFile);
|
|
||||||
streamPath = outputFile;
|
|
||||||
} catch {
|
|
||||||
// 尝试解码 silk 到 pcm 再用 ffmpeg 转换
|
|
||||||
await this.decodeFile(downloadPath, pcmFile);
|
|
||||||
await FFmpegService.convertFile(pcmFile, outputFile, payload.out_format);
|
|
||||||
streamPath = outputFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = await fs.promises.stat(streamPath);
|
|
||||||
const totalSize = fileSize || stats.size;
|
|
||||||
|
|
||||||
await req.send({
|
|
||||||
type: StreamStatus.Stream,
|
|
||||||
data_type: 'file_info',
|
|
||||||
file_name: fileName,
|
|
||||||
file_size: totalSize,
|
|
||||||
chunk_size: chunkSize,
|
|
||||||
out_format: payload.out_format
|
|
||||||
});
|
|
||||||
|
|
||||||
const { totalChunks, totalBytes } = await this.streamFileChunks(req, streamPath, chunkSize, 'file_chunk');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: StreamStatus.Response,
|
|
||||||
data_type: 'file_complete',
|
|
||||||
total_chunks: totalChunks,
|
|
||||||
total_bytes: totalBytes,
|
|
||||||
message: 'Download completed'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Download failed: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async decodeFile(inputFile: string, outputFile: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const inputData = await fs.promises.readFile(inputFile);
|
|
||||||
const decodedData = await decode(inputData, 24000);
|
|
||||||
await fs.promises.writeFile(outputFile, Buffer.from(decodedData.data));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding file:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
import { OneBotRequestToolkit } from '@/onebot/action/OneBotAction';
|
import { OneBotAction, OneBotRequestToolkit } from '@/onebot/action/OneBotAction';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
import { NetworkAdapterConfig } from '@/onebot/config/config';
|
||||||
import { StreamPacket, StreamStatus } from './StreamBasic';
|
import { StreamPacket, StreamStatus } from './StreamBasic';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { BaseDownloadStream, DownloadResult } from './BaseDownloadStream';
|
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||||
const SchemaData = Type.Object({
|
const SchemaData = Type.Object({
|
||||||
file: Type.Optional(Type.String()),
|
file: Type.Optional(Type.String()),
|
||||||
file_id: Type.Optional(Type.String()),
|
file_id: Type.Optional(Type.String()),
|
||||||
@@ -13,7 +13,28 @@ const SchemaData = Type.Object({
|
|||||||
|
|
||||||
type Payload = Static<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
export class DownloadFileStream extends BaseDownloadStream<Payload, DownloadResult> {
|
// 下载结果类型
|
||||||
|
interface DownloadResult {
|
||||||
|
// 文件信息
|
||||||
|
file_name?: string;
|
||||||
|
file_size?: number;
|
||||||
|
chunk_size?: number;
|
||||||
|
|
||||||
|
// 分片数据
|
||||||
|
index?: number;
|
||||||
|
data?: string;
|
||||||
|
size?: number;
|
||||||
|
progress?: number;
|
||||||
|
base64_size?: number;
|
||||||
|
|
||||||
|
// 完成信息
|
||||||
|
total_chunks?: number;
|
||||||
|
total_bytes?: number;
|
||||||
|
message?: string;
|
||||||
|
data_type?: 'file_info' | 'file_chunk' | 'file_complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DownloadFileStream extends OneBotAction<Payload, StreamPacket<DownloadResult>> {
|
||||||
override actionName = ActionName.DownloadFileStream;
|
override actionName = ActionName.DownloadFileStream;
|
||||||
override payloadSchema = SchemaData;
|
override payloadSchema = SchemaData;
|
||||||
override useStream = true;
|
override useStream = true;
|
||||||
@@ -22,12 +43,50 @@ export class DownloadFileStream extends BaseDownloadStream<Payload, DownloadResu
|
|||||||
try {
|
try {
|
||||||
payload.file ||= payload.file_id || '';
|
payload.file ||= payload.file_id || '';
|
||||||
const chunkSize = payload.chunk_size || 64 * 1024;
|
const chunkSize = payload.chunk_size || 64 * 1024;
|
||||||
|
let downloadPath = '';
|
||||||
|
let fileName = '';
|
||||||
|
let fileSize = 0;
|
||||||
|
|
||||||
const { downloadPath, fileName, fileSize } = await this.resolveDownload(payload.file);
|
//接收消息标记模式
|
||||||
|
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file);
|
||||||
|
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
|
||||||
|
const { peer, msgId, elementId } = contextMsgFile;
|
||||||
|
downloadPath = await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
|
||||||
|
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList
|
||||||
|
.find(msg => msg.msgId === msgId);
|
||||||
|
const mixElement = rawMessage?.elements.find(e => e.elementId === elementId);
|
||||||
|
const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement;
|
||||||
|
if (!mixElementInner) throw new Error('element not found');
|
||||||
|
fileSize = parseInt(mixElementInner.fileSize?.toString() ?? '0');
|
||||||
|
fileName = mixElementInner.fileName ?? '';
|
||||||
|
}
|
||||||
|
//群文件模式
|
||||||
|
else if (FileNapCatOneBotUUID.decodeModelId(payload.file)) {
|
||||||
|
const contextModelIdFile = FileNapCatOneBotUUID.decodeModelId(payload.file);
|
||||||
|
if (contextModelIdFile && contextModelIdFile.modelId) {
|
||||||
|
const { peer, modelId } = contextModelIdFile;
|
||||||
|
downloadPath = await this.core.apis.FileApi.downloadFileForModelId(peer, modelId, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//搜索名字模式
|
||||||
|
else {
|
||||||
|
const searchResult = (await this.core.apis.FileApi.searchForFile([payload.file]));
|
||||||
|
if (searchResult) {
|
||||||
|
downloadPath = await this.core.apis.FileApi.downloadFileById(searchResult.id, parseInt(searchResult.fileSize));
|
||||||
|
fileSize = parseInt(searchResult.fileSize);
|
||||||
|
fileName = searchResult.fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!downloadPath) {
|
||||||
|
throw new Error('file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件大小
|
||||||
const stats = await fs.promises.stat(downloadPath);
|
const stats = await fs.promises.stat(downloadPath);
|
||||||
const totalSize = fileSize || stats.size;
|
const totalSize = fileSize || stats.size;
|
||||||
|
|
||||||
|
// 发送文件信息
|
||||||
await req.send({
|
await req.send({
|
||||||
type: StreamStatus.Stream,
|
type: StreamStatus.Stream,
|
||||||
data_type: 'file_info',
|
data_type: 'file_info',
|
||||||
@@ -36,13 +95,34 @@ export class DownloadFileStream extends BaseDownloadStream<Payload, DownloadResu
|
|||||||
chunk_size: chunkSize
|
chunk_size: chunkSize
|
||||||
});
|
});
|
||||||
|
|
||||||
const { totalChunks, totalBytes } = await this.streamFileChunks(req, downloadPath, chunkSize, 'file_chunk');
|
// 创建读取流并分块发送
|
||||||
|
const readStream = fs.createReadStream(downloadPath, { highWaterMark: chunkSize });
|
||||||
|
let chunkIndex = 0;
|
||||||
|
let bytesRead = 0;
|
||||||
|
|
||||||
|
for await (const chunk of readStream) {
|
||||||
|
const base64Chunk = chunk.toString('base64');
|
||||||
|
bytesRead += chunk.length;
|
||||||
|
|
||||||
|
await req.send({
|
||||||
|
type: StreamStatus.Stream,
|
||||||
|
data_type: 'file_chunk',
|
||||||
|
index: chunkIndex,
|
||||||
|
data: base64Chunk,
|
||||||
|
size: chunk.length,
|
||||||
|
progress: Math.round((bytesRead / totalSize) * 100),
|
||||||
|
base64_size: base64Chunk.length
|
||||||
|
});
|
||||||
|
|
||||||
|
chunkIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回完成状态
|
||||||
return {
|
return {
|
||||||
type: StreamStatus.Response,
|
type: StreamStatus.Response,
|
||||||
data_type: 'file_complete',
|
data_type: 'file_complete',
|
||||||
total_chunks: totalChunks,
|
total_chunks: chunkIndex,
|
||||||
total_bytes: totalBytes,
|
total_bytes: bytesRead,
|
||||||
message: 'Download completed'
|
message: 'Download completed'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,17 @@ export class OneBotGroupApi {
|
|||||||
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
|
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
|
||||||
if (member && member.cardName !== msg.sendMemberName) {
|
if (member && member.cardName !== msg.sendMemberName) {
|
||||||
const newCardName = msg.sendMemberName ?? '';
|
const newCardName = msg.sendMemberName ?? '';
|
||||||
|
// 防止误触:如果消息包含转发元素且新名片为空,不触发事件(转发消息的sendMemberName不可靠)
|
||||||
|
const hasForwardElement = msg.elements.some(e => e.multiForwardMsgElement || e.arkElement);
|
||||||
|
if (newCardName === '' && member.cardName !== '' && hasForwardElement) {
|
||||||
|
this.core.context.logger.logDebug('忽略转发消息的不可靠群名片变更事件', {
|
||||||
|
peerUid: msg.peerUid,
|
||||||
|
senderUin: msg.senderUin,
|
||||||
|
oldCard: member.cardName,
|
||||||
|
newCard: newCardName
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
|
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
|
||||||
member.cardName = newCardName;
|
member.cardName = newCardName;
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ export class OneBotMsgApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.peer || !atQQ || context.peer.chatType == ChatType.KCHATTYPEC2C) return undefined; // 过滤掉空atQQ
|
if (!context.peer || context.peer.chatType == ChatType.KCHATTYPEC2C) return undefined;
|
||||||
if (atQQ === 'all') return at(atQQ, atQQ, NTMsgAtType.ATTYPEALL, '全体成员');
|
if (atQQ === 'all') return at(atQQ, atQQ, NTMsgAtType.ATTYPEALL, '全体成员');
|
||||||
const atMember = await this.core.apis.GroupApi.getGroupMember(context.peer.peerUid, atQQ);
|
const atMember = await this.core.apis.GroupApi.getGroupMember(context.peer.peerUid, atQQ);
|
||||||
if (atMember) {
|
if (atMember) {
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ export const createUrl = (
|
|||||||
url.searchParams.set(key, search[key])
|
url.searchParams.set(key, search[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return url.toString()
|
||||||
/** 进行url解码 对特殊字符进行处理 */
|
|
||||||
return decodeURIComponent(url.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user