Compare commits

..

1 Commits

Author SHA1 Message Date
手瓜一十雪
3a3eaeec7c Add UploadForwardMsgV2 support for multi-message forwarding
Introduces UploadForwardMsgV2 transformer and integrates it into the message sending flow to support forwarding multiple messages with custom action commands. Updates related interfaces and logic to handle UUIDs and nested forwarded messages, improving flexibility and extensibility for message forwarding operations.
2026-01-30 18:47:45 +08:00
7 changed files with 159 additions and 73 deletions

View File

@@ -2,14 +2,14 @@ import * as crypto from 'node:crypto';
import { PacketMsg } from '@/napcat-core/packet/message/message'; import { PacketMsg } from '@/napcat-core/packet/message/message';
interface ForwardMsgJson { interface ForwardMsgJson {
app: string app: string;
config: ForwardMsgJsonConfig, config: ForwardMsgJsonConfig,
desc: string, desc: string,
extra: ForwardMsgJsonExtra, extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta, meta: ForwardMsgJsonMeta,
prompt: string, prompt: string,
ver: string, ver: string,
view: string view: string;
} }
interface ForwardMsgJsonConfig { interface ForwardMsgJsonConfig {
@@ -17,7 +17,7 @@ interface ForwardMsgJsonConfig {
forward: number, forward: number,
round: number, round: number,
type: string, type: string,
width: number width: number;
} }
interface ForwardMsgJsonExtra { interface ForwardMsgJsonExtra {
@@ -26,17 +26,17 @@ interface ForwardMsgJsonExtra {
} }
interface ForwardMsgJsonMeta { interface ForwardMsgJsonMeta {
detail: ForwardMsgJsonMetaDetail detail: ForwardMsgJsonMetaDetail;
} }
interface ForwardMsgJsonMetaDetail { interface ForwardMsgJsonMetaDetail {
news: { news: {
text: string text: string;
}[], }[],
resid: string, resid: string,
source: string, source: string,
summary: string, summary: string,
uniseq: string uniseq: string;
} }
interface ForwardAdaptMsg { interface ForwardAdaptMsg {
@@ -50,8 +50,8 @@ interface ForwardAdaptMsgElement {
} }
export class ForwardMsgBuilder { export class ForwardMsgBuilder {
private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson { private static build (resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
const id = crypto.randomUUID(); const id = uuid ?? crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg); const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) { if (!source) {
source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录'); source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
@@ -104,13 +104,19 @@ export class ForwardMsgBuilder {
return this.build(resId, []); return this.build(resId, []);
} }
static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string): ForwardMsgJson { static fromPacketMsg (resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail['news'], summary?: string, prompt?: string, uuid?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({ return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName, senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined, isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({ msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]', preview: m.valid ? m.toPreview() : '[该消息类型暂不支持查看]',
})), })),
})), source, news, summary, prompt); })),
source,
news,
summary,
prompt,
uuid,
);
} }
} }

View File

@@ -18,6 +18,7 @@ import { OidbPacket } from '@/napcat-core/packet/transformer/base';
import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult'; import { ImageOcrResult } from '@/napcat-core/packet/entities/ocrResult';
import { gunzipSync } from 'zlib'; import { gunzipSync } from 'zlib';
import { PacketMsgConverter } from '@/napcat-core/packet/message/converter'; import { PacketMsgConverter } from '@/napcat-core/packet/message/converter';
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
export class PacketOperationContext { export class PacketOperationContext {
private readonly context: PacketContext; private readonly context: PacketContext;
@@ -224,7 +225,15 @@ export class PacketOperationContext {
const res = trans.UploadForwardMsg.parse(resp); const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId; return res.result.resId;
} }
async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) {
//await this.SendPreprocess(msg, groupUin);
// 遍历上传资源
await Promise.allSettled(msg.map(async (item) => { return await this.SendPreprocess(item.actionMsg, groupUin); }));
const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async MoveGroupFile ( async MoveGroupFile (
groupUin: number, groupUin: number,
fileUUID: string, fileUUID: string,

View File

@@ -0,0 +1,51 @@
import zlib from 'node:zlib';
import * as proto from '@/napcat-core/packet/transformer/proto';
import { NapProtoMsg } from 'napcat-protobuf';
import { OidbPacket, PacketBufBuilder, PacketTransformer } from '@/napcat-core/packet/transformer/base';
import { PacketMsg } from '@/napcat-core/packet/message/message';
export interface UploadForwardMsgParams {
actionCommand: string;
actionMsg: PacketMsg[];
}
class UploadForwardMsgV2 extends PacketTransformer<typeof proto.SendLongMsgResp> {
build (selfUid: string, msg: UploadForwardMsgParams[], groupUin: number = 0): OidbPacket {
const reqdata = msg.map((item) => ({
actionCommand: item.actionCommand,
actionData: {
msgBody: this.msgBuilder.buildFakeMsg(selfUid, item.actionMsg),
}
}));
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
{
action: reqdata,
}
);
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
const req = new NapProtoMsg(proto.SendLongMsgReq).encode(
{
info: {
type: groupUin === 0 ? 1 : 3,
uid: {
uid: groupUin === 0 ? selfUid : groupUin.toString(),
},
groupUin,
payload,
},
settings: {
field1: 4, field2: 1, field3: 7, field4: 0,
},
}
);
return {
cmd: 'trpc.group.long_msg_interface.MsgService.SsoSendLongMsg',
data: PacketBufBuilder(req),
};
}
parse (data: Buffer) {
return new NapProtoMsg(proto.SendLongMsgResp).decode(data);
}
}
export default new UploadForwardMsgV2();

View File

@@ -2,3 +2,4 @@ export { default as UploadForwardMsg } from './UploadForwardMsg';
export { default as FetchGroupMessage } from './FetchGroupMessage'; export { default as FetchGroupMessage } from './FetchGroupMessage';
export { default as FetchC2CMessage } from './FetchC2CMessage'; export { default as FetchC2CMessage } from './FetchC2CMessage';
export { default as DownloadForwardMsg } from './DownloadForwardMsg'; export { default as DownloadForwardMsg } from './DownloadForwardMsg';
export { default as UploadForwardMsgV2 } from './UploadForwardMsgV2';

View File

@@ -17,6 +17,7 @@ import { rawMsgWithSendMsg } from 'napcat-core/packet/message/converter';
import { Static, Type } from '@sinclair/typebox'; import { Static, Type } from '@sinclair/typebox';
import { MsgActionsExamples } from '@/napcat-onebot/action/msg/examples'; import { MsgActionsExamples } from '@/napcat-onebot/action/msg/examples';
import { OB11MessageMixTypeSchema } from '@/napcat-onebot/types/message'; import { OB11MessageMixTypeSchema } from '@/napcat-onebot/types/message';
import { UploadForwardMsgParams } from '@/napcat-core/packet/transformer/message/UploadForwardMsgV2';
export const SendMsgPayloadSchema = Type.Object({ export const SendMsgPayloadSchema = Type.Object({
message_type: Type.Optional(Type.Union([Type.Literal('private'), Type.Literal('group')], { description: '消息类型 (private/group)' })), message_type: Type.Optional(Type.Union([Type.Literal('private'), Type.Literal('group')], { description: '消息类型 (private/group)' })),
@@ -211,10 +212,13 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
}, dp: number = 0): Promise<{ }, dp: number = 0): Promise<{
finallySendElements: SendArkElement, finallySendElements: SendArkElement,
res_id?: string, res_id?: string,
uuid?: string,
packetMsg: PacketMsg[],
deleteAfterSentFiles: string[], deleteAfterSentFiles: string[],
} | null> { } | null> {
const packetMsg: PacketMsg[] = []; const packetMsg: PacketMsg[] = [];
const delFiles: string[] = []; const delFiles: string[] = [];
const innerMsg: Array<{ uuid: string, packetMsg: PacketMsg[]; }> = new Array();
for (const node of messageNodes) { for (const node of messageNodes) {
if (dp >= 3) { if (dp >= 3) {
this.core.context.logger.logWarn('转发消息深度超过3层将停止解析'); this.core.context.logger.logWarn('转发消息深度超过3层将停止解析');
@@ -232,6 +236,10 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
}, dp + 1); }, dp + 1);
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : []; sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || [])); delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || []));
if (uploadReturnData?.uuid) {
innerMsg.push({ uuid: uploadReturnData.uuid, packetMsg: uploadReturnData.packetMsg });
}
} else { } else {
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer); const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
sendElements = sendElementsCreateReturn.sendElements; sendElements = sendElementsCreateReturn.sendElements;
@@ -273,8 +281,19 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
this.core.context.logger.logWarn('handleForwardedNodesPacket 元素为空!'); this.core.context.logger.logWarn('handleForwardedNodesPacket 元素为空!');
return null; return null;
} }
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); const uploadMsgData: UploadForwardMsgParams[] = [{
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt); actionCommand: 'MultiMsg',
actionMsg: packetMsg,
}];
innerMsg.forEach(({ uuid, packetMsg: msg }) => {
uploadMsgData.push({
actionCommand: uuid,
actionMsg: msg,
});
});
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsgV2(uploadMsgData, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
const uuid = crypto.randomUUID();
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt, uuid);
return { return {
deleteAfterSentFiles: delFiles, deleteAfterSentFiles: delFiles,
finallySendElements: { finallySendElements: {
@@ -285,6 +304,8 @@ export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
}, },
} as SendArkElement, } as SendArkElement,
res_id: resid, res_id: resid,
uuid: uuid,
packetMsg: packetMsg,
}; };
} }

View File

@@ -180,7 +180,7 @@ const GenericForm = <T extends keyof NetworkConfigType> ({
export default GenericForm; export default GenericForm;
export function random_token (length: number) { export function random_token (length: number) {
const chars = const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~'; 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%^&*()-_=+[]{}|;:,.<>?';
let result = ''; let result = '';
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length)); result += chars.charAt(Math.floor(Math.random() * chars.length));

View File

@@ -58,7 +58,7 @@ export default function ExtensionPage () {
pluginName: page.pluginName, pluginName: page.pluginName,
path: page.path, path: page.path,
icon: page.icon, icon: page.icon,
description: page.description, description: page.description
})); }));
}, [extensionPages]); }, [extensionPages]);
@@ -69,7 +69,7 @@ export default function ExtensionPage () {
const path = pathParts.join(':').replace(/^\//, ''); const path = pathParts.join(':').replace(/^\//, '');
// 获取认证 token // 获取认证 token
const token = localStorage.getItem('token') || ''; const token = localStorage.getItem('token') || '';
return `/api/Plugin/page/${pluginId}/${path}?webui_token=${token}`; return `/api/Plugin/page/${pluginId}/${path}?webui_token=${encodeURIComponent(token)}`;
}, [selectedTab]); }, [selectedTab]);
useEffect(() => { useEffect(() => {
@@ -89,68 +89,66 @@ export default function ExtensionPage () {
return ( return (
<> <>
<title> - NapCat WebUI</title> <title> - NapCat WebUI</title>
<div className='p-2 md:p-4 relative h-full flex flex-col'> <div className="p-2 md:p-4 relative h-full flex flex-col">
<PageLoading loading={loading} /> <PageLoading loading={loading} />
<div className='flex mb-4 items-center gap-4'> <div className="flex mb-4 items-center gap-4">
<div className='flex items-center gap-2 text-default-600'> <div className="flex items-center gap-2 text-default-600">
<MdExtension size={24} /> <MdExtension size={24} />
<span className='text-lg font-medium'></span> <span className="text-lg font-medium"></span>
</div> </div>
<Button <Button
isIconOnly isIconOnly
className='bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md' className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
radius='full' radius="full"
onPress={refresh} onPress={refresh}
> >
<IoMdRefresh size={24} /> <IoMdRefresh size={24} />
</Button> </Button>
</div> </div>
{extensionPages.length === 0 && !loading {extensionPages.length === 0 && !loading ? (
? ( <div className="flex-1 flex flex-col items-center justify-center text-default-400">
<div className='flex-1 flex flex-col items-center justify-center text-default-400'> <MdExtension size={64} className="mb-4 opacity-50" />
<MdExtension size={64} className='mb-4 opacity-50' /> <p className="text-lg"></p>
<p className='text-lg'></p> <p className="text-sm mt-2"> WebUI </p>
<p className='text-sm mt-2'> WebUI </p>
</div> </div>
) ) : (
: ( <div className="flex-1 flex flex-col min-h-0">
<div className='flex-1 flex flex-col min-h-0'>
<Tabs <Tabs
aria-label='Extension Pages' aria-label="Extension Pages"
className='max-w-full' className="max-w-full"
selectedKey={selectedTab} selectedKey={selectedTab}
onSelectionChange={(key) => setSelectedTab(key as string)} onSelectionChange={(key) => setSelectedTab(key as string)}
classNames={{ classNames={{
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-wrap', tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md flex-wrap',
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm', cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
panel: 'flex-1 min-h-0 p-0', panel: 'flex-1 min-h-0 p-0'
}} }}
> >
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab <Tab
key={tab.key} key={tab.key}
title={ title={
<div className='flex items-center gap-2'> <div className="flex items-center gap-2">
{tab.icon && <span>{tab.icon}</span>} {tab.icon && <span>{tab.icon}</span>}
<span>{tab.title}</span> <span>{tab.title}</span>
<span className='text-xs text-default-400'>({tab.pluginName})</span> <span className="text-xs text-default-400">({tab.pluginName})</span>
</div> </div>
} }
> >
<div className='relative w-full h-[calc(100vh-220px)] bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden'> <div className="relative w-full h-[calc(100vh-220px)] bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-lg overflow-hidden">
{iframeLoading && ( {iframeLoading && (
<div className='absolute inset-0 flex items-center justify-center bg-default-100/50 z-10'> <div className="absolute inset-0 flex items-center justify-center bg-default-100/50 z-10">
<Spinner size='lg' /> <Spinner size="lg" />
</div> </div>
)} )}
<iframe <iframe
src={currentPageUrl} src={currentPageUrl}
className='w-full h-full border-0' className="w-full h-full border-0"
onLoad={handleIframeLoad} onLoad={handleIframeLoad}
title={tab.title} title={tab.title}
sandbox='allow-scripts allow-same-origin allow-forms allow-popups' sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
/> />
</div> </div>
</Tab> </Tab>