mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fa12865924 | ||
![]() |
ecdd717742 | ||
![]() |
6851334af9 | ||
![]() |
9051b29565 | ||
![]() |
95c7d3dfbd | ||
![]() |
bc1148c00a | ||
![]() |
d4556d9299 | ||
![]() |
5d389a2359 | ||
![]() |
305116874b | ||
![]() |
b08a29897f | ||
![]() |
b59c1d9122 | ||
![]() |
adb9cea701 | ||
![]() |
5e148d2e82 | ||
![]() |
a0d780558e | ||
![]() |
ad56065a4e | ||
![]() |
d6848e2855 |
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.5.6",
|
"version": "4.5.12",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -82,7 +82,7 @@ export default function FileTable({
|
|||||||
setPreviewImages([])
|
setPreviewImages([])
|
||||||
setPreviewIndex(0)
|
setPreviewIndex(0)
|
||||||
setShowImage(false)
|
setShowImage(false)
|
||||||
}, [files])
|
}, [currentPath])
|
||||||
|
|
||||||
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||||
const index = images.findIndex((image) => image.key === name)
|
const index = images.findIndex((image) => image.key === name)
|
||||||
|
@@ -74,6 +74,9 @@ export default function ImageNameButton({
|
|||||||
src={data}
|
src={data}
|
||||||
alt={name}
|
alt={name}
|
||||||
className="w-8 h-8 flex-shrink-0"
|
className="w-8 h-8 flex-shrink-0"
|
||||||
|
classNames={{
|
||||||
|
wrapper: 'w-8 h-8 flex-shrink-0'
|
||||||
|
}}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
69
napcat.webui/src/components/input/file_input.tsx
Normal file
69
napcat.webui/src/components/input/file_input.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Button } from '@heroui/button'
|
||||||
|
import { Input } from '@heroui/input'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export interface FileInputProps {
|
||||||
|
onChange: (file: File) => Promise<void> | void
|
||||||
|
onDelete?: () => Promise<void> | void
|
||||||
|
label?: string
|
||||||
|
accept?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileInput: React.FC<FileInputProps> = ({
|
||||||
|
onChange,
|
||||||
|
onDelete,
|
||||||
|
label,
|
||||||
|
accept
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
return (
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Input
|
||||||
|
isDisabled={isLoading}
|
||||||
|
ref={inputRef}
|
||||||
|
label={label}
|
||||||
|
type="file"
|
||||||
|
placeholder="选择文件"
|
||||||
|
accept={accept}
|
||||||
|
onChange={async (e) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
await onChange(file)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
if (inputRef.current) inputRef.current.value = ''
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isDisabled={isLoading}
|
||||||
|
onPress={async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
if (onDelete) await onDelete()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
if (inputRef.current) inputRef.current.value = ''
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileInput
|
@@ -196,4 +196,26 @@ export default class FileManager {
|
|||||||
)
|
)
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async uploadWebUIFont(file: File) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/font/upload/webui',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteWebUIFont() {
|
||||||
|
const { data } = await serverRequest.post<ServerResponse<boolean>>(
|
||||||
|
'/File/font/delete/webui'
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,11 +7,13 @@ import toast from 'react-hot-toast'
|
|||||||
import key from '@/const/key'
|
import key from '@/const/key'
|
||||||
|
|
||||||
import SaveButtons from '@/components/button/save_buttons'
|
import SaveButtons from '@/components/button/save_buttons'
|
||||||
|
import FileInput from '@/components/input/file_input'
|
||||||
import ImageInput from '@/components/input/image_input'
|
import ImageInput from '@/components/input/image_input'
|
||||||
|
|
||||||
import useMusic from '@/hooks/use-music'
|
import useMusic from '@/hooks/use-music'
|
||||||
|
|
||||||
import { siteConfig } from '@/config/site'
|
import { siteConfig } from '@/config/site'
|
||||||
|
import FileManager from '@/controllers/file_manager'
|
||||||
|
|
||||||
const WebUIConfigCard = () => {
|
const WebUIConfigCard = () => {
|
||||||
const {
|
const {
|
||||||
@@ -59,6 +61,35 @@ const WebUIConfigCard = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>WebUI配置 - NapCat WebUI</title>
|
<title>WebUI配置 - NapCat WebUI</title>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex-shrink-0 w-full">WebUI字体</div>
|
||||||
|
<div className="text-sm text-default-400">
|
||||||
|
此项不需要手动保存,上传成功后需清空网页缓存并刷新
|
||||||
|
<FileInput
|
||||||
|
label="中文字体"
|
||||||
|
onChange={async (file) => {
|
||||||
|
try {
|
||||||
|
await FileManager.uploadWebUIFont(file)
|
||||||
|
toast.success('上传成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload()
|
||||||
|
}, 1000)
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('上传失败: ' + (error as Error).message)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDelete={async () => {
|
||||||
|
try {
|
||||||
|
await FileManager.deleteWebUIFont()
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('删除失败: ' + (error as Error).message)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex-shrink-0 w-full">WebUI音乐播放器</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="musicListID"
|
name="musicListID"
|
||||||
@@ -70,6 +101,7 @@ const WebUIConfigCard = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex-shrink-0 w-full">背景图</div>
|
<div className="flex-shrink-0 w-full">背景图</div>
|
||||||
<Controller
|
<Controller
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.5.6",
|
"version": "4.5.12",
|
||||||
"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",
|
||||||
|
@@ -66,7 +66,7 @@ export abstract class ConfigBase<T> {
|
|||||||
|
|
||||||
private handleError(e: unknown, message: string): void {
|
private handleError(e: unknown, message: string): void {
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
this.core.context.logger.logError(`[Core] [Config] 操作配置文件格式错误,请检查配置文件:`, e.message);
|
this.core.context.logger.logError('[Core] [Config] 操作配置文件格式错误,请检查配置文件:', e.message);
|
||||||
} else {
|
} else {
|
||||||
this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message);
|
this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message);
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '4.5.6';
|
export const napCatVersion = '4.5.12';
|
||||||
|
@@ -8,13 +8,15 @@ import {
|
|||||||
PacketMsgPttElement,
|
PacketMsgPttElement,
|
||||||
PacketMsgVideoElement
|
PacketMsgVideoElement
|
||||||
} from '@/core/packet/message/element';
|
} from '@/core/packet/message/element';
|
||||||
import { ChatType } from '@/core';
|
import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core';
|
||||||
import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp';
|
import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp';
|
||||||
import {AIVoiceChatType} from '@/core/packet/entities/aiChat';
|
import {AIVoiceChatType} from '@/core/packet/entities/aiChat';
|
||||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from '@napneko/nap-proto-core';
|
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
|
||||||
import { IndexNode, MsgInfo } from '@/core/packet/transformer/proto';
|
import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto';
|
||||||
import {OidbPacket} from '@/core/packet/transformer/base';
|
import {OidbPacket} from '@/core/packet/transformer/base';
|
||||||
import {ImageOcrResult} from '@/core/packet/entities/ocrResult';
|
import {ImageOcrResult} from '@/core/packet/entities/ocrResult';
|
||||||
|
import {gunzipSync} from 'zlib';
|
||||||
|
import {PacketMsgConverter} from '@/core/packet/message/converter';
|
||||||
|
|
||||||
export class PacketOperationContext {
|
export class PacketOperationContext {
|
||||||
private readonly context: PacketContext;
|
private readonly context: PacketContext;
|
||||||
@@ -116,6 +118,13 @@ export class PacketOperationContext {
|
|||||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
|
const req = trans.DownloadGroupImage.build(groupUin, node);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.DownloadImage.parse(resp);
|
||||||
|
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||||
|
}
|
||||||
|
|
||||||
async ImageOCR(imgUrl: string) {
|
async ImageOCR(imgUrl: string) {
|
||||||
const req = trans.ImageOCR.build(imgUrl);
|
const req = trans.ImageOCR.build(imgUrl);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
@@ -195,4 +204,74 @@ export class PacketOperationContext {
|
|||||||
return res.msgInfo;
|
return res.msgInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async FetchForwardMsg(res_id: string): Promise<RawMessage[]> {
|
||||||
|
const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, res_id);
|
||||||
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
|
const res = trans.DownloadForwardMsg.parse(resp);
|
||||||
|
const inflate = gunzipSync(res.result.payload);
|
||||||
|
const result = new NapProtoMsg(LongMsgResult).decode(inflate);
|
||||||
|
|
||||||
|
const main = result.action.find((r) => r.actionCommand === 'MultiMsg');
|
||||||
|
if (!main?.actionData.msgBody) {
|
||||||
|
throw new Error('msgBody is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesPromises = main.actionData.msgBody.map(async (msg) => {
|
||||||
|
if (!msg?.body?.richText?.elems) {
|
||||||
|
throw new Error('msg.body.richText.elems is empty');
|
||||||
|
}
|
||||||
|
const rawChains = new PacketMsgConverter().packetMsgToRaw(msg?.body?.richText?.elems);
|
||||||
|
const elements = await Promise.all(
|
||||||
|
rawChains.map(async ([element, rawElem]) => {
|
||||||
|
if (element.picElement && rawElem?.commonElem?.pbElem) {
|
||||||
|
const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem);
|
||||||
|
const index = extra?.msgInfoBody[0]?.index;
|
||||||
|
if (msg?.responseHead.grp !== undefined) {
|
||||||
|
const groupUin = msg?.responseHead.grp?.groupUin ?? 0;
|
||||||
|
element.picElement = {
|
||||||
|
...element.picElement,
|
||||||
|
originImageUrl: await this.GetGroupImageUrl(groupUin, index!)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
element.picElement = {
|
||||||
|
...element.picElement,
|
||||||
|
originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
chatType: ChatType.KCHATTYPEGROUP,
|
||||||
|
elements: elements,
|
||||||
|
guildId: '',
|
||||||
|
isOnlineMsg: false,
|
||||||
|
msgId: '7467703692092974645', // TODO: no necessary
|
||||||
|
msgRandom: '0',
|
||||||
|
msgSeq: String(msg.contentHead.sequence ?? 0),
|
||||||
|
msgTime: String(msg.contentHead.timeStamp ?? 0),
|
||||||
|
msgType: NTMsgType.KMSGTYPEMIX,
|
||||||
|
parentMsgIdList: [],
|
||||||
|
parentMsgPeer: {
|
||||||
|
chatType: ChatType.KCHATTYPEGROUP,
|
||||||
|
peerUid: String(msg?.responseHead.grp?.groupUin ?? 0),
|
||||||
|
},
|
||||||
|
peerName: '',
|
||||||
|
peerUid: '1094950020',
|
||||||
|
peerUin: '1094950020',
|
||||||
|
recallTime: '0',
|
||||||
|
records: [],
|
||||||
|
sendNickName: msg?.responseHead.grp?.memberName ?? '',
|
||||||
|
sendRemarkName: msg?.responseHead.grp?.memberName ?? '',
|
||||||
|
senderUid: '',
|
||||||
|
senderUin: '1094950020',
|
||||||
|
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
|
||||||
|
subMsgType: 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return await Promise.all(messagesPromises);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
Peer,
|
|
||||||
ChatType,
|
ChatType,
|
||||||
ElementType,
|
ElementType,
|
||||||
MessageElement,
|
MessageElement,
|
||||||
|
Peer,
|
||||||
RawMessage,
|
RawMessage,
|
||||||
SendArkElement,
|
SendArkElement,
|
||||||
SendFaceElement,
|
SendFaceElement,
|
||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
PacketMultiMsgElement
|
PacketMultiMsgElement
|
||||||
} from '@/core/packet/message/element';
|
} from '@/core/packet/message/element';
|
||||||
import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
|
import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
|
||||||
|
import {NapProtoDecodeStructType} from '@napneko/nap-proto-core';
|
||||||
|
import {Elem} from '@/core/packet/transformer/proto';
|
||||||
|
|
||||||
const SupportedElementTypes = [
|
const SupportedElementTypes = [
|
||||||
ElementType.TEXT,
|
ElementType.TEXT,
|
||||||
@@ -154,4 +156,16 @@ export class PacketMsgConverter {
|
|||||||
}).filter((e) => e !== null)
|
}).filter((e) => e !== null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packetMsgToRaw(msg: NapProtoDecodeStructType<typeof Elem>[]): [MessageElement, NapProtoDecodeStructType<typeof Elem> | null][] {
|
||||||
|
const converters = [PacketMsgTextElement.parseElement,
|
||||||
|
PacketMsgAtElement.parseElement, PacketMsgReplyElement.parseElement, PacketMsgPicElement.parseElement];
|
||||||
|
return msg.map((element) => {
|
||||||
|
for (const converter of converters) {
|
||||||
|
const result = converter(element);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter((e) => e !== null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
import * as zlib from 'node:zlib';
|
import * as zlib from 'node:zlib';
|
||||||
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
|
||||||
import {
|
import {
|
||||||
CustomFace,
|
CustomFace,
|
||||||
Elem,
|
Elem,
|
||||||
|
FileExtra,
|
||||||
|
GroupFileExtra,
|
||||||
MarkdownData,
|
MarkdownData,
|
||||||
MentionExtra,
|
MentionExtra,
|
||||||
|
MsgInfo,
|
||||||
NotOnlineImage,
|
NotOnlineImage,
|
||||||
|
OidbSvcTrpcTcp0XE37_800Response,
|
||||||
QBigFaceExtra,
|
QBigFaceExtra,
|
||||||
QSmallFaceExtra,
|
QSmallFaceExtra,
|
||||||
MsgInfo,
|
|
||||||
OidbSvcTrpcTcp0XE37_800Response,
|
|
||||||
FileExtra,
|
|
||||||
GroupFileExtra
|
|
||||||
} from '@/core/packet/transformer/proto';
|
} from '@/core/packet/transformer/proto';
|
||||||
import {
|
import {
|
||||||
|
ElementType,
|
||||||
FaceType,
|
FaceType,
|
||||||
|
MessageElement,
|
||||||
NTMsgAtType,
|
NTMsgAtType,
|
||||||
PicType,
|
PicType,
|
||||||
SendArkElement,
|
SendArkElement,
|
||||||
@@ -32,6 +34,9 @@ import {
|
|||||||
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
|
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
|
||||||
import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
|
import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message';
|
||||||
|
|
||||||
|
export type ParseElementFnR = [MessageElement, NapProtoDecodeStructType<typeof Elem> | null] | undefined;
|
||||||
|
type ParseElementFn = (elem: NapProtoDecodeStructType<typeof Elem>) => ParseElementFnR;
|
||||||
|
|
||||||
// raw <-> packet
|
// raw <-> packet
|
||||||
// TODO: SendStructLongMsgElement
|
// TODO: SendStructLongMsgElement
|
||||||
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
||||||
@@ -51,6 +56,8 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static parseElement: ParseElementFn;
|
||||||
|
|
||||||
toPreview(): string {
|
toPreview(): string {
|
||||||
return '[暂不支持该消息类型喵~]';
|
return '[暂不支持该消息类型喵~]';
|
||||||
}
|
}
|
||||||
@@ -72,10 +79,29 @@ export class PacketMsgTextElement extends IPacketMsgElement<SendTextElement> {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
|
||||||
|
if (elem.text?.str && (elem.text?.attr6Buf === undefined || elem.text?.attr6Buf?.length === 0)) {
|
||||||
|
return [{
|
||||||
|
textElement: {
|
||||||
|
content: elem.text?.str,
|
||||||
|
atType: NTMsgAtType.ATTYPEUNKNOWN,
|
||||||
|
atUid: '',
|
||||||
|
atTinyId: '',
|
||||||
|
atNtUid: '',
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, null];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
override toPreview(): string {
|
override toPreview(): string {
|
||||||
return this.text;
|
return this.text;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class PacketMsgAtElement extends PacketMsgTextElement {
|
export class PacketMsgAtElement extends PacketMsgTextElement {
|
||||||
targetUid: string;
|
targetUid: string;
|
||||||
@@ -101,6 +127,22 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
|
|||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
|
||||||
|
if (elem.text?.str && (elem.text?.attr6Buf?.length ?? 100) >= 11) {
|
||||||
|
return [{
|
||||||
|
textElement: {
|
||||||
|
content: elem.text?.str,
|
||||||
|
atType: NTMsgAtType.ATTYPEONE,
|
||||||
|
atUid: String(Buffer.from(elem.text!.attr6Buf!).readUInt32BE(7)), // FIXME: hack
|
||||||
|
atTinyId: '',
|
||||||
|
atNtUid: '',
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, null];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||||
@@ -143,6 +185,22 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
|
||||||
|
if (elem.srcMsg && elem.srcMsg.pbReserve) {
|
||||||
|
const reserve = elem.srcMsg.pbReserve;
|
||||||
|
return [{
|
||||||
|
replyElement: {
|
||||||
|
replayMsgSeq: String(reserve.friendSeq ?? elem.srcMsg?.origSeqs?.[0] ?? 0),
|
||||||
|
replayMsgId: String(reserve.messageId ?? 0),
|
||||||
|
senderUin: String(elem?.srcMsg ?? 0)
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, null];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
override toPreview(): string {
|
override toPreview(): string {
|
||||||
return '[回复消息]';
|
return '[回复消息]';
|
||||||
}
|
}
|
||||||
@@ -198,6 +256,46 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
|
||||||
|
if (elem.face?.index) {
|
||||||
|
return [{
|
||||||
|
faceElement: {
|
||||||
|
faceIndex: elem.face.index,
|
||||||
|
faceType: FaceType.Normal
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, null];
|
||||||
|
}
|
||||||
|
if (elem?.commonElem?.serviceType === 37 && elem?.commonElem?.pbElem) {
|
||||||
|
const qface = new NapProtoMsg(QBigFaceExtra).decode(elem?.commonElem?.pbElem);
|
||||||
|
if (qface?.faceId) {
|
||||||
|
return [{
|
||||||
|
faceElement: {
|
||||||
|
faceIndex: qface.faceId,
|
||||||
|
faceType: FaceType.Normal
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (elem?.commonElem?.serviceType === 33 && elem?.commonElem?.pbElem) {
|
||||||
|
const qface = new NapProtoMsg(QSmallFaceExtra).decode(elem?.commonElem?.pbElem);
|
||||||
|
if (qface?.faceId) {
|
||||||
|
return [{
|
||||||
|
faceElement: {
|
||||||
|
faceIndex: qface.faceId,
|
||||||
|
faceType: FaceType.Normal
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
override toPreview(): string {
|
override toPreview(): string {
|
||||||
return '[表情]';
|
return '[表情]';
|
||||||
}
|
}
|
||||||
@@ -286,6 +384,60 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static override parseElement = (elem: NapProtoDecodeStructType<typeof Elem>): ParseElementFnR => {
|
||||||
|
if (elem?.commonElem?.serviceType === 48 || [10, 20].includes(elem?.commonElem?.businessType ?? 0)) {
|
||||||
|
const extra = new NapProtoMsg(MsgInfo).decode(elem.commonElem!.pbElem!);
|
||||||
|
const msgInfoBody = extra.msgInfoBody[0];
|
||||||
|
const index = msgInfoBody?.index;
|
||||||
|
return [{
|
||||||
|
picElement: {
|
||||||
|
fileSize: index?.info.fileSize ?? 0,
|
||||||
|
picWidth: index?.info?.width ?? 0,
|
||||||
|
picHeight: index?.info?.height ?? 0,
|
||||||
|
fileName: index?.info?.fileHash ?? '',
|
||||||
|
sourcePath: '',
|
||||||
|
original: false,
|
||||||
|
picType: PicType.NEWPIC_APNG,
|
||||||
|
fileUuid: '',
|
||||||
|
fileSubId: '',
|
||||||
|
thumbFileSize: 0,
|
||||||
|
summary: '[图片]',
|
||||||
|
thumbPath: new Map(),
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
}, elem];
|
||||||
|
}
|
||||||
|
if (elem?.notOnlineImage) {
|
||||||
|
const img = elem?.notOnlineImage; // url in originImageUrl
|
||||||
|
const preImg: MessageElement = {
|
||||||
|
picElement: {
|
||||||
|
fileSize: img.fileLen ?? 0,
|
||||||
|
picWidth: img.picWidth ?? 0,
|
||||||
|
picHeight: img.picHeight ?? 0,
|
||||||
|
fileName: Buffer.from(img.picMd5!).toString('hex') ?? '',
|
||||||
|
sourcePath: '',
|
||||||
|
original: false,
|
||||||
|
picType: PicType.NEWPIC_APNG,
|
||||||
|
fileUuid: '',
|
||||||
|
fileSubId: '',
|
||||||
|
thumbFileSize: 0,
|
||||||
|
summary: '[图片]',
|
||||||
|
thumbPath: new Map(),
|
||||||
|
},
|
||||||
|
elementType: ElementType.UNKNOWN,
|
||||||
|
elementId: '',
|
||||||
|
};
|
||||||
|
if (img.origUrl?.includes('&fileid=')) {
|
||||||
|
preImg.picElement!.originImageUrl = `https://multimedia.nt.qq.com.cn${img.origUrl}`;
|
||||||
|
} else {
|
||||||
|
preImg.picElement!.originImageUrl = `https://gchat.qpic.cn${img.origUrl}`;
|
||||||
|
}
|
||||||
|
return [preImg, elem];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
override toPreview(): string {
|
override toPreview(): string {
|
||||||
return this.summary;
|
return this.summary;
|
||||||
}
|
}
|
||||||
|
50
src/core/packet/transformer/highway/DownloadGroupImage.ts
Normal file
50
src/core/packet/transformer/highway/DownloadGroupImage.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import * as proto from '@/core/packet/transformer/proto';
|
||||||
|
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
|
||||||
|
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
|
||||||
|
import { IndexNode } from '@/core/packet/transformer/proto';
|
||||||
|
|
||||||
|
class DownloadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(group_uin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||||
|
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
|
||||||
|
reqHead: {
|
||||||
|
common: {
|
||||||
|
requestId: 1,
|
||||||
|
command: 200
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
requestType: 2,
|
||||||
|
businessType: 1,
|
||||||
|
sceneType: 2,
|
||||||
|
group: {
|
||||||
|
groupUin: group_uin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
agentType: 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
node: node,
|
||||||
|
download: {
|
||||||
|
video: {
|
||||||
|
busiType: 0,
|
||||||
|
sceneType: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return OidbBase.build(0x11C4, 200, body, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data: Buffer) {
|
||||||
|
const oidbBody = OidbBase.parse(data).body;
|
||||||
|
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DownloadGroupImage();
|
@@ -14,7 +14,7 @@ class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
|||||||
reqHead: {
|
reqHead: {
|
||||||
common: {
|
common: {
|
||||||
requestId: 1,
|
requestId: 1,
|
||||||
command: 100
|
command: 200
|
||||||
},
|
},
|
||||||
scene: {
|
scene: {
|
||||||
requestType: 2,
|
requestType: 2,
|
||||||
|
@@ -12,3 +12,4 @@ export { default as UploadPrivateImage } from './UploadPrivateImage';
|
|||||||
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
|
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
|
||||||
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
||||||
export { default as DownloadImage } from './DownloadImage';
|
export { default as DownloadImage } from './DownloadImage';
|
||||||
|
export { default as DownloadGroupImage } from './DownloadGroupImage';
|
||||||
|
37
src/core/packet/transformer/message/DownloadForwardMsg.ts
Normal file
37
src/core/packet/transformer/message/DownloadForwardMsg.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as proto from '@/core/packet/transformer/proto';
|
||||||
|
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base';
|
||||||
|
|
||||||
|
class DownloadForwardMsg extends PacketTransformer<typeof proto.RecvLongMsgResp> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(uid: string, resId: string): OidbPacket {
|
||||||
|
const req = new NapProtoMsg(proto.RecvLongMsgReq).encode({
|
||||||
|
info: {
|
||||||
|
uid: {
|
||||||
|
uid: uid
|
||||||
|
},
|
||||||
|
resId: resId,
|
||||||
|
acquire: true
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
field1: 2,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
cmd: 'trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg',
|
||||||
|
data: PacketHexStrBuilder(req)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(data: Buffer) {
|
||||||
|
return new NapProtoMsg(proto.RecvLongMsgResp).decode(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DownloadForwardMsg();
|
@@ -13,12 +13,12 @@ class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
|
|||||||
const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg);
|
const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg);
|
||||||
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
|
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
|
||||||
{
|
{
|
||||||
action: {
|
action: [{
|
||||||
actionCommand: 'MultiMsg',
|
actionCommand: 'MultiMsg',
|
||||||
actionData: {
|
actionData: {
|
||||||
msgBody: msgBody
|
msgBody: msgBody
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
|
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
|
||||||
|
@@ -1 +1,2 @@
|
|||||||
export { default as UploadForwardMsg } from './UploadForwardMsg';
|
export { default as UploadForwardMsg } from './UploadForwardMsg';
|
||||||
|
export { default as DownloadForwardMsg } from './DownloadForwardMsg';
|
@@ -2,7 +2,7 @@ import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
|
|||||||
import { PushMsgBody } from '@/core/packet/transformer/proto';
|
import { PushMsgBody } from '@/core/packet/transformer/proto';
|
||||||
|
|
||||||
export const LongMsgResult = {
|
export const LongMsgResult = {
|
||||||
action: ProtoField(2, () => LongMsgAction)
|
action: ProtoField(2, () => LongMsgAction, false, true)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LongMsgAction = {
|
export const LongMsgAction = {
|
||||||
|
@@ -15,7 +15,6 @@ let napCatInitialized = false; // 添加一个标志
|
|||||||
function createServiceProxy(ServiceName) {
|
function createServiceProxy(ServiceName) {
|
||||||
return new Proxy(() => { }, {
|
return new Proxy(() => { }, {
|
||||||
get: (target, FunctionName) => {
|
get: (target, FunctionName) => {
|
||||||
console.log(ServiceName, FunctionName);
|
|
||||||
if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') {
|
if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') {
|
||||||
return () => new Proxy({}, {
|
return () => new Proxy({}, {
|
||||||
get: function (target, ClassFunName, receiver) {
|
get: function (target, ClassFunName, receiver) {
|
||||||
|
@@ -3,6 +3,7 @@ import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward,
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
import { MessageUnique } from '@/common/message-unique';
|
import { MessageUnique } from '@/common/message-unique';
|
||||||
import { Static, Type } from '@sinclair/typebox';
|
import { Static, Type } from '@sinclair/typebox';
|
||||||
|
import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
|
||||||
|
|
||||||
const SchemaData = Type.Object({
|
const SchemaData = Type.Object({
|
||||||
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
|
||||||
@@ -57,24 +58,72 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, {
|
|||||||
throw new Error('message_id is required');
|
throw new Error('message_id is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fakeForwardMsg = (res_id: string) => {
|
||||||
|
return {
|
||||||
|
chatType: ChatType.KCHATTYPEGROUP,
|
||||||
|
elements: [{
|
||||||
|
elementType: ElementType.MULTIFORWARD,
|
||||||
|
elementId: '',
|
||||||
|
multiForwardMsgElement: {
|
||||||
|
resId: res_id,
|
||||||
|
fileName: '',
|
||||||
|
xmlContent: '',
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
guildId: '',
|
||||||
|
isOnlineMsg: false,
|
||||||
|
msgId: '', // TODO: no necessary
|
||||||
|
msgRandom: '0',
|
||||||
|
msgSeq: '',
|
||||||
|
msgTime: '',
|
||||||
|
msgType: NTMsgType.KMSGTYPEMIX,
|
||||||
|
parentMsgIdList: [],
|
||||||
|
parentMsgPeer: {
|
||||||
|
chatType: ChatType.KCHATTYPEGROUP,
|
||||||
|
peerUid: '',
|
||||||
|
},
|
||||||
|
peerName: '',
|
||||||
|
peerUid: '284840486',
|
||||||
|
peerUin: '284840486',
|
||||||
|
recallTime: '0',
|
||||||
|
records: [],
|
||||||
|
sendNickName: '',
|
||||||
|
sendRemarkName: '',
|
||||||
|
senderUid: '',
|
||||||
|
senderUin: '1094950020',
|
||||||
|
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
|
||||||
|
subMsgType: 1,
|
||||||
|
} as RawMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const protocolFallbackLogic = async (res_id: string) => {
|
||||||
|
const ob = (await this.obContext.apis.MsgApi.parseMessageV2(fakeForwardMsg(res_id)))?.arrayMsg;
|
||||||
|
if (ob) {
|
||||||
|
return {
|
||||||
|
messages: (ob?.message?.[0] as OB11MessageForward)?.data?.content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('protocolFallbackLogic: 找不到相关的聊天记录');
|
||||||
|
};
|
||||||
|
|
||||||
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
|
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
|
||||||
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
|
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
|
||||||
if (!rootMsg) {
|
if (!rootMsg) {
|
||||||
throw new Error('msg not found');
|
return await protocolFallbackLogic(msgId.toString());
|
||||||
}
|
}
|
||||||
const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]);
|
const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]);
|
||||||
|
|
||||||
if (!data || data.result !== 0) {
|
if (!data || data.result !== 0) {
|
||||||
throw new Error('找不到相关的聊天记录' + data?.errMsg);
|
return await protocolFallbackLogic(msgId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
const singleMsg = data.msgList[0];
|
const singleMsg = data.msgList[0];
|
||||||
if (!singleMsg) {
|
if (!singleMsg) {
|
||||||
throw new Error('找不到相关的聊天记录');
|
return await protocolFallbackLogic(msgId.toString());
|
||||||
}
|
}
|
||||||
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理
|
const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理
|
||||||
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
|
if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
|
||||||
throw new Error('找不到相关的聊天记录');
|
return await protocolFallbackLogic(msgId.toString());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
|
messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
|
||||||
|
@@ -1,25 +1,34 @@
|
|||||||
import {FileNapCatOneBotUUID} from '@/common/file-uuid';
|
import {FileNapCatOneBotUUID} from '@/common/file-uuid';
|
||||||
import {MessageUnique} from '@/common/message-unique';
|
import {MessageUnique} from '@/common/message-unique';
|
||||||
import {
|
import {
|
||||||
NTMsgAtType,
|
|
||||||
ChatType,
|
ChatType,
|
||||||
CustomMusicSignPostData,
|
CustomMusicSignPostData,
|
||||||
ElementType,
|
ElementType,
|
||||||
FaceIndex,
|
FaceIndex,
|
||||||
|
FaceType,
|
||||||
|
GrayTipElement,
|
||||||
|
GroupNotify,
|
||||||
IdMusicSignPostData,
|
IdMusicSignPostData,
|
||||||
MessageElement,
|
MessageElement,
|
||||||
NapCatCore,
|
NapCatCore,
|
||||||
NTGrayTipElementSubTypeV2,
|
NTGrayTipElementSubTypeV2,
|
||||||
|
NTMsgAtType,
|
||||||
Peer,
|
Peer,
|
||||||
RawMessage,
|
RawMessage,
|
||||||
SendMessageElement,
|
SendMessageElement,
|
||||||
SendTextElement,
|
SendTextElement,
|
||||||
FaceType,
|
|
||||||
GrayTipElement,
|
|
||||||
GroupNotify,
|
|
||||||
} from '@/core';
|
} from '@/core';
|
||||||
import faceConfig from '@/core/external/face_config.json';
|
import faceConfig from '@/core/external/face_config.json';
|
||||||
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, OB11MessageImage, OB11MessageVideo, } from '@/onebot';
|
import {
|
||||||
|
NapCatOneBot11Adapter,
|
||||||
|
OB11Message,
|
||||||
|
OB11MessageData,
|
||||||
|
OB11MessageDataType,
|
||||||
|
OB11MessageFileBase,
|
||||||
|
OB11MessageForward,
|
||||||
|
OB11MessageImage,
|
||||||
|
OB11MessageVideo,
|
||||||
|
} from '@/onebot';
|
||||||
import {OB11Construct} from '@/onebot/helper/data';
|
import {OB11Construct} from '@/onebot/helper/data';
|
||||||
import {EventType} from '@/onebot/event/OneBotEvent';
|
import {EventType} from '@/onebot/event/OneBotEvent';
|
||||||
import {encodeCQCode} from '@/onebot/helper/cqcode';
|
import {encodeCQCode} from '@/onebot/helper/cqcode';
|
||||||
@@ -30,7 +39,7 @@ import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNot
|
|||||||
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
|
import {ForwardMsgBuilder} from '@/common/forward-msg-builder';
|
||||||
import {NapProtoMsg} from '@napneko/nap-proto-core';
|
import {NapProtoMsg} from '@napneko/nap-proto-core';
|
||||||
import {OB11GroupIncreaseEvent} from '../event/notice/OB11GroupIncreaseEvent';
|
import {OB11GroupIncreaseEvent} from '../event/notice/OB11GroupIncreaseEvent';
|
||||||
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
|
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from '../event/notice/OB11GroupDecreaseEvent';
|
||||||
import {GroupAdmin} from '@/core/packet/transformer/proto/message/groupAdmin';
|
import {GroupAdmin} from '@/core/packet/transformer/proto/message/groupAdmin';
|
||||||
import {OB11GroupAdminNoticeEvent} from '../event/notice/OB11GroupAdminNoticeEvent';
|
import {OB11GroupAdminNoticeEvent} from '../event/notice/OB11GroupAdminNoticeEvent';
|
||||||
import {GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody} from '@/core/packet/transformer/proto';
|
import {GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody} from '@/core/packet/transformer/proto';
|
||||||
@@ -264,7 +273,6 @@ export class OneBotMsgApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 丢弃该消息段
|
// 丢弃该消息段
|
||||||
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
|
||||||
this.core.context.logger.logError(
|
this.core.context.logger.logError(
|
||||||
@@ -355,15 +363,22 @@ export class OneBotMsgApi {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
multiForwardMsgElement: async (_, msg, _wrapper, context) => {
|
multiForwardMsgElement: async (element, msg, _wrapper, context) => {
|
||||||
const parentMsgPeer = msg.parentMsgPeer ?? {
|
const parentMsgPeer = msg.parentMsgPeer ?? {
|
||||||
chatType: msg.chatType,
|
chatType: msg.chatType,
|
||||||
guildId: '',
|
guildId: '',
|
||||||
peerUid: msg.peerUid,
|
peerUid: msg.peerUid,
|
||||||
};
|
};
|
||||||
const multiMsgs = await this.getMultiMessages(msg, parentMsgPeer);
|
let multiMsgs = await this.getMultiMessages(msg, parentMsgPeer);
|
||||||
// 拉取失败则跳过
|
// 拉取失败则跳过
|
||||||
if (!multiMsgs) return null;
|
if (!multiMsgs || multiMsgs.length === 0) {
|
||||||
|
try {
|
||||||
|
multiMsgs = await this.core.apis.PacketApi.pkt.operation.FetchForwardMsg(element.resId);
|
||||||
|
} catch (e) {
|
||||||
|
this.core.context.logger.logError('Protocol FetchForwardMsg fallback failed!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const forward: OB11MessageForward = {
|
const forward: OB11MessageForward = {
|
||||||
type: OB11MessageDataType.forward,
|
type: OB11MessageDataType.forward,
|
||||||
data: {id: msg.msgId}
|
data: {id: msg.msgId}
|
||||||
@@ -692,7 +707,10 @@ export class OneBotMsgApi {
|
|||||||
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
|
||||||
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
|
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
|
||||||
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||||
if (PokeEvent) { return PokeEvent; };
|
if (PokeEvent) {
|
||||||
|
return PokeEvent;
|
||||||
|
}
|
||||||
|
;
|
||||||
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
|
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
|
||||||
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
|
||||||
}
|
}
|
||||||
@@ -998,6 +1016,7 @@ export class OneBotMsgApi {
|
|||||||
return {path, fileName: inputdata.name ?? fileName};
|
return {path, fileName: inputdata.name ?? fileName};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleObfuckName(name: string) {
|
async handleObfuckName(name: string) {
|
||||||
const contextMsgFile = FileNapCatOneBotUUID.decode(name);
|
const contextMsgFile = FileNapCatOneBotUUID.decode(name);
|
||||||
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
|
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
|
||||||
@@ -1021,6 +1040,7 @@ export class OneBotMsgApi {
|
|||||||
}
|
}
|
||||||
throw new Error('文件名解析失败');
|
throw new Error('文件名解析失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
|
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 130:
|
case 130:
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ConfigBase } from '@/common/config-base';
|
import { ConfigBase } from '@/common/config-base';
|
||||||
import { NapCatCore } from '@/core';
|
import type { NapCatCore } from '@/core';
|
||||||
import { OneBotConfig } from './config';
|
import { OneBotConfig } from './config';
|
||||||
import { AnySchema } from 'ajv';
|
import { AnySchema } from 'ajv';
|
||||||
|
|
||||||
|
@@ -27,8 +27,3 @@ export function require_dlopen(modulename: string) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return module.exports as any;
|
return module.exports as any;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Expose the native API when not Windows, note that this is not public API and
|
|
||||||
* could be removed at any time.
|
|
||||||
*/
|
|
||||||
export const native = (process.platform !== 'win32' ? require_dlopen('./pty/' + process.platform + '.' + process.arch + '/pty.node') : null);
|
|
||||||
|
@@ -13,12 +13,13 @@ import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from '@homebridge/node-
|
|||||||
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/types';
|
||||||
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
|
import { assign } from '@homebridge/node-pty-prebuilt-multiarch/src/utils';
|
||||||
import { pty_loader } from './prebuild-loader';
|
import { pty_loader } from './prebuild-loader';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
export const pty = pty_loader();
|
export const pty = pty_loader();
|
||||||
|
|
||||||
let helperPath: string;
|
let helperPath: string;
|
||||||
helperPath = '../build/Release/spawn-helper';
|
helperPath = '../build/Release/spawn-helper';
|
||||||
|
const import__dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
helperPath = path.resolve(__dirname, helperPath);
|
helperPath = path.resolve(import__dirname, helperPath);
|
||||||
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||||
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||||
|
|
||||||
|
@@ -14,6 +14,8 @@ import { ArgvOrCommandLine } from '@homebridge/node-pty-prebuilt-multiarch/src/t
|
|||||||
import { fork } from 'child_process';
|
import { fork } from 'child_process';
|
||||||
import { ConoutConnection } from './windowsConoutConnection';
|
import { ConoutConnection } from './windowsConoutConnection';
|
||||||
import { require_dlopen } from '.';
|
import { require_dlopen } from '.';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
let conptyNative: IConptyNative;
|
let conptyNative: IConptyNative;
|
||||||
let winptyNative: IWinptyNative;
|
let winptyNative: IWinptyNative;
|
||||||
@@ -176,8 +178,9 @@ export class WindowsPtyAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getConsoleProcessList(): Promise<number[]> {
|
private _getConsoleProcessList(): Promise<number[]> {
|
||||||
|
const import__dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
return new Promise<number[]>(resolve => {
|
return new Promise<number[]>(resolve => {
|
||||||
const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]);
|
const agent = fork(path.join(import__dirname, 'conpty_console_list_agent'), [this._innerPid.toString()]);
|
||||||
agent.on('message', message => {
|
agent.on('message', message => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
// @ts-expect-error no need to check if it is null
|
// @ts-expect-error no need to check if it is null
|
||||||
|
@@ -314,7 +314,15 @@ export async function NCoreInitShell() {
|
|||||||
await initializeSession(session, sessionConfig);
|
await initializeSession(session, sessionConfig);
|
||||||
|
|
||||||
const accountDataPath = path.resolve(dataPath, './NapCat/data');
|
const accountDataPath = path.resolve(dataPath, './NapCat/data');
|
||||||
fs.mkdirSync(dataPath, { recursive: true });
|
//判断dataPath是否为根目录 或者 D:/ 之类的盘目录
|
||||||
|
if (dataPath !== '/' && /^[a-zA-Z]:\\$/.test(dataPath) === false) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(accountDataPath, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.logError('创建accountDataPath失败', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
|
logger.logDebug('本账号数据/缓存目录:', accountDataPath);
|
||||||
|
|
||||||
await new NapCatShell(
|
await new NapCatShell(
|
||||||
|
@@ -10,9 +10,10 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
|
|||||||
import { ALLRouter } from '@webapi/router';
|
import { ALLRouter } from '@webapi/router';
|
||||||
import { cors } from '@webapi/middleware/cors';
|
import { cors } from '@webapi/middleware/cors';
|
||||||
import { createUrl } from '@webapi/utils/url';
|
import { createUrl } from '@webapi/utils/url';
|
||||||
import { sendSuccess } from '@webapi/utils/response';
|
import { sendError, sendSuccess } from '@webapi/utils/response';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { terminalManager } from '@webapi/terminal/terminal_manager';
|
import { terminalManager } from '@webapi/terminal/terminal_manager';
|
||||||
|
import multer from 'multer'; // 新增:引入multer用于错误捕获
|
||||||
|
|
||||||
// 实例化Express
|
// 实例化Express
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -42,10 +43,22 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
// CORS中间件
|
// CORS中间件
|
||||||
// TODO:
|
// TODO:
|
||||||
app.use(cors);
|
app.use(cors);
|
||||||
|
|
||||||
|
// 如果是webui字体文件,挂载字体文件
|
||||||
|
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
|
||||||
|
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist();
|
||||||
|
console.log(isFontExist, 'isFontExist');
|
||||||
|
if (isFontExist) {
|
||||||
|
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath());
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ------------中间件结束------------
|
// ------------中间件结束------------
|
||||||
|
|
||||||
// ------------挂载路由------------
|
// ------------挂载路由------------
|
||||||
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
// 挂载静态路由(前端),路径为 /webui
|
||||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
app.use('/webui', express.static(pathWrapper.staticPath));
|
||||||
// 初始化WebSocket服务器
|
// 初始化WebSocket服务器
|
||||||
server.on('upgrade', (request, socket, head) => {
|
server.on('upgrade', (request, socket, head) => {
|
||||||
@@ -64,7 +77,19 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
app.all('/', (_req, res) => {
|
app.all('/', (_req, res) => {
|
||||||
sendSuccess(res, null, 'NapCat WebAPI is now running!');
|
sendSuccess(res, null, 'NapCat WebAPI is now running!');
|
||||||
});
|
});
|
||||||
// ------------路由挂载结束------------
|
|
||||||
|
// 错误处理中间件,捕获multer的错误
|
||||||
|
app.use((err: Error, _: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
if (err instanceof multer.MulterError) {
|
||||||
|
return sendError(res, err.message, true);
|
||||||
|
}
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全局错误处理中间件(非multer错误)
|
||||||
|
app.use((_: Error, __: express.Request, res: express.Response, ___: express.NextFunction) => {
|
||||||
|
sendError(res, 'An unknown error occurred.', true);
|
||||||
|
});
|
||||||
|
|
||||||
// ------------启动服务------------
|
// ------------启动服务------------
|
||||||
server.listen(config.port, config.host, async () => {
|
server.listen(config.port, config.host, async () => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { RequestHandler, Request } from 'express';
|
import type { RequestHandler } from 'express';
|
||||||
import { sendError, sendSuccess } from '../utils/response';
|
import { sendError, sendSuccess } from '../utils/response';
|
||||||
import fsProm from 'fs/promises';
|
import fsProm from 'fs/promises';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -7,7 +7,9 @@ import os from 'os';
|
|||||||
import compressing from 'compressing';
|
import compressing from 'compressing';
|
||||||
import { PassThrough } from 'stream';
|
import { PassThrough } from 'stream';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { randomUUID } from 'crypto';
|
import { WebUiConfigWrapper } from '../helper/config';
|
||||||
|
import webUIFontUploader from '../uploader/webui_font';
|
||||||
|
import diskUploader from '../uploader/disk';
|
||||||
|
|
||||||
const isWindows = os.platform() === 'win32';
|
const isWindows = os.platform() === 'win32';
|
||||||
|
|
||||||
@@ -327,74 +329,71 @@ export const BatchDownloadHandler: RequestHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
|
|
||||||
const decodeFileName = (fileName: string): string => {
|
|
||||||
try {
|
|
||||||
return Buffer.from(fileName, 'binary').toString('utf8');
|
|
||||||
} catch {
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 修改上传处理方法
|
// 修改上传处理方法
|
||||||
export const UploadHandler: RequestHandler = (req, res) => {
|
export const UploadHandler: RequestHandler = async (req, res) => {
|
||||||
const uploadPath = (req.query['path'] || '') as string;
|
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: (
|
|
||||||
_: Request,
|
|
||||||
file: Express.Multer.File,
|
|
||||||
cb: (error: Error | null, destination: string) => void
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const decodedName = decodeFileName(file.originalname);
|
await diskUploader(req, res);
|
||||||
|
return sendSuccess(res, true, '文件上传成功', true);
|
||||||
if (!uploadPath) {
|
|
||||||
return cb(new Error('上传路径不能为空'), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWindows && uploadPath === '\\') {
|
|
||||||
return cb(new Error('根目录不允许上传文件'), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理文件夹上传的情况
|
|
||||||
if (decodedName.includes('/') || decodedName.includes('\\')) {
|
|
||||||
const fullPath = path.join(uploadPath, path.dirname(decodedName));
|
|
||||||
fs.mkdirSync(fullPath, { recursive: true });
|
|
||||||
cb(null, fullPath);
|
|
||||||
} else {
|
|
||||||
cb(null, uploadPath);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cb(error as Error, '');
|
let errorMessage = '文件上传失败';
|
||||||
}
|
|
||||||
},
|
|
||||||
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
|
|
||||||
try {
|
|
||||||
const decodedName = decodeFileName(file.originalname);
|
|
||||||
const fileName = path.basename(decodedName);
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
if (error instanceof multer.MulterError) {
|
||||||
const fullPath = path.join(uploadPath, decodedName);
|
switch (error.code) {
|
||||||
if (fs.existsSync(fullPath)) {
|
case 'LIMIT_FILE_SIZE':
|
||||||
const ext = path.extname(fileName);
|
errorMessage = '文件大小超过限制(40MB)';
|
||||||
const name = path.basename(fileName, ext);
|
break;
|
||||||
cb(null, `${name}-${randomUUID()}${ext}`);
|
case 'LIMIT_UNEXPECTED_FILE':
|
||||||
} else {
|
errorMessage = '无效的文件上传字段';
|
||||||
cb(null, fileName);
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = `上传错误: ${error.message}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else if (error instanceof Error) {
|
||||||
cb(error as Error, '');
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
},
|
return sendError(res, errorMessage, true);
|
||||||
});
|
}
|
||||||
|
};
|
||||||
const upload = multer({ storage }).array('files');
|
|
||||||
|
// 上传WebUI字体文件处理方法
|
||||||
upload(req, res, (err: any) => {
|
export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
|
||||||
if (err) {
|
try {
|
||||||
return sendError(res, err.message || '文件上传失败');
|
await webUIFontUploader(req, res);
|
||||||
|
return sendSuccess(res, true, '字体文件上传成功', true);
|
||||||
|
} catch (error) {
|
||||||
|
let errorMessage = '字体文件上传失败';
|
||||||
|
|
||||||
|
if (error instanceof multer.MulterError) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 'LIMIT_FILE_SIZE':
|
||||||
|
errorMessage = '字体文件大小超过限制(40MB)';
|
||||||
|
break;
|
||||||
|
case 'LIMIT_UNEXPECTED_FILE':
|
||||||
|
errorMessage = '无效的文件上传字段';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = `上传错误: ${error.message}`;
|
||||||
|
}
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
return sendError(res, errorMessage, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除WebUI字体文件处理方法
|
||||||
|
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const fontPath = WebUiConfigWrapper.GetWebUIFontPath();
|
||||||
|
const exists = await WebUiConfigWrapper.CheckWebUIFontExist();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return sendSuccess(res, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsProm.unlink(fontPath);
|
||||||
|
return sendSuccess(res, true);
|
||||||
|
} catch (error) {
|
||||||
|
return sendError(res, '删除字体文件失败');
|
||||||
}
|
}
|
||||||
return sendSuccess(res, true);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
import { loadConfig, OneBotConfig } from '@/onebot/config/config';
|
||||||
import { OneBotConfig } from '@/onebot/config/config';
|
|
||||||
|
|
||||||
import { webUiPathWrapper } from '@/webui';
|
import { webUiPathWrapper } from '@/webui';
|
||||||
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
import { WebUiDataRuntime } from '@webapi/helper/Data';
|
||||||
import { sendError, sendSuccess } from '@webapi/utils/response';
|
import { sendError, sendSuccess } from '@webapi/utils/response';
|
||||||
import { isEmpty } from '@webapi/utils/check';
|
import { isEmpty } from '@webapi/utils/check';
|
||||||
|
import json5 from 'json5';
|
||||||
|
|
||||||
// 获取OneBot11配置
|
// 获取OneBot11配置
|
||||||
export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
||||||
@@ -19,16 +18,16 @@ export const OB11GetConfigHandler: RequestHandler = (_, res) => {
|
|||||||
}
|
}
|
||||||
// 获取登录的QQ号
|
// 获取登录的QQ号
|
||||||
const uin = WebUiDataRuntime.getQQLoginUin();
|
const uin = WebUiDataRuntime.getQQLoginUin();
|
||||||
// 读取配置文件
|
// 读取配置文件路径
|
||||||
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
|
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
|
||||||
// 尝试解析配置文件
|
// 尝试解析配置文件
|
||||||
try {
|
try {
|
||||||
// 读取配置文件
|
// 读取配置文件内容
|
||||||
const data = JSON.parse(
|
const configFileContent = existsSync(configFilePath)
|
||||||
existsSync(configFilePath)
|
|
||||||
? readFileSync(configFilePath).toString()
|
? readFileSync(configFilePath).toString()
|
||||||
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString()
|
: readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString();
|
||||||
) as OneBotConfig;
|
// 解析配置文件并加载配置
|
||||||
|
const data = loadConfig(json5.parse(configFileContent)) as OneBotConfig;
|
||||||
// 返回配置文件
|
// 返回配置文件
|
||||||
return sendSuccess(res, data);
|
return sendSuccess(res, data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -50,7 +49,10 @@ export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
// 写入配置
|
// 写入配置
|
||||||
try {
|
try {
|
||||||
await WebUiDataRuntime.setOB11Config(JSON.parse(req.body.config));
|
// 解析并加载配置
|
||||||
|
const config = loadConfig(json5.parse(req.body.config)) as OneBotConfig;
|
||||||
|
// 写入配置
|
||||||
|
await WebUiDataRuntime.setOB11Config(config);
|
||||||
return sendSuccess(res, null);
|
return sendSuccess(res, null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return sendError(res, 'Error: ' + e);
|
return sendError(res, 'Error: ' + e);
|
||||||
|
@@ -203,4 +203,32 @@ export class WebUiConfigWrapper {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字体文件夹内的字体列表
|
||||||
|
public static async GetFontList(): Promise<string[]> {
|
||||||
|
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
|
||||||
|
if (
|
||||||
|
await fs
|
||||||
|
.access(fontsPath, constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false)
|
||||||
|
) {
|
||||||
|
return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf'));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断字体是否存在(webui.woff)
|
||||||
|
public static async CheckWebUIFontExist(): Promise<boolean> {
|
||||||
|
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
|
||||||
|
return await fs
|
||||||
|
.access(resolve(fontsPath, './webui.woff'), constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取webui字体文件路径
|
||||||
|
public static GetWebUIFontPath(): string {
|
||||||
|
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,9 @@ import {
|
|||||||
BatchMoveHandler,
|
BatchMoveHandler,
|
||||||
DownloadHandler,
|
DownloadHandler,
|
||||||
BatchDownloadHandler, // 新增下载处理方法
|
BatchDownloadHandler, // 新增下载处理方法
|
||||||
UploadHandler, // 添加上传处理器
|
UploadHandler,
|
||||||
|
UploadWebUIFontHandler,
|
||||||
|
DeleteWebUIFontHandler, // 添加上传处理器
|
||||||
} from '../api/File';
|
} from '../api/File';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -37,5 +39,8 @@ router.post('/move', MoveHandler);
|
|||||||
router.post('/batchMove', BatchMoveHandler);
|
router.post('/batchMove', BatchMoveHandler);
|
||||||
router.post('/download', DownloadHandler);
|
router.post('/download', DownloadHandler);
|
||||||
router.post('/batchDownload', BatchDownloadHandler);
|
router.post('/batchDownload', BatchDownloadHandler);
|
||||||
router.post('/upload', UploadHandler); // 添加上传处理路由
|
router.post('/upload', UploadHandler);
|
||||||
|
|
||||||
|
router.post('/font/upload/webui', UploadWebUIFontHandler);
|
||||||
|
router.post('/font/delete/webui', DeleteWebUIFontHandler);
|
||||||
export { router as FileRouter };
|
export { router as FileRouter };
|
||||||
|
85
src/webui/src/uploader/disk.ts
Normal file
85
src/webui/src/uploader/disk.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import multer from 'multer';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
|
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
|
||||||
|
const decodeFileName = (fileName: string): string => {
|
||||||
|
try {
|
||||||
|
return Buffer.from(fileName, 'binary').toString('utf8');
|
||||||
|
} catch {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDiskStorage = (uploadPath: string) => {
|
||||||
|
return multer.diskStorage({
|
||||||
|
destination: (
|
||||||
|
_: Request,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
cb: (error: Error | null, destination: string) => void
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const decodedName = decodeFileName(file.originalname);
|
||||||
|
|
||||||
|
if (!uploadPath) {
|
||||||
|
return cb(new Error('上传路径不能为空'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWindows && uploadPath === '\\') {
|
||||||
|
return cb(new Error('根目录不允许上传文件'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件夹上传的情况
|
||||||
|
if (decodedName.includes('/') || decodedName.includes('\\')) {
|
||||||
|
const fullPath = path.join(uploadPath, path.dirname(decodedName));
|
||||||
|
fs.mkdirSync(fullPath, { recursive: true });
|
||||||
|
cb(null, fullPath);
|
||||||
|
} else {
|
||||||
|
cb(null, uploadPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
cb(error as Error, '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
|
||||||
|
try {
|
||||||
|
const decodedName = decodeFileName(file.originalname);
|
||||||
|
const fileName = path.basename(decodedName);
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
const fullPath = path.join(uploadPath, decodedName);
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
const ext = path.extname(fileName);
|
||||||
|
const name = path.basename(fileName, ext);
|
||||||
|
cb(null, `${name}-${randomUUID()}${ext}`);
|
||||||
|
} else {
|
||||||
|
cb(null, fileName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
cb(error as Error, '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDiskUpload = (uploadPath: string) => {
|
||||||
|
const upload = multer({ storage: createDiskStorage(uploadPath) }).array('files');
|
||||||
|
return upload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const diskUploader = (req: Request, res: Response) => {
|
||||||
|
const uploadPath = (req.query['path'] || '') as string;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
createDiskUpload(uploadPath)(req, res, (error) => {
|
||||||
|
if (error) {
|
||||||
|
// 错误处理
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
return resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export default diskUploader;
|
52
src/webui/src/uploader/webui_font.ts
Normal file
52
src/webui/src/uploader/webui_font.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import multer from 'multer';
|
||||||
|
import { WebUiConfigWrapper } from '../helper/config';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
|
export const webUIFontStorage = multer.diskStorage({
|
||||||
|
destination: (_, __, cb) => {
|
||||||
|
try {
|
||||||
|
const fontsPath = path.dirname(WebUiConfigWrapper.GetWebUIFontPath());
|
||||||
|
// 确保字体目录存在
|
||||||
|
fs.mkdirSync(fontsPath, { recursive: true });
|
||||||
|
cb(null, fontsPath);
|
||||||
|
} catch (error) {
|
||||||
|
// 确保错误信息被正确传递
|
||||||
|
cb(new Error(`创建字体目录失败:${(error as Error).message}`), '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filename: (_, __, cb) => {
|
||||||
|
// 统一保存为webui.woff
|
||||||
|
cb(null, 'webui.woff');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const webUIFontUpload = multer({
|
||||||
|
storage: webUIFontStorage,
|
||||||
|
fileFilter: (_, file, cb) => {
|
||||||
|
// 再次验证文件类型
|
||||||
|
if (!file.originalname.toLowerCase().endsWith('.woff')) {
|
||||||
|
cb(new Error('只支持WOFF格式的字体文件'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb(null, true);
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
fileSize: 40 * 1024 * 1024, // 限制40MB
|
||||||
|
},
|
||||||
|
}).single('file');
|
||||||
|
|
||||||
|
const webUIFontUploader = (req: Request, res: Response) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
webUIFontUpload(req, res, (error) => {
|
||||||
|
if (error) {
|
||||||
|
// 错误处理
|
||||||
|
// sendError(res, error.message, true);
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
return resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export default webUIFontUploader;
|
@@ -2,25 +2,46 @@ import type { Response } from 'express';
|
|||||||
|
|
||||||
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
|
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
|
||||||
|
|
||||||
export const sendResponse = <T>(res: Response, data?: T, code: ResponseCode = 0, message = 'success') => {
|
export const sendResponse = <T>(
|
||||||
res.status(HttpStatusCode.OK).json({
|
res: Response,
|
||||||
|
data?: T,
|
||||||
|
code: ResponseCode = 0,
|
||||||
|
message = 'success',
|
||||||
|
useSend: boolean = false
|
||||||
|
) => {
|
||||||
|
const result = {
|
||||||
code,
|
code,
|
||||||
message,
|
message,
|
||||||
data,
|
data,
|
||||||
});
|
};
|
||||||
|
if (useSend) {
|
||||||
|
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(HttpStatusCode.OK).json(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendError = (res: Response, message = 'error') => {
|
export const sendError = (res: Response, message = 'error', useSend: boolean = false) => {
|
||||||
res.status(HttpStatusCode.OK).json({
|
const result = {
|
||||||
code: ResponseCode.Error,
|
code: ResponseCode.Error,
|
||||||
message,
|
message,
|
||||||
});
|
};
|
||||||
|
if (useSend) {
|
||||||
|
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(HttpStatusCode.OK).json(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendSuccess = <T>(res: Response, data?: T, message = 'success') => {
|
export const sendSuccess = <T>(res: Response, data?: T, message = 'success', useSend: boolean = false) => {
|
||||||
res.status(HttpStatusCode.OK).json({
|
const result = {
|
||||||
code: ResponseCode.Success,
|
code: ResponseCode.Success,
|
||||||
data,
|
data,
|
||||||
message,
|
message,
|
||||||
});
|
};
|
||||||
|
if (useSend) {
|
||||||
|
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(HttpStatusCode.OK).json(result);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user