mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-12 16:00:27 +00:00
Compare commits
8 Commits
copilot/ad
...
v4.8.121
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbacc89907 | ||
|
|
8e6da5e2d0 | ||
|
|
02980c4d1a | ||
|
|
0129188739 | ||
|
|
98ef642cd1 | ||
|
|
32e886e53b | ||
|
|
315d847f06 | ||
|
|
381d320967 |
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"verHash": "cc326038",
|
||||
"version": "9.9.21-39038",
|
||||
"linuxVersion": "3.2.19-39038",
|
||||
"linuxVerHash": "c773cdf7",
|
||||
"verHash": "c50d6326",
|
||||
"version": "9.9.22-40768",
|
||||
"linuxVersion": "3.2.20-40768",
|
||||
"linuxVerHash": "ab90fdfa",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
"productName": "QQ",
|
||||
@@ -17,7 +17,7 @@
|
||||
"qd": "externals/devtools/cli/index.js"
|
||||
},
|
||||
"main": "./loadNapCat.js",
|
||||
"buildVersion": "39038",
|
||||
"buildVersion": "40768",
|
||||
"isPureShell": true,
|
||||
"isByteCodeShell": true,
|
||||
"platform": "win32",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.8.119",
|
||||
"version": "4.8.120",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
24
napcat.webui/package-lock.json
generated
24
napcat.webui/package-lock.json
generated
@@ -93,7 +93,6 @@
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
@@ -8153,19 +8152,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-color": {
|
||||
"version": "3.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.13.tgz",
|
||||
"integrity": "sha512-2c/9FZ4ixC5T3JzN0LP5Cke2Mf0MKOP2Eh0NPDPWmuVH3NjPyhEjqNMQpN1Phr5m74egAy+p2lYNAFrX1z9Yrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/reactcss": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||
@@ -8186,16 +8172,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/reactcss": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.13.tgz",
|
||||
"integrity": "sha512-gi3S+aUi6kpkF5vdhUsnkwbiSEIU/BEJyD7kBy2SudWBUuKmJk8AQKE0OVcQQeEy40Azh0lV6uynxlikYIJuwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
|
||||
@@ -197,13 +197,4 @@ export default class WebUIManager {
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
public static async cleanCache() {
|
||||
const { data } =
|
||||
await serverRequest.post<
|
||||
ServerResponse<{ result: boolean; message: string }>
|
||||
>('/base/CleanCache')
|
||||
return data.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Input } from '@heroui/input'
|
||||
import { Switch } from '@heroui/switch'
|
||||
import { useRequest } from 'ahooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import { MdDeleteSweep } from 'react-icons/md'
|
||||
|
||||
import SaveButtons from '@/components/button/save_buttons'
|
||||
import PageLoading from '@/components/page_loading'
|
||||
|
||||
import useDialog from '@/hooks/use-dialog'
|
||||
|
||||
import WebUIManager from '@/controllers/webui_manager'
|
||||
|
||||
const ServerConfigCard = () => {
|
||||
const dialog = useDialog()
|
||||
const [isCleaningCache, setIsCleaningCache] = useState(false)
|
||||
const {
|
||||
data: configData,
|
||||
loading: configLoading,
|
||||
@@ -75,42 +69,6 @@ const ServerConfigCard = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleCleanCache = () => {
|
||||
dialog.confirm({
|
||||
title: '清理缓存',
|
||||
content: (
|
||||
<div className="space-y-2">
|
||||
<p>确定要清理缓存吗?此操作将清理以下内容:</p>
|
||||
<ul className="list-disc list-inside text-sm text-default-600">
|
||||
<li>临时文件夹中的所有文件</li>
|
||||
<li>图片缓存 (Pic)</li>
|
||||
<li>语音缓存 (Ptt)</li>
|
||||
<li>视频缓存 (Video)</li>
|
||||
<li>文件缓存 (File)</li>
|
||||
<li>日志文件 (log)</li>
|
||||
</ul>
|
||||
<p className="text-warning text-sm">此操作不可撤销,请谨慎操作。</p>
|
||||
</div>
|
||||
),
|
||||
onConfirm: async () => {
|
||||
setIsCleaningCache(true)
|
||||
try {
|
||||
const result = await WebUIManager.cleanCache()
|
||||
if (result.result) {
|
||||
toast.success(result.message || '缓存清理成功')
|
||||
} else {
|
||||
toast.error(result.message || '缓存清理失败')
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = (error as Error).message
|
||||
toast.error(`清理缓存失败: ${msg}`)
|
||||
} finally {
|
||||
setIsCleaningCache(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
}, [configData])
|
||||
@@ -173,30 +131,6 @@ const ServerConfigCard = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-shrink-0 w-full">维护操作</div>
|
||||
<div className="flex flex-col gap-2 p-4 rounded-lg bg-default-50">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">清理缓存</div>
|
||||
<div className="text-sm text-default-500">
|
||||
清理临时文件、图片、语音、视频、文件缓存和日志文件
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
startContent={<MdDeleteSweep size={20} />}
|
||||
onPress={handleCleanCache}
|
||||
isLoading={isCleaningCache}
|
||||
isDisabled={!!configError}
|
||||
>
|
||||
清理缓存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-shrink-0 w-full">安全配置</div>
|
||||
<Controller
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "napcat",
|
||||
"version": "4.8.119",
|
||||
"version": "4.8.98",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "napcat",
|
||||
"version": "4.8.119",
|
||||
"version": "4.8.98",
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.8.119",
|
||||
"version": "4.8.120",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
|
||||
@@ -163,8 +163,10 @@ export function getQQVersionConfigPath(exePath: string = ''): string | undefined
|
||||
|
||||
export function calcQQLevel(level?: QQLevel) {
|
||||
if (!level) return 0;
|
||||
const { crownNum, sunNum, moonNum, starNum } = level;
|
||||
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
||||
//const { penguinNum, crownNum, sunNum, moonNum, starNum } = level;
|
||||
const { crownNum, sunNum, moonNum, starNum } = level
|
||||
//没补类型
|
||||
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
||||
}
|
||||
|
||||
export function stringifyWithBigInt(obj: any) {
|
||||
@@ -204,4 +206,4 @@ export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.8.119';
|
||||
export const napCatVersion = '4.8.120';
|
||||
|
||||
24
src/core/external/appid.json
vendored
24
src/core/external/appid.json
vendored
@@ -390,5 +390,29 @@
|
||||
"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"
|
||||
},
|
||||
"3.2.20-40824": {
|
||||
"appid": 537319840,
|
||||
"qua": "V1_LNX_NQ_3.2.20_40824_GW_B"
|
||||
},
|
||||
"9.9.22-40824": {
|
||||
"appid": 537319804,
|
||||
"qua": "V1_WIN_NQ_9.9.22_40824_GW_B"
|
||||
},
|
||||
"6.9.82-40824": {
|
||||
"appid": 537319829,
|
||||
"qua": "V1_MAC_NQ_6.9.82_40824_GW_B"
|
||||
}
|
||||
}
|
||||
28
src/core/external/offset.json
vendored
28
src/core/external/offset.json
vendored
@@ -514,5 +514,33 @@
|
||||
"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"
|
||||
},
|
||||
"9.9.22-40824-x64": {
|
||||
"send": "31C1838",
|
||||
"recv": "31C4FDC"
|
||||
},
|
||||
"3.2.20-40824-arm64": {
|
||||
"send": "7D49B18",
|
||||
"recv": "7D4D4A8"
|
||||
},
|
||||
"6.9.82-40824-arm64": {
|
||||
"send": "202A198",
|
||||
"recv": "202B718"
|
||||
}
|
||||
}
|
||||
BIN
src/native/packet/MoeHoo.darwin.arm64.new.node
Normal file
BIN
src/native/packet/MoeHoo.darwin.arm64.new.node
Normal file
Binary file not shown.
@@ -4,7 +4,10 @@ import { MessageUnique } from '@/common/message-unique';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
|
||||
const SchemaData = Type.Object({
|
||||
message_id: Type.Union([Type.Number(), Type.String()]),
|
||||
message_id: Type.Optional(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>;
|
||||
@@ -13,6 +16,20 @@ export default class DelEssenceMsg extends OneBotAction<Payload, unknown> {
|
||||
override payloadSchema = SchemaData;
|
||||
|
||||
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);
|
||||
if (!msg) {
|
||||
const data = this.core.apis.GroupApi.essenceLRU.getValue(+payload.message_id);
|
||||
|
||||
@@ -132,6 +132,8 @@ import { SetGroupAlbumMediaLike } from './extends/SetGroupAlbumMediaLike';
|
||||
import { DelGroupAlbumMedia } from './extends/DelGroupAlbumMedia';
|
||||
import { CleanStreamTempFile } from './stream/CleanStreamTempFile';
|
||||
import { DownloadFileStream } from './stream/DownloadFileStream';
|
||||
import { DownloadFileRecordStream } from './stream/DownloadFileRecordStream';
|
||||
import { DownloadFileImageStream } from './stream/DownloadFileImageStream';
|
||||
import { TestDownloadStream } from './stream/TestStreamDownload';
|
||||
import { UploadFileStream } from './stream/UploadFileStream';
|
||||
|
||||
@@ -140,6 +142,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
|
||||
const actionHandlers = [
|
||||
new CleanStreamTempFile(obContext, core),
|
||||
new DownloadFileStream(obContext, core),
|
||||
new DownloadFileRecordStream(obContext, core),
|
||||
new DownloadFileImageStream(obContext, core),
|
||||
new TestDownloadStream(obContext, core),
|
||||
new UploadFileStream(obContext, core),
|
||||
new DelGroupAlbumMedia(obContext, core),
|
||||
|
||||
@@ -17,6 +17,8 @@ export const ActionName = {
|
||||
TestDownloadStream: 'test_download_stream',
|
||||
UploadFileStream: 'upload_file_stream',
|
||||
DownloadFileStream: 'download_file_stream',
|
||||
DownloadFileRecordStream: 'download_file_record_stream',
|
||||
DownloadFileImageStream: 'download_file_image_stream',
|
||||
|
||||
DelGroupAlbumMedia: 'del_group_album_media',
|
||||
SetGroupAlbumMediaLike: 'set_group_album_media_like',
|
||||
|
||||
99
src/onebot/action/stream/BaseDownloadStream.ts
Normal file
99
src/onebot/action/stream/BaseDownloadStream.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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 };
|
||||
}
|
||||
}
|
||||
60
src/onebot/action/stream/DownloadFileImageStream.ts
Normal file
60
src/onebot/action/stream/DownloadFileImageStream.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/onebot/action/stream/DownloadFileRecordStream.ts
Normal file
96
src/onebot/action/stream/DownloadFileRecordStream.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
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 { OneBotAction, OneBotRequestToolkit } from '@/onebot/action/OneBotAction';
|
||||
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 { FileNapCatOneBotUUID } from '@/common/file-uuid';
|
||||
import { BaseDownloadStream, DownloadResult } from './BaseDownloadStream';
|
||||
const SchemaData = Type.Object({
|
||||
file: Type.Optional(Type.String()),
|
||||
file_id: Type.Optional(Type.String()),
|
||||
@@ -13,28 +13,7 @@ const SchemaData = Type.Object({
|
||||
|
||||
type Payload = Static<typeof SchemaData>;
|
||||
|
||||
// 下载结果类型
|
||||
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>> {
|
||||
export class DownloadFileStream extends BaseDownloadStream<Payload, DownloadResult> {
|
||||
override actionName = ActionName.DownloadFileStream;
|
||||
override payloadSchema = SchemaData;
|
||||
override useStream = true;
|
||||
@@ -43,50 +22,12 @@ export class DownloadFileStream extends OneBotAction<Payload, StreamPacket<Downl
|
||||
try {
|
||||
payload.file ||= payload.file_id || '';
|
||||
const chunkSize = payload.chunk_size || 64 * 1024;
|
||||
let downloadPath = '';
|
||||
let fileName = '';
|
||||
let fileSize = 0;
|
||||
|
||||
//接收消息标记模式
|
||||
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;
|
||||
}
|
||||
}
|
||||
const { downloadPath, fileName, fileSize } = await this.resolveDownload(payload.file);
|
||||
|
||||
if (!downloadPath) {
|
||||
throw new Error('file not found');
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
const stats = await fs.promises.stat(downloadPath);
|
||||
const totalSize = fileSize || stats.size;
|
||||
|
||||
// 发送文件信息
|
||||
await req.send({
|
||||
type: StreamStatus.Stream,
|
||||
data_type: 'file_info',
|
||||
@@ -95,34 +36,13 @@ export class DownloadFileStream extends OneBotAction<Payload, StreamPacket<Downl
|
||||
chunk_size: chunkSize
|
||||
});
|
||||
|
||||
// 创建读取流并分块发送
|
||||
const readStream = fs.createReadStream(downloadPath, { highWaterMark: chunkSize });
|
||||
let chunkIndex = 0;
|
||||
let bytesRead = 0;
|
||||
const { totalChunks, totalBytes } = await this.streamFileChunks(req, downloadPath, chunkSize, 'file_chunk');
|
||||
|
||||
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 {
|
||||
type: StreamStatus.Response,
|
||||
data_type: 'file_complete',
|
||||
total_chunks: chunkIndex,
|
||||
total_bytes: bytesRead,
|
||||
total_chunks: totalChunks,
|
||||
total_bytes: totalBytes,
|
||||
message: 'Download completed'
|
||||
};
|
||||
|
||||
|
||||
@@ -217,15 +217,6 @@ export class NapCatOneBot11Adapter {
|
||||
//this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
|
||||
await this.reloadNetwork(prev, newConfig);
|
||||
});
|
||||
WebUiDataRuntime.setCleanCacheCall(async () => {
|
||||
try {
|
||||
await this.actions.get('clean_cache')?.handle({});
|
||||
return { result: true, message: '缓存清理成功' };
|
||||
} catch (error) {
|
||||
this.context.logger.logError('清理缓存失败:', error);
|
||||
return { result: false, message: `清理缓存失败: ${(error as Error).message}` };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,8 +24,3 @@ export const SetThemeConfigHandler: RequestHandler = async (req, res) => {
|
||||
await WebUiConfig.UpdateTheme(theme);
|
||||
sendSuccess(res, { message: '更新成功' });
|
||||
};
|
||||
|
||||
export const CleanCacheHandler: RequestHandler = async (_, res) => {
|
||||
const result = await WebUiDataRuntime.requestCleanCache();
|
||||
sendSuccess(res, result);
|
||||
};
|
||||
|
||||
@@ -27,9 +27,6 @@ const LoginRuntime: LoginRuntimeType = {
|
||||
onQuickLoginRequested: async () => {
|
||||
return { result: false, message: '' };
|
||||
},
|
||||
onCleanCacheRequested: async () => {
|
||||
return { result: false, message: '' };
|
||||
},
|
||||
QQLoginList: [],
|
||||
NewQQLoginList: [],
|
||||
},
|
||||
@@ -133,14 +130,6 @@ export const WebUiDataRuntime = {
|
||||
return LoginRuntime.NapCatHelper.onOB11ConfigChanged(ob11);
|
||||
} as LoginRuntimeType['NapCatHelper']['onOB11ConfigChanged'],
|
||||
|
||||
setCleanCacheCall(func: LoginRuntimeType['NapCatHelper']['onCleanCacheRequested']): void {
|
||||
LoginRuntime.NapCatHelper.onCleanCacheRequested = func;
|
||||
},
|
||||
|
||||
requestCleanCache: function () {
|
||||
return LoginRuntime.NapCatHelper.onCleanCacheRequested();
|
||||
} as LoginRuntimeType['NapCatHelper']['onCleanCacheRequested'],
|
||||
|
||||
getPackageJson() {
|
||||
return LoginRuntime.packageJson;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express';
|
||||
import { CleanCacheHandler, GetThemeConfigHandler, PackageInfoHandler, QQVersionHandler, SetThemeConfigHandler } from '../api/BaseInfo';
|
||||
import { GetThemeConfigHandler, PackageInfoHandler, QQVersionHandler, SetThemeConfigHandler } from '../api/BaseInfo';
|
||||
import { StatusRealTimeHandler } from '@webapi/api/Status';
|
||||
import { GetProxyHandler } from '../api/Proxy';
|
||||
|
||||
@@ -11,6 +11,5 @@ router.get('/GetSysStatusRealTime', StatusRealTimeHandler);
|
||||
router.get('/proxy', GetProxyHandler);
|
||||
router.get('/Theme', GetThemeConfigHandler);
|
||||
router.post('/SetTheme', SetThemeConfigHandler);
|
||||
router.post('/CleanCache', CleanCacheHandler);
|
||||
|
||||
export { router as BaseRouter };
|
||||
|
||||
1
src/webui/src/types/data.d.ts
vendored
1
src/webui/src/types/data.d.ts
vendored
@@ -15,7 +15,6 @@ interface LoginRuntimeType {
|
||||
NapCatHelper: {
|
||||
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
|
||||
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise<void>;
|
||||
onCleanCacheRequested: () => Promise<{ result: boolean; message: string }>;
|
||||
QQLoginList: string[];
|
||||
NewQQLoginList: LoginListItem[];
|
||||
};
|
||||
|
||||
@@ -91,7 +91,9 @@ export const createUrl = (
|
||||
url.searchParams.set(key, search[key])
|
||||
}
|
||||
}
|
||||
return url.toString()
|
||||
|
||||
/** 进行url解码 对特殊字符进行处理 */
|
||||
return decodeURIComponent(url.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user