From ec56e32be18c5dc0c1745f97706bcf81dfc6559f Mon Sep 17 00:00:00 2001
From: Simplxs <simplxsa@gmail.com>
Date: Wed, 28 Feb 2024 12:55:27 +0800
Subject: [PATCH] refactor send_forward_msg

---
 .../src/main/java/protobuf/message/Ptt.kt     |  18 +-
 .../main/java/protobuf/message/RichText.kt    |   2 +-
 .../fuqiuluo/qqinterface/servlet/MsgSvc.kt    | 247 ++++++++++++++---
 .../fuqiuluo/qqinterface/servlet/PacketSvc.kt |  33 +--
 .../qqinterface/servlet/msg/Converter.kt      |  33 ++-
 .../qqinterface/servlet/msg/MessageSegment.kt |   6 +-
 .../servlet/msg/converter/ElemConverter.kt    |  13 +-
 .../msg/converter/NtMsgElementConverter.kt    |  20 +-
 .../servlet/msg/maker/ElemMaker.kt            | 251 ++++++++++++------
 .../servlet/msg/maker/NtMsgElementMaker.kt    |  78 +++++-
 .../servlet/transfile/RichProtoSvc.kt         |  23 +-
 .../fuqiuluo/shamrock/helper/MessageHelper.kt |  37 +--
 .../remote/action/handlers/GetForwardMsg.kt   |  16 +-
 .../remote/action/handlers/GetHistoryMsg.kt   |   2 +
 .../shamrock/remote/action/handlers/GetMsg.kt |   1 +
 .../action/handlers/SendForwardMessage.kt     | 227 +---------------
 .../handlers/SendGroupForwardMessage.kt       |  11 +-
 .../remote/action/handlers/SendMessage.kt     | 127 ++++++---
 .../handlers/SendPrivateForwardMessage.kt     |  11 +-
 .../action/handlers/UploadMultiMessage.kt     |  97 +++++++
 .../shamrock/remote/api/MessageAction.kt      | 146 ++++++----
 .../shamrock/remote/service/data/Message.kt   |  20 +-
 .../service/listener/PrimitiveListener.kt     |   5 +-
 .../java/moe/fuqiuluo/shamrock/tools/Json.kt  |   1 +
 24 files changed, 891 insertions(+), 534 deletions(-)
 create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt

diff --git a/protobuf/src/main/java/protobuf/message/Ptt.kt b/protobuf/src/main/java/protobuf/message/Ptt.kt
index 2aedae4f..a310b0f3 100644
--- a/protobuf/src/main/java/protobuf/message/Ptt.kt
+++ b/protobuf/src/main/java/protobuf/message/Ptt.kt
@@ -7,9 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
 data class Ptt(
     @ProtoNumber(1) var fileType: UInt?=null,
     @ProtoNumber(2) var srcUin: ULong?=null,
-    @ProtoNumber(3) var fileUuid: ByteArray?=null,
+    @ProtoNumber(3) var fileUuid: String?=null,
     @ProtoNumber(4) var fileMd5: ByteArray?=null,
-    @ProtoNumber(5) var fileName: ByteArray?=null,
+    @ProtoNumber(5) var fileName: String?=null,
     @ProtoNumber(6) var fileSize: UInt?=null,
     @ProtoNumber(7) var reserve: ByteArray?=null,
     @ProtoNumber(8) var fileId: UInt?=null,
@@ -22,11 +22,19 @@ data class Ptt(
     @ProtoNumber(15) var magicPttIndex: UInt?=null,
     @ProtoNumber(16) var voiceSwitch: UInt?=null,
     @ProtoNumber(17) var pttUrl: ByteArray?=null,
-    @ProtoNumber(18) var groupFileKey: ByteArray?=null,
+    @ProtoNumber(18) var groupFileKey: String?=null,
     @ProtoNumber(19) var time: UInt?=null,
     @ProtoNumber(20) var downPara: ByteArray?=null,
     @ProtoNumber(29) var format: UInt?=null,
-    @ProtoNumber(30) var pbReserve: ByteArray?=null,
+    @ProtoNumber(30) var pbReserve: PbReserve?=null,
     @ProtoNumber(31) var rptPttUrls: List<String>? = null,
     @ProtoNumber(32) var downloadFlag: UInt?=null,
-)
\ No newline at end of file
+){
+    companion object{
+        @Serializable
+        data class PbReserve(
+            @ProtoNumber(2) var magic: Int?=null,
+            @ProtoNumber(7) var reserve: Int?=null,
+        )
+    }
+}
\ No newline at end of file
diff --git a/protobuf/src/main/java/protobuf/message/RichText.kt b/protobuf/src/main/java/protobuf/message/RichText.kt
index 3e5c1280..f0f8bfb1 100644
--- a/protobuf/src/main/java/protobuf/message/RichText.kt
+++ b/protobuf/src/main/java/protobuf/message/RichText.kt
@@ -9,7 +9,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
 @Serializable
 data class RichText(
     @ProtoNumber(1) val attr: Attr? = null,
-    @ProtoNumber(2) val elements: List<Elem>? = null,
+    @ProtoNumber(2) var elements: List<Elem>? = null,
     @ProtoNumber(3) val not_online_file: NotOnlineFile? = null,
     @ProtoNumber(4) val ptt: Ptt? = null,
     @ProtoNumber(5) val tmp_ptt: TmpPtt? = null,
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt
index 92799001..3a1ad077 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt
@@ -9,7 +9,9 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withTimeoutOrNull
 import kotlinx.serialization.json.JsonArray
-
+import kotlinx.serialization.json.JsonObject
+import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
+import moe.fuqiuluo.qqinterface.servlet.msg.toJson
 import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
 import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
 import moe.fuqiuluo.shamrock.helper.ContactHelper
@@ -25,10 +27,12 @@ import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
 import moe.fuqiuluo.shamrock.xposed.helper.msgService
 import moe.fuqiuluo.symbols.decodeProtobuf
 import protobuf.auto.toByteArray
-import protobuf.message.PushMsgBody
+import protobuf.message.*
 import protobuf.message.longmsg.*
+import java.util.*
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
+import kotlin.random.Random
 
 internal object MsgSvc : BaseSvc() {
     private suspend fun prepareTempChatFromGroup(
@@ -207,45 +211,195 @@ internal object MsgSvc : BaseSvc() {
         }
         val result =
             MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
-        if (result.isFailure) {
-            LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
-            return result
-        }
-        val sendResult = result.getOrThrow()
-        return if (sendResult.isTimeout) {
+                .getOrElse { return Result.failure(it) }
+        return if (result.isTimeout) {
             // 发送失败,可能网络问题出现红色感叹号,重试
             // 例如 rich media transfer failed
             delay(100)
-            MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, retryCnt, sendResult.msgHashId)
+            MessageHelper.resendMsg(chatType, peedId, fromId, result.qqMsgId, retryCnt, result.msgHashId)
         } else {
-            result
+            Result.success(result)
         }
     }
 
     suspend fun uploadMultiMsg(
-        uid: String,
-        groupUin: String?,
-        messages: List<PushMsgBody>,
-    ): Result<String> {
+        chatType: Int,
+        peerId: String,
+        fromId: String,
+        messages: JsonArray,
+        retryCnt: Int,
+    ): Result<MessageSegment> {
+        var i = -1
+        val desc = MutableList(messages.size) { "" }
+        val forwardMsg = mutableMapOf<String, String>()
+
+        val msgs = messages.mapNotNull { msg ->
+            kotlin.runCatching {
+                val data = msg.asJsonObject["data"].asJsonObject
+                if (data.containsKey("id")) {
+                    val record = getMsg(data["id"].asInt).getOrElse {
+                        error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
+                    }
+                    PushMsgBody(
+                        msgHead = ResponseHead(
+                            peerUid = record.senderUid,
+                            receiverUid = record.peerUid,
+                            forward = ResponseForward(
+                                friendName = record.sendNickName
+                            ),
+                            responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
+                                groupCode = record.peerUin.toULong(),
+                                memberCard = record.sendMemberName,
+                                u1 = 2
+                            ) else null
+                        ),
+                        contentHead = ContentHead(
+                            msgType = when (record.chatType) {
+                                MsgConstant.KCHATTYPEC2C -> 9
+                                MsgConstant.KCHATTYPEGROUP -> 82
+                                else -> throw UnsupportedOperationException(
+                                    "Unsupported chatType: $chatType"
+                                )
+                            },
+                            msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
+                            divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
+                            msgViaRandom = record.msgId,
+                            sequence = record.msgSeq, // idk what this is(i++)
+                            msgTime = record.msgTime,
+                            u2 = 1,
+                            u6 = 0,
+                            u7 = 0,
+                            msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
+                            forwardHead = ForwardHead(
+                                u1 = 0,
+                                u2 = 0,
+                                u3 = 0,
+                                ub641 = "",
+                                avatar = ""
+                            )
+                        ),
+                        body = MsgBody(
+                            richText = MessageHelper.messageArrayToRichText(
+                                record.chatType,
+                                record.msgId,
+                                record.peerUin.toString(),
+                                record.elements.toSegments(
+                                    record.chatType,
+                                    record.peerUin.toString(),
+                                    "0"
+                                ).onEach { segment ->
+                                    if (segment.type == "forward")
+                                        forwardMsg[segment.data["filename"] as String] =
+                                            segment.data["id"] as String
+                                }.toJson()
+                            ).getOrElse { throw Exception("消息合成失败: $it") }.let {
+                                desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first
+                                it.second
+                            }
+                        )
+                    )
+                } else if (data.containsKey("content")) {
+                    PushMsgBody(
+                        msgHead = ResponseHead(
+                            peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(),
+                            peerUid = data["uid"]?.asString ?: TicketSvc.getUid(),
+                            receiverUid = TicketSvc.getUid(),
+                            forward = ResponseForward(
+                                friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname()
+                            )
+                        ),
+                        contentHead = ContentHead(
+                            msgType = 9,
+                            msgSubType = 175,
+                            divSeq = 175,
+                            msgViaRandom = Random.nextLong(),
+                            sequence = data["seq"]?.asLong ?: Random.nextLong(),
+                            msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000),
+                            u2 = 1,
+                            u6 = 0,
+                            u7 = 0,
+                            msgSeq = data["seq"]?.asLong ?: Random.nextLong(),
+                            forwardHead = ForwardHead(
+                                u1 = 0,
+                                u2 = 0,
+                                u3 = 2,
+                                ub641 = "",
+                                avatar = ""
+                            )
+                        ),
+                        body = MsgBody(
+                            richText = MessageHelper.messageArrayToRichText(
+                                chatType = chatType,
+                                msgId = Random.nextLong(),
+                                peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
+                                messageList = when (data["content"]) {
+                                    is JsonObject -> listOf(data["content"] as JsonObject).json
+                                    is JsonArray -> data["content"] as JsonArray
+                                    else -> MessageHelper.decodeCQCode(data["content"].asString)
+                                }.onEach { element ->
+                                    val elementData = element.asJsonObject["data"].asJsonObject
+                                    if (element.asJsonObject["type"].asString == "forward")
+                                        forwardMsg[elementData["filename"].asString] =
+                                            elementData["id"].asString
+                                }
+                            ).getOrElse { throw Exception("消息合成失败: $it") }.let {
+                                desc[++i] =
+                                    (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
+                                    ?: TicketSvc.getNickname()) + ": " + it.first
+                                it.second
+                            }
+                        )
+                    )
+                } else {
+                    error("消息节点缺少id或content字段")
+                }
+            }.getOrElse {
+                LogCenter.log("消息节点解析失败:$it", Level.WARN)
+                null
+            }
+        }.ifEmpty { return Result.failure(Exception("消息节点为空")) }
+
         val payload = LongMsgPayload(
-            action = listOf(
+            action = mutableListOf(
                 LongMsgAction(
                     command = "MultiMsg",
                     data = LongMsgContent(
-                        body = messages
+                        body = msgs
                     )
                 )
-            )
+            ).apply {
+                forwardMsg.map { msg ->
+                    addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) }
+                        .map { action ->
+                            if (action.command == "MultiMsg") LongMsgAction(
+                                command = msg.key,
+                                data = action.data
+                            ) else action
+                        })
+                }
+            }
         )
         LogCenter.log(payload.toByteArray().toHexString(), Level.DEBUG)
 
         val req = LongMsgReq(
-            sendInfo = SendLongMsgInfo(
-                type = if (groupUin == null) 1 else 3,
-                uid = LongMsgUid(groupUin ?: uid),
-                groupUin = groupUin?.toInt(),
-                payload = DeflateTools.gzip(payload.toByteArray())
-            ),
+            sendInfo = when (chatType) {
+                MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
+                    type = 1,
+                    uid = LongMsgUid(peerId),
+                    payload = DeflateTools.gzip(payload.toByteArray())
+                )
+
+                MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo(
+                    type = 3,
+                    uid = LongMsgUid(fromId),
+                    groupUin = fromId.toInt(),
+                    payload = DeflateTools.gzip(payload.toByteArray())
+                )
+
+                else -> throw UnsupportedOperationException(
+                    "Unsupported chatType: $chatType"
+                )
+            },
             setting = LongMsgSettings(
                 field1 = 4,
                 field2 = 2,
@@ -253,17 +407,29 @@ internal object MsgSvc : BaseSvc() {
                 field4 = 0
             )
         )
+
         val buffer = sendBufferAW(
             "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
             true,
             req.toByteArray()
         ) ?: return Result.failure(Exception("unable to upload multi message"))
         val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
-        return rsp.sendResult?.resId?.let { Result.success(it) }
-            ?: Result.failure(Exception("unable to upload multi message"))
+        val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message"))
+        val filename = UUID.randomUUID().toString().uppercase()
+        return Result.success(
+            MessageSegment(
+                "forward",
+                mapOf(
+                    "id" to resId,
+                    "filename" to filename,
+                    "summary" to "查看${desc.size}条转发消息",
+                    "desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n")
+                )
+            )
+        )
     }
 
-    suspend fun getMultiMsg(resId: String): Result<List<MessageDetail>> {
+    suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> {
         val req = LongMsgReq(
             recvInfo = RecvLongMsgInfo(
                 uid = LongMsgUid(TicketSvc.getUid()),
@@ -284,11 +450,18 @@ internal object MsgSvc : BaseSvc() {
         ) ?: return Result.failure(Exception("unable to get multi message"))
         val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
         val zippedPayload = DeflateTools.ungzip(
-            rsp.recvResult?.payload ?: return Result.failure(Exception("unable to get multi message"))
+            rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty"))
         )
         LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
-        val payload = zippedPayload.decodeProtobuf<LongMsgPayload>()
-        payload.action?.forEach {
+        return Result.success(
+            zippedPayload.decodeProtobuf<LongMsgPayload>().action
+                ?: return Result.failure(Exception("action is empty"))
+        )
+    }
+
+    suspend fun getForwardMsg(resId: String): Result<List<MessageDetail>> {
+        val result = getMultiMsg(resId).getOrElse { return Result.failure(it) }
+        result.forEach {
             if (it.command == "MultiMsg") {
                 return Result.success(it.data?.body?.map { msg ->
                     val chatType =
@@ -296,27 +469,29 @@ internal object MsgSvc : BaseSvc() {
                     MessageDetail(
                         time = msg.contentHead?.msgTime?.toInt() ?: 0,
                         msgType = MessageHelper.obtainDetailTypeByMsgType(chatType),
-                        msgId = 0, // MessageHelper.generateMsgIdHash(chatType, msg.content!!.msgViaRandom), msgViaRandom 为空
+                        msgId = 0, // msgViaRandom为空 tx不给
+                        qqMsgId = 0,
                         msgSeq = msg.contentHead!!.msgSeq ?: 0,
                         realId = msg.contentHead!!.msgSeq ?: 0,
                         sender = MessageSender(
                             msg.msgHead?.peer ?: 0,
-                            msg.msgHead?.responseGrp?.memberCard?.ifEmpty { msg.msgHead?.forward?.friendName }
-                                ?: msg.msgHead?.forward?.friendName ?: "",
+                            msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "",
                             "unknown",
                             0,
                             msg.msgHead?.peerUid ?: "",
                             msg.msgHead?.peerUid ?: ""
                         ),
-                        message = msg.body?.richText?.elements?.toSegments(chatType, msg.msgHead?.peer.toString(), "0")
-                            ?.toListMap() ?: emptyList(),
+                        message = msg.body?.richText?.toSegments(
+                            chatType,
+                            msg.msgHead?.peer.toString(),
+                            "0"
+                        )?.toListMap() ?: emptyList(),
                         peerId = msg.msgHead?.peer ?: 0,
                         groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
                             ?: 0 else 0,
                         targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0
                     )
-                }
-                    ?: return Result.failure(Exception("Msg is empty")))
+                } ?: return Result.failure(Exception("Msg is empty")))
             }
         }
         return Result.failure(Exception("Can't find msg"))
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt
index fc99577c..d74bf4e3 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt
@@ -10,24 +10,18 @@ import io.ktor.utils.io.core.writeInt
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withTimeoutOrNull
 import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
-
 import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
-import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
 import moe.fuqiuluo.shamrock.tools.broadcast
 import moe.fuqiuluo.shamrock.utils.DeflateTools
-import protobuf.message.element.LightAppElem
-import protobuf.message.PushMsgBody
-import protobuf.message.ContentHead
-import protobuf.message.Elem
-import protobuf.message.RichText
-import protobuf.message.ResponseHead
-import protobuf.message.MsgBody
-import protobuf.push.MessagePush
 import mqq.app.MobileQQ
 import protobuf.auto.toByteArray
+import protobuf.message.*
+import protobuf.message.element.LightAppElem
+import protobuf.push.MessagePush
 import kotlin.coroutines.resume
+import kotlin.text.toByteArray
 
-internal object PacketSvc: BaseSvc() {
+internal object PacketSvc : BaseSvc() {
     /**
      * 伪造收到Json卡片消息
      */
@@ -36,7 +30,7 @@ internal object PacketSvc: BaseSvc() {
             listOf(
                 Elem(
                     lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray())))
-            )
+                )
             )
         }
     }
@@ -44,7 +38,12 @@ internal object PacketSvc: BaseSvc() {
     private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List<Elem>): Long {
         val latestMsg = withTimeoutOrNull(3000) {
             suspendCancellableCoroutine {
-                msgService.getMsgs(Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), 0L, 1, true) { code, why, msgs ->
+                msgService.getMsgs(
+                    Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""),
+                    0L,
+                    1,
+                    true
+                ) { code, why, msgs ->
                     it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
                 }
             }
@@ -72,9 +71,11 @@ internal object PacketSvc: BaseSvc() {
                     u4 = msgSeq - 2,
                     u5 = msgSeq
                 ),
-                body = MsgBody(RichText(
-                    elements = builder()
-                ))
+                body = MsgBody(
+                    RichText(
+                        elements = builder()
+                    )
+                )
             )
         )
 
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt
index 751ac028..9f647ac7 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt
@@ -1,21 +1,46 @@
 package moe.fuqiuluo.qqinterface.servlet.msg
 
+import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
 import com.tencent.qqnt.kernel.nativeinterface.MsgElement
 import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
 import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter
+import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
 import moe.fuqiuluo.shamrock.helper.Level
 import moe.fuqiuluo.shamrock.helper.LogCenter
 import moe.fuqiuluo.shamrock.helper.MessageHelper
+import moe.fuqiuluo.shamrock.tools.toHexString
 import protobuf.message.Elem
+import protobuf.message.RichText
 
-@JvmName("elemListToSegments")
-internal suspend fun List<Elem>.toSegments(
+@JvmName("richTextToSegments")
+internal suspend fun RichText.toSegments(
     chatType: Int,
     peerId: String,
     subPeer: String
 ): List<MessageSegment> {
     val messageData = arrayListOf<MessageSegment>()
-    this.forEach { msg ->
+    if (ptt != null) {
+        val md5 = ptt!!.fileMd5!!
+        messageData.add(
+            MessageSegment(
+                "record", mapOf(
+                    "file" to md5.toHexString(),
+                    "url" to when (chatType) {
+                        MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt!!.fileUuid!!)
+                        MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
+                            "0",
+                            md5,
+                            ptt!!.groupFileKey!!
+                        )
+
+                        else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
+                    },
+                    "magic" to ptt!!.pbReserve?.magic,
+                )
+            )
+        )
+    }
+    elements?.forEach { msg ->
         kotlin.runCatching {
             val elementType = if (msg.text != null) {
                 1
@@ -29,7 +54,7 @@ internal suspend fun List<Elem>.toSegments(
                 37
             } else if (msg.srcMsg != null) {
                 45
-            }  else if (msg.lightApp != null) {
+            } else if (msg.lightApp != null) {
                 51
             } else if (msg.commonElem != null) {
                 53
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt
index cea7d17a..9f5fd5c6 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt
@@ -12,8 +12,8 @@ internal data class MessageSegment(
 ) {
     fun toJson(): JsonObject {
         return mapOf(
-            "type" to type.json,
-            "data" to data.json
+            "type" to type,
+            "data" to data
         ).json
     }
 }
@@ -29,6 +29,6 @@ internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
         mapOf(
             "type" to it.type.json,
             "data" to it.data.json
-        ).json
+        )
     }
 }
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt
index 60ae7a44..a4683efe 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt
@@ -13,11 +13,13 @@ import moe.fuqiuluo.shamrock.helper.LogCenter
 import moe.fuqiuluo.shamrock.helper.db.ImageDB
 import moe.fuqiuluo.shamrock.helper.db.ImageMapping
 import moe.fuqiuluo.shamrock.helper.db.MessageDB
+import moe.fuqiuluo.shamrock.tools.asJsonArray
 import moe.fuqiuluo.shamrock.utils.DeflateTools
 import moe.fuqiuluo.shamrock.tools.asJsonObject
 import moe.fuqiuluo.shamrock.tools.asString
 import moe.fuqiuluo.shamrock.tools.toHexString
 import moe.fuqiuluo.symbols.decodeProtobuf
+import moe.fuqiuluo.shamrock.tools.slice
 import protobuf.message.Elem
 import protobuf.message.element.commelem.ButtonExtra
 import protobuf.message.element.commelem.MarkdownExtra
@@ -335,10 +337,8 @@ internal object ElemConverter {
         element: Elem
     ): MessageSegment {
         val data = element.lightApp!!.data!!
-        val jsonStr =
-            (if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray(
-                1 until data.size
-            )).toString()
+        val jsonStr = String(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1))
+        LogCenter.log(jsonStr, Level.DEBUG)
         val json = jsonStr.asJsonObject
         return when (json["app"].asString) {
             "com.tencent.multimsg" -> {
@@ -346,7 +346,10 @@ internal object ElemConverter {
                 MessageSegment(
                     type = "forward",
                     data = mapOf(
-                        "id" to info["resid"].asString
+                        "id" to info["resid"].asString,
+                        "filename" to info["uniseq"].asString,
+                        "summary" to info["summary"].asString,
+                        "desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString }
                     )
                 )
             }
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt
index 365c1f35..03177adb 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt
@@ -2,10 +2,6 @@ package moe.fuqiuluo.qqinterface.servlet.msg.converter
 
 import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
 import com.tencent.qqnt.kernel.nativeinterface.MsgElement
-import kotlinx.serialization.json.add
-import kotlinx.serialization.json.buildJsonObject
-import kotlinx.serialization.json.put
-import kotlinx.serialization.json.putJsonArray
 import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
 import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
 import moe.fuqiuluo.shamrock.helper.ContactHelper
@@ -15,6 +11,7 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper
 import moe.fuqiuluo.shamrock.helper.db.ImageDB
 import moe.fuqiuluo.shamrock.helper.db.ImageMapping
 import moe.fuqiuluo.shamrock.helper.db.MessageDB
+import moe.fuqiuluo.shamrock.tools.asJsonArray
 import moe.fuqiuluo.shamrock.tools.asJsonObject
 import moe.fuqiuluo.shamrock.tools.asString
 import moe.fuqiuluo.shamrock.tools.hex2ByteArray
@@ -242,16 +239,10 @@ internal object NtMsgElementConverter {
             data = hashMapOf(
                 "file" to md5,
                 "url" to when (chatType) {
-                    MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
-                        "0",
-                        record.md5HexStr,
-                        record.fileUuid
-                    )
-
                     MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
-                    MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
+                    MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
                         "0",
-                        record.md5HexStr,
+                        md5.hex2ByteArray(),
                         record.fileUuid
                     )
 
@@ -343,7 +334,10 @@ internal object NtMsgElementConverter {
                 MessageSegment(
                     type = "forward",
                     data = mapOf(
-                        "id" to info["resid"].asString
+                        "id" to info["resid"].asString,
+                        "filename" to info["uniseq"].asString,
+                        "summary" to info["summary"].asString,
+                        "desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString }
                     )
                 )
             }
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt
index 6c82cc10..441da395 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt
@@ -13,64 +13,84 @@ import moe.fuqiuluo.qqinterface.servlet.msg.toJson
 import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
 import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
 import moe.fuqiuluo.shamrock.helper.*
-import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
+import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToRichText
+import moe.fuqiuluo.shamrock.helper.MessageHelper.obtainMessageTypeByDetailType
 import moe.fuqiuluo.shamrock.tools.*
 import moe.fuqiuluo.shamrock.utils.DeflateTools
 import moe.fuqiuluo.shamrock.utils.FileUtils
 import protobuf.auto.toByteArray
 import protobuf.message.Elem
+import protobuf.message.RichText
 import protobuf.message.element.*
 import protobuf.message.element.commelem.*
 import java.io.File
 import java.nio.ByteBuffer
+import java.util.*
 import kotlin.random.Random
 import kotlin.random.nextULong
 import kotlin.time.Duration.Companion.seconds
 
-internal typealias IElemMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
+internal typealias IElemMaker = suspend (ElemMaker, Int, Long, String, JsonObject) -> Unit
 
-internal object ElemMaker {
-    private val makerArray = hashMapOf(
-        "text" to ElemMaker::createTextElem,
-        "at" to ElemMaker::createAtElem,
-        "face" to ElemMaker::createFaceElem,
-        "pic" to ElemMaker::createImageElem,
-        "image" to ElemMaker::createImageElem,
+internal class ElemMaker {
+    companion object {
+        private val makerArray = hashMapOf(
+            "text" to ElemMaker::createTextElem,
+            "at" to ElemMaker::createAtElem,
+            "face" to ElemMaker::createFaceElem,
+            "pic" to ElemMaker::createImageElem,
+            "image" to ElemMaker::createImageElem,
 //        "voice" to ElemMaker::createRecordElem,
 //        "record" to ElemMaker::createRecordElem,
 //        "video" to ElemMaker::createVideoElem,
-        "markdown" to ElemMaker::createMarkdownElem,
-        "button" to ElemMaker::createButtonElem,
-        "dice" to ElemMaker::createNewDiceElem,
-        "rps" to ElemMaker::createNewRpsElem,
-        "poke" to ElemMaker::createPokeElem,
+            "forward" to ElemMaker::createForwardStruct,
+            "json" to ElemMaker::createJsonElem,
+            "poke" to ElemMaker::createPokeElem,
+            "dice" to ElemMaker::createNewDiceElem,
+            "rps" to ElemMaker::createNewRpsElem,
+            "markdown" to ElemMaker::createMarkdownElem,
+            "button" to ElemMaker::createButtonElem,
 //        "anonymous" to ElemMaker::createAnonymousElem,
 //        "share" to ElemMaker::createShareElem,
 //        "contact" to ElemMaker::createContactElem,
 //        "location" to ElemMaker::createLocationElem,
 //        "music" to ElemMaker::createMusicElem,
-        "reply" to ElemMaker::createReplyElem,
+            "reply" to ElemMaker::createReplyElem,
 //        "touch" to ElemMaker::createTouchElem,
-        "weather" to ElemMaker::createWeatherElem,
-        "json" to ElemMaker::createJsonElem,
-        //"forward" to MessageMaker::createForwardElem,
-        //"multi_msg" to MessageMaker::createLongMsgStruct,
-        //"bubble_face" to ElemMaker::createBubbleFaceElem,
-    )
+            "weather" to ElemMaker::createWeatherElem,
+            //"forward" to MessageMaker::createForwardElem,
+            //"multi_msg" to MessageMaker::createLongMsgStruct,
+            //"bubble_face" to ElemMaker::createBubbleFaceElem,
+        )
+
+        operator fun get(type: String): IElemMaker? = makerArray[type]
+    }
 
-    operator fun get(type: String): IElemMaker? = makerArray[type]
+    private var rich = RichText()
+    private val elems = mutableListOf<Elem>()
+    private var desc = ""
+
+    fun getRich(): RichText {
+        rich.elements = elems
+        return rich
+    }
+
+    fun getDesc(): String {
+        return desc
+    }
 
     private suspend fun createTextElem(
         chatType: Int,
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("text")
         val elem = Elem(
             text = TextMsg(data["text"].asString)
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += data["text"].asString
     }
 
     private suspend fun createAtElem(
@@ -78,8 +98,8 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
-        return when (chatType) {
+    ) {
+        when (chatType) {
             MsgConstant.KCHATTYPEGROUP -> {
                 data.checkAndThrow("qq")
 
@@ -105,15 +125,14 @@ internal object ElemMaker {
                             peerId.toLong(),
                             qq,
                             true
-                        )
-                            .let {
-                                val info = it.getOrNull()
-                                if (info == null)
-                                    LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
-                                info?.troopnick
-                                    .ifNullOrEmpty(info?.friendnick)
-                                    .ifNullOrEmpty(qqStr)
-                            })
+                        ).let {
+                            val info = it.getOrNull()
+                            if (info == null)
+                                LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
+                            else info.troopnick
+                                .ifNullOrEmpty(info.friendnick)
+                                .ifNullOrEmpty(qqStr)
+                        })
                     }
                 }
 
@@ -126,7 +145,8 @@ internal object ElemMaker {
                 val elem = Elem(
                     text = TextMsg(str = display, attr6Buf = attr6.array())
                 )
-                Result.success(elem)
+                elems.add(elem)
+                desc += display
             }
 
             MsgConstant.KCHATTYPEC2C -> {
@@ -144,10 +164,11 @@ internal object ElemMaker {
                 val elem = Elem(
                     text = TextMsg(str = display)
                 )
-                Result.success(elem)
+                elems.add(elem)
+                desc += display
             }
 
-            else -> Result.failure(ActionMsgException)
+            else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg")
         }
     }
 
@@ -156,7 +177,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("id")
         val faceId = data["id"].asInt
         val elem = if (data["big"].asBooleanOrNull == true) {
@@ -183,7 +204,8 @@ internal object ElemMaker {
                 )
             )
         }
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[表情]"
     }
 
     private suspend fun createImageElem(
@@ -191,7 +213,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         val isOriginal = data["original"].asBooleanOrNull ?: true
         val isFlash = data["flash"].asBooleanOrNull ?: false
         val filePath = data["file"].asStringOrNull
@@ -307,7 +329,8 @@ internal object ElemMaker {
 
             else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
         }
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[图片]"
     }
 
     private suspend fun createReplyElem(
@@ -315,16 +338,15 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("id")
         val msgHash = data["id"].asInt
         val mapping = MessageHelper.getMsgMappingByHash(msgHash)
-            ?: return Result.failure(Exception("不存在该消息映射,无法回复消息"))
+            ?: throw Exception("不存在该消息映射,无法回复消息")
 
         if (mapping.qqMsgId == 0L) {
             // 貌似获取失败了,555
-            LogCenter.log("无法获取被回复消息", Level.ERROR)
-            return Result.failure(Exception("无法获取被回复消息"))
+            throw Exception("无法获取被回复消息")
         }
 
         val elem = if (data.containsKey("text")) {
@@ -351,16 +373,15 @@ internal object ElemMaker {
             )
         } else {
             val msg =
-                MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() ?: return Result.failure(
-                    Exception("无法获取被回复消息")
-                )
+                MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull()
+                    ?: throw Exception("无法获取被回复消息")
             Elem(
                 srcMsg = SourceMsg(
                     origSeqs = listOf(msg.msgSeq.toInt()),
                     senderUin = msg.senderUin.toULong(),
                     time = msg.msgTime.toULong(),
                     flag = 1u,
-                    elems = messageArrayToMessageElements(
+                    elems = messageArrayToRichText(
                         msg.chatType,
                         msg.msgId,
                         msg.peerUin.toString(),
@@ -369,7 +390,7 @@ internal object ElemMaker {
                             if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
                             msg.channelId ?: msg.peerUin.toString()
                         ).toJson()
-                    ).second,
+                    ).getOrElse { throw Exception("解析回复消息失败: $it") }.second.elements,
                     type = 0u,
                     pbReserve = SourceMsg.Companion.PbReserve(
                         msgRand = Random.nextULong(),
@@ -380,7 +401,8 @@ internal object ElemMaker {
                 )
             )
         }
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[回复消息]"
     }
 
     private suspend fun createJsonElem(
@@ -388,15 +410,82 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("data")
 
         val elem = Elem(
             lightApp = LightAppElem(
-                data = DeflateTools.compress(data.toString().toByteArray())
+                data = byteArrayOf(1) + DeflateTools.compress(data.toString().toByteArray())
+            )
+        )
+        elems.add(elem)
+        desc += "[Json消息]"
+    }
+
+    private suspend fun createForwardStruct(
+        chatType: Int,
+        msgId: Long,
+        peerId: String,
+        data: JsonObject
+    ) {
+        data.checkAndThrow("id")
+        val resId = data["id"].asString
+        val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase()
+        var summary = data["summary"].asStringOrNull
+        val descriptions = data["desc"].asStringOrNull
+        var news = descriptions?.split("\n")?.map { "text" to it }
+
+        if (news == null || summary == null) {
+            val forwardMsg = MsgSvc.getForwardMsg(resId).getOrThrow()
+            if (news == null) {
+                news = forwardMsg.map {
+                    "text" to it.sender.nickName + ": " + messageArrayToRichText(
+                        obtainMessageTypeByDetailType(it.msgType),
+                        it.qqMsgId,
+                        it.peerId.toString(),
+                        it.message.json
+                    ).getOrThrow().first
+                }
+            }
+            if (summary == null) {
+                summary = "查看${forwardMsg.size}条转发消息"
+            }
+        }
+
+        val json = mapOf(
+            "app" to "com.tencent.multimsg",
+            "config" to mapOf(
+                "autosize" to 1,
+                "forward" to 1,
+                "round" to 1,
+                "type" to "normal",
+                "width" to 300
+            ),
+            "desc" to "[聊天记录]",
+            "extra" to mapOf(
+                "filename" to filename,
+                "tsum" to 2
+            ).json.toString(),
+            "meta" to mapOf(
+                "detail" to mapOf(
+                    "news" to news,
+                    "resid" to resId,
+                    "source" to "群聊的聊天记录",
+                    "summary" to summary,
+                    "uniseq" to filename
+                )
+            ),
+            "prompt" to "[聊天记录]",
+            "ver" to "0.0.0.5",
+            "view" to "contact"
+        )
+        val elem = Elem(
+            lightApp = LightAppElem(
+                data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray())
             )
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[聊天记录]"
     }
 
     private suspend fun createWeatherElem(
@@ -404,7 +493,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         var code = data["code"].asIntOrNull
 
         if (code == null) {
@@ -416,19 +505,22 @@ internal object ElemMaker {
         }
 
         if (code != null) {
-            WeatherSvc.fetchWeatherCard(code).onSuccess {
-//              OidbSvc.0xdc2_34
-//              00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38
-                return createJsonElem(
-                    chatType, msgId, peerId, it["weekStore"]
-                        .asJsonObject["share"].asJsonObject
+            val weatherCard = WeatherSvc.fetchWeatherCard(code).getOrThrow()
+//          OidbSvc.0xdc2_34
+//          00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38
+            val elem = Elem(
+                lightApp = LightAppElem(
+                    data = byteArrayOf(1) + DeflateTools.compress(
+                        weatherCard["weekStore"]
+                            .asJsonObject["share"].asString.toByteArray()
+                    )
                 )
-            }.onFailure {
-                LogCenter.log("无法发送天气分享", Level.ERROR)
-            }
+            )
+            elems.add(elem)
+            desc += "[天气卡片]"
+        } else {
+            throw LogicException("无法获取城市天气")
         }
-
-        return Result.failure(ActionMsgException)
     }
 
     private suspend fun createPokeElem(
@@ -436,7 +528,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("type", "id")
         val elem = Elem(
             commonElem = CommonElem(
@@ -449,7 +541,8 @@ internal object ElemMaker {
                 businessType = data["id"].asInt
             )
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[戳一戳]"
     }
 
     private suspend fun createNewDiceElem(
@@ -457,7 +550,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         val elem = Elem(
             commonElem = CommonElem(
                 serviceType = 37,
@@ -474,7 +567,8 @@ internal object ElemMaker {
                 businessType = 2
             )
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[骰子]"
     }
 
     private suspend fun createNewRpsElem(
@@ -482,7 +576,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         val elem = Elem(
             commonElem = CommonElem(
                 serviceType = 37,
@@ -499,7 +593,8 @@ internal object ElemMaker {
                 businessType = 1
             )
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[包剪锤]"
     }
 
     private suspend fun createMarkdownElem(
@@ -507,7 +602,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("content")
         val elem = Elem(
             commonElem = CommonElem(
@@ -516,7 +611,8 @@ internal object ElemMaker {
                 businessType = 1
             )
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[Markdown消息]"
     }
 
     private suspend fun createButtonElem(
@@ -524,7 +620,7 @@ internal object ElemMaker {
         msgId: Long,
         peerId: String,
         data: JsonObject
-    ): Result<Elem> {
+    ) {
         data.checkAndThrow("buttons")
         val elem = Elem(
             commonElem = CommonElem(
@@ -565,7 +661,8 @@ internal object ElemMaker {
                 businessType = 1
             )
         )
-        return Result.success(elem)
+        elems.add(elem)
+        desc += "[Button消息]"
     }
 
     private fun JsonObject.checkAndThrow(vararg key: String) {
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt
index e09a97cf..4699dc8c 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt
@@ -16,6 +16,7 @@ import kotlinx.serialization.json.JsonPrimitive
 import moe.fuqiuluo.qqinterface.servlet.CardSvc
 import moe.fuqiuluo.qqinterface.servlet.GroupSvc
 import moe.fuqiuluo.qqinterface.servlet.LbsSvc
+import moe.fuqiuluo.qqinterface.servlet.MsgSvc
 import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
 import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
 import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
@@ -51,6 +52,8 @@ import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
 import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2
 import tencent.im.oidb.oidb_sso
 import java.io.File
+import java.util.*
+import kotlin.collections.ArrayList
 import kotlin.math.roundToInt
 import kotlin.random.Random
 import kotlin.random.nextInt
@@ -80,10 +83,11 @@ internal object NtMsgElementMaker {
         "touch" to NtMsgElementMaker::createTouchElem,
         "weather" to NtMsgElementMaker::createWeatherElem,
         "json" to NtMsgElementMaker::createJsonElem,
+        "forward" to NtMsgElementMaker::createForwardStruct,
         "new_dice" to NtMsgElementMaker::createNewDiceElem,
         "new_rps" to NtMsgElementMaker::createNewRpsElem,
         "basketball" to NtMsgElementMaker::createBasketballElem,
-        //"multi_msg" to MessageMaker::createLongMsgStruct,
+        //"multi_msg" to NtMsgElementMaker::createLongMsgStruct,
         "bubble_face" to NtMsgElementMaker::createBubbleFaceElem,
         "button" to NtMsgElementMaker::createInlineKeywordElem,
         "inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem
@@ -91,6 +95,70 @@ internal object NtMsgElementMaker {
 
     operator fun get(type: String): IMsgElementMaker? = makerMap[type]
 
+    private suspend fun createForwardStruct(
+        chatType: Int,
+        msgId: Long,
+        peerId: String,
+        data: JsonObject
+    ): Result<MsgElement> {
+        data.checkAndThrow("id")
+        val resId = data["id"].asString
+        val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase()
+        var summary = data["summary"].asStringOrNull
+        val descriptions = data["desc"].asStringOrNull
+        var news = descriptions?.split("\n")?.map { "text" to it }
+
+        if (news == null || summary == null) {
+            val forwardMsg = MsgSvc.getForwardMsg(resId).getOrElse { return Result.failure(it) }
+            if (news == null) {
+                news = forwardMsg.map {
+                    "text" to it.sender.nickName + ": " + MessageHelper.messageArrayToRichText(
+                        MessageHelper.obtainMessageTypeByDetailType(it.msgType),
+                        it.qqMsgId,
+                        it.peerId.toString(),
+                        it.message.json
+                    ).getOrThrow().first
+                }
+            }
+            if (summary == null) {
+                summary = "查看${forwardMsg.size}条转发消息"
+            }
+        }
+
+        val json = mapOf(
+            "app" to "com.tencent.multimsg",
+            "config" to mapOf(
+                "autosize" to 1,
+                "forward" to 1,
+                "round" to 1,
+                "type" to "normal",
+                "width" to 300
+            ),
+            "desc" to "[聊天记录]",
+            "extra" to mapOf(
+                "filename" to filename,
+                "tsum" to 2
+            ).json.toString(),
+            "meta" to mapOf(
+                "detail" to mapOf(
+                    "news" to news,
+                    "resid" to resId,
+                    "source" to "群聊的聊天记录",
+                    "summary" to summary,
+                    "uniseq" to filename
+                )
+            ),
+            "prompt" to "[聊天记录]",
+            "ver" to "0.0.0.5",
+            "view" to "contact"
+        )
+        return createJsonElem(
+            chatType, msgId, peerId, mapOf(
+                "data" to json
+            ).json
+        )
+    }
+
     private suspend fun createInlineKeywordElem(
         chatType: Int,
         msgId: Long,
@@ -262,19 +330,21 @@ internal object NtMsgElementMaker {
     ): Result<MsgElement> {
         data.checkAndThrow("data")
         val jsonStr = data["data"].let {
-            if (it is JsonObject) it.asJsonObject.toString() else {
-                val str = it.asStringOrNull ?: ""
+            if (it is JsonObject) {
+                it.asJsonObject.toString()
+            } else {
                 // 检查字符串是否是合法json,不然qq会闪退
                 try {
+                    val str = it.asString
                     val element = Json.decodeFromString<JsonElement>(str)
                     if (element !is JsonObject) {
                         return Result.failure(Exception("不合法的JSON字符串"))
                     }
+                    str
                 } catch (err: Throwable) {
                     LogCenter.log(err.stackTraceToString(), Level.ERROR)
                     return Result.failure(Exception("不合法的JSON字符串"))
                 }
-                str
             }
         }
         val element = MsgElement()
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt
index 567a6994..f37a6cca 100644
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt
@@ -6,7 +6,6 @@ import com.tencent.mobileqq.transfile.FileMsg
 import com.tencent.mobileqq.transfile.api.IProtoReqManager
 import com.tencent.mobileqq.transfile.protohandler.RichProto
 import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.serialization.ExperimentalSerializationApi
 import moe.fuqiuluo.qqinterface.servlet.BaseSvc
@@ -22,23 +21,9 @@ import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
 import moe.fuqiuluo.symbols.decodeProtobuf
 import mqq.app.MobileQQ
 import protobuf.auto.toByteArray
-import protobuf.oidb.TrpcOidb
 import protobuf.oidb.cmd0x11c5.C2CUserInfo
 import protobuf.oidb.cmd0x11c5.ChannelUserInfo
-import protobuf.oidb.cmd0x11c5.ClientMeta
-import protobuf.oidb.cmd0x11c5.CodecConfigReq
-import protobuf.oidb.cmd0x11c5.CommonHead
-import protobuf.oidb.cmd0x11c5.DownloadExt
-import protobuf.oidb.cmd0x11c5.DownloadReq
-import protobuf.oidb.cmd0x11c5.FileInfo
-import protobuf.oidb.cmd0x11c5.FileType
 import protobuf.oidb.cmd0x11c5.GroupUserInfo
-import protobuf.oidb.cmd0x11c5.IndexNode
-import protobuf.oidb.cmd0x11c5.MultiMediaReqHead
-import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq
-import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
-import protobuf.oidb.cmd0x11c5.SceneInfo
-import protobuf.oidb.cmd0x11c5.VideoDownloadExt
 import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
 import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
 import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
@@ -405,8 +390,8 @@ internal object RichProtoSvc: BaseSvc() {
 
     suspend fun getGroupPttDownUrl(
         peerId: String,
-        md5Hex: String,
-        fileUUId: String
+        md5: ByteArray,
+        groupFileKey: String
     ): String {
         return suspendCancellableCoroutine {
             val runtime = AppRuntimeFetcher.appRuntime
@@ -417,8 +402,8 @@ internal object RichProtoSvc: BaseSvc() {
             groupPttDownReq.secondUin = peerId
             groupPttDownReq.uinType = FileMsg.UIN_TROOP
             groupPttDownReq.groupFileID = 0
-            groupPttDownReq.groupFileKey = fileUUId
-            groupPttDownReq.md5 = md5Hex.hex2ByteArray()
+            groupPttDownReq.groupFileKey = groupFileKey
+            groupPttDownReq.md5 = md5
             groupPttDownReq.voiceType = 1
             groupPttDownReq.downType = 1
             richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt
index 56a15258..0b990e48 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt
@@ -21,12 +21,9 @@ import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker
 import moe.fuqiuluo.shamrock.helper.db.MessageDB
 import moe.fuqiuluo.shamrock.helper.db.MessageMapping
 import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
-import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
-import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
-import moe.fuqiuluo.shamrock.tools.asString
-import moe.fuqiuluo.shamrock.tools.json
-import moe.fuqiuluo.shamrock.tools.jsonArray
+import moe.fuqiuluo.shamrock.tools.*
 import protobuf.message.Elem
+import protobuf.message.RichText
 import kotlin.coroutines.resume
 import kotlin.math.abs
 import kotlin.time.Duration.Companion.seconds
@@ -75,7 +72,7 @@ internal object MessageHelper {
             }
             if (resendRet != 0 &&
                 resendRet != 4 // 使用OldBDH 100%触发
-                ) {
+            ) {
                 resendMsg(contact, msgId, retryCnt - 1, msgHashId)
             } else {
                 Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
@@ -319,38 +316,30 @@ internal object MessageHelper {
         return hasActionMsg to msgList
     }
 
-    suspend fun messageArrayToMessageElements(
+    suspend fun messageArrayToRichText(
         chatType: Int,
         msgId: Long,
         peerId: String,
         messageList: JsonArray
-    ): Pair<Boolean, ArrayList<Elem>> {
-        val msgList = arrayListOf<Elem>()
-        var hasActionMsg = false
-        messageList.forEach {
-            val msg = it.jsonObject
+    ): Result<Pair<String, RichText>> {
+        val elemMaker = ElemMaker()
+        messageList.forEach { element ->
+            val msg = element.asJsonObject
             val maker = ElemMaker[msg["type"].asString]
             if (maker != null) {
                 try {
                     val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
-                    maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
-                        msgList.add(msgElem)
-                    }.onFailure {
-                        if (it.javaClass != ActionMsgException::class.java) {
-                            throw it
-                        } else {
-                            hasActionMsg = true
-                        }
-                    }
+                    maker(elemMaker, chatType, msgId, peerId, data)
                 } catch (e: Throwable) {
-                    LogCenter.log(e.stackTraceToString(), Level.ERROR)
+                    if (e.javaClass != ActionMsgException::class.java) {
+                        LogCenter.log(e.stackTraceToString(), Level.ERROR)
+                    }
                 }
             } else {
                 LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
-                return false to arrayListOf()
             }
         }
-        return hasActionMsg to msgList
+        return Result.success(elemMaker.getDesc() to elemMaker.getRich())
     }
 
     fun generateMsgIdHash(chatType: Int, msgId: Long): Int {
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt
index 54a2522f..f4a8d6b6 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt
@@ -1,12 +1,10 @@
 package moe.fuqiuluo.shamrock.remote.action.handlers
 
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
 import kotlinx.serialization.json.JsonElement
 import moe.fuqiuluo.qqinterface.servlet.MsgSvc
 import moe.fuqiuluo.shamrock.remote.action.ActionSession
 import moe.fuqiuluo.shamrock.remote.action.IActionHandler
-import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
+import moe.fuqiuluo.shamrock.remote.service.data.GetForwardMsgResult
 import moe.fuqiuluo.shamrock.tools.EmptyJsonString
 import moe.fuqiuluo.symbols.OneBotHandler
 
@@ -21,14 +19,12 @@ internal object GetForwardMsg : IActionHandler() {
         resId: String,
         echo: JsonElement = EmptyJsonString
     ): String {
-        val result = MsgSvc.getMultiMsg(resId).getOrElse { return logic(it.toString(), echo) }
-        return ok(data = GetForwardMsgResult(result), echo = echo)
+        return ok(
+            data = GetForwardMsgResult(
+                msgs = MsgSvc.getForwardMsg(resId).getOrElse { return logic(it.toString(), echo = echo) }),
+            echo = echo
+        )
     }
 
-    @Serializable
-    data class GetForwardMsgResult(
-        @SerialName("messages") val msgs: List<MessageDetail>
-    )
-
     override val requiredParams: Array<String> = arrayOf("id")
 }
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt
index 86432710..a587dbb0 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt
@@ -69,6 +69,7 @@ internal object GetHistoryMsg : IActionHandler() {
                     time = msg.msgTime.toInt(),
                     msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
                     msgId = msgHash,
+                    qqMsgId = msg.msgId,
                     msgSeq = msg.msgSeq,
                     realId = msg.msgSeq,
                     sender = MessageSender(
@@ -92,6 +93,7 @@ internal object GetHistoryMsg : IActionHandler() {
                     time = msg.msgTime.toInt(),
                     msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
                     msgId = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId),
+                    qqMsgId = msg.msgId,
                     msgSeq = msg.msgSeq,
                     realId = msg.msgSeq,
                     sender = MessageSender(
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt
index 7657cafe..c7dfbe89 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt
@@ -29,6 +29,7 @@ internal object GetMsg: IActionHandler() {
             time = msg.msgTime.toInt(),
             msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
             msgId = msgHash,
+            qqMsgId = msg.msgId,
             msgSeq = msg.msgSeq,
             realId = msg.msgSeq,
             sender = MessageSender(
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt
index 069649c6..7acf84d9 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt
@@ -3,20 +3,14 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
 import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
 import kotlinx.serialization.json.*
 import moe.fuqiuluo.qqinterface.servlet.MsgSvc
-import moe.fuqiuluo.qqinterface.servlet.TicketSvc
-import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
+import moe.fuqiuluo.qqinterface.servlet.msg.toJson
 import moe.fuqiuluo.shamrock.helper.MessageHelper
 import moe.fuqiuluo.shamrock.helper.ParamsException
 import moe.fuqiuluo.shamrock.remote.action.ActionSession
 import moe.fuqiuluo.shamrock.remote.action.IActionHandler
-import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
+import moe.fuqiuluo.shamrock.remote.service.data.SendForwardMessageResult
 import moe.fuqiuluo.shamrock.tools.*
 import moe.fuqiuluo.symbols.OneBotHandler
-import protobuf.message.*
-import java.util.*
-import kotlin.random.Random
 
 @OneBotHandler("send_forward_msg")
 internal object SendForwardMessage : IActionHandler() {
@@ -60,9 +54,10 @@ internal object SendForwardMessage : IActionHandler() {
 
                 else -> error("unknown chat type: $chatType")
             }.toString()
+            val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
             return if (session.isArray("messages")) {
                 val messages = session.getArray("messages")
-                invoke(chatType, peerId, messages, fromId, echo = session.echo)
+                invoke(chatType, peerId, fromId, messages, retryCnt, session.echo)
             } else {
                 logic("未知格式合并转发消息", session.echo)
             }
@@ -76,222 +71,22 @@ internal object SendForwardMessage : IActionHandler() {
     suspend operator fun invoke(
         chatType: Int,
         peerId: String,
-        messages: JsonArray,
         fromId: String = peerId,
+        messages: JsonArray,
+        retryCnt: Int,
         echo: JsonElement = EmptyJsonString
     ): String {
-        var uid: String? = null
-        var groupUin: String? = null
-
-        var i = -1
-        val desc = MutableList(messages.size) { "" }
-
-        val msgs = messages.map { msg ->
-            kotlin.runCatching {
-                val data = msg.asJsonObject["data"].asJsonObject
-                if (data.containsKey("id")) {
-                    val record = MsgSvc.getMsg(data["id"].asInt).getOrElse {
-                        error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
-                    }
-                    if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
-                    if (record.chatType == MsgConstant.KCHATTYPEC2C) uid = record.peerUid
-                    PushMsgBody(
-                        msgHead = ResponseHead(
-                            peerUid = record.senderUid,
-                            receiverUid = record.peerUid,
-                            forward = ResponseForward(
-                                friendName = record.sendNickName
-                            ),
-                            responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
-                                groupCode = record.peerUin.toULong(),
-                                memberCard = record.sendMemberName,
-                                u1 = 2
-                            ) else null
-                        ),
-                        contentHead = ContentHead(
-                            msgType = when (record.chatType) {
-                                MsgConstant.KCHATTYPEC2C -> 9
-                                MsgConstant.KCHATTYPEGROUP -> 82
-                                else -> throw UnsupportedOperationException(
-                                    "Unsupported chatType: $chatType"
-                                )
-                            },
-                            msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
-                            divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
-                            msgViaRandom = record.msgId,
-                            sequence = record.msgSeq, // idk what this is(i++)
-                            msgTime = record.msgTime,
-                            u2 = 1,
-                            u6 = 0,
-                            u7 = 0,
-                            msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
-                            forwardHead = ForwardHead(
-                                u1 = 0,
-                                u2 = 0,
-                                u3 = 0,
-                                ub641 = "",
-                                avatar = ""
-                            )
-                        ),
-                        body = MsgBody(
-                            richText = RichText(
-                                elements = MessageHelper.messageArrayToMessageElements(
-                                    record.chatType,
-                                    record.msgId,
-                                    record.peerUin.toString(),
-                                    record.elements.toSegments(
-                                        record.chatType,
-                                        record.peerUin.toString(),
-                                        "0"
-                                    ).also {
-                                        desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": "
-                                    }.map {
-                                        desc[i] += when (it.type) {
-                                            "text" -> it.data["text"] as String
-                                            "at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}"
-                                            "face" -> "[表情]"
-                                            "pic", "image" -> "[图片]"
-                                            "voice", "record" -> "[语音]"
-                                            "video" -> "[视频]"
-                                            "node" -> "[合并转发消息]"
-                                            "markdown" -> "[Markdown消息]"
-                                            "button" -> "[Button类型]"
-                                            else -> "[未知消息类型]"
-                                        }
-                                        it.toJson()
-                                    }.json
-                                ).also {
-                                    if (it.second.isEmpty() && !it.first)
-                                        error("消息合成失败,请查看日志或者检查输入。")
-                                }.second
-                            )
-                        )
-                    )
-                } else if (data.containsKey("content")) {
-                    PushMsgBody(
-                        msgHead = ResponseHead(
-                            peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(),
-                            peerUid = data["uid"]?.asString ?: TicketSvc.getUid(),
-                            receiverUid = TicketSvc.getUid(),
-                            forward = ResponseForward(
-                                friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname()
-                            )
-                        ),
-                        contentHead = ContentHead(
-                            msgType = 9,
-                            msgSubType = 175,
-                            divSeq = 175,
-                            msgViaRandom = Random.nextLong(),
-                            sequence = data["seq"]?.asLong ?: Random.nextLong(),
-                            msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000),
-                            u2 = 1,
-                            u6 = 0,
-                            u7 = 0,
-                            msgSeq = data["seq"]?.asLong ?: Random.nextLong(),
-                            forwardHead = ForwardHead(
-                                u1 = 0,
-                                u2 = 0,
-                                u3 = 2,
-                                ub641 = "",
-                                avatar = ""
-                            )
-                        ),
-                        body = MsgBody(
-                            richText = RichText(
-                                elements = MessageHelper.messageArrayToMessageElements(
-                                    chatType = chatType,
-                                    msgId = Random.nextLong(),
-                                    peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
-                                    messageList = when (data["content"]) {
-                                        is JsonObject -> listOf(data["content"] as JsonObject).json
-                                        is JsonArray -> data["content"] as JsonArray
-                                        else -> MessageHelper.decodeCQCode(data["content"].asString)
-                                    }.also {
-                                        desc[++i] =
-                                            (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
-                                            ?: TicketSvc.getNickname()) + ": "
-                                    }.onEach {
-                                        val type = it.asJsonObject["type"].asString
-                                        val itData = it.asJsonObject["data"].asJsonObject
-                                        desc[i] += when (type) {
-                                            "text" -> itData["text"].asString
-                                            "at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
-                                            "face" -> "[表情]"
-                                            "pic", "image" -> "[图片]"
-                                            "voice", "record" -> "[语音]"
-                                            "video" -> "[视频]"
-                                            "node" -> "[合并转发消息]"
-                                            "markdown" -> "[Markdown消息]"
-                                            "button" -> "[Button类型]"
-                                            else -> "[未知消息类型]"
-                                        }
-                                    }
-                                ).also {
-                                    if (it.second.isEmpty() && !it.first)
-                                        error("消息合成失败,请查看日志或者检查输入。")
-                                }.second
-                            )
-                        )
-                    )
-                } else {
-                    error("消息节点缺少id或content字段")
-                }
-            }.getOrElse {
-                LogCenter.log("消息节点解析失败:$it", Level.WARN)
-                null
-            }
-        }.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
-
-
         kotlin.runCatching {
-            val resid = MsgSvc.uploadMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
+            val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt)
                 .getOrElse { return logic(it.message ?: "", echo) }
-            val uniseq = UUID.randomUUID().toString().uppercase()
 
-            val result = MsgSvc.sendToAio(
-                chatType, peerId,
-                listOf(
-                    mapOf(
-                        "type" to "json",
-                        "data" to mapOf(
-                            "data" to mapOf(
-                                "app" to "com.tencent.multimsg",
-                                "config" to mapOf(
-                                    "autosize" to 1,
-                                    "forward" to 1,
-                                    "round" to 1,
-                                    "type" to "normal",
-                                    "width" to 300
-                                ),
-                                "desc" to "[聊天记录]",
-                                "extra" to mapOf(
-                                    "filename" to uniseq,
-                                    "tsum" to 2
-                                ).json.toString(),
-                                "meta" to mapOf(
-                                    "detail" to mapOf(
-                                        "news" to desc.slice(0..if (i < 3) i else 3)
-                                            .map { mapOf("text" to it) },
-                                        "resid" to resid,
-                                        "source" to "群聊的聊天记录",
-                                        "summary" to "查看${msgs.size}条转发消息",
-                                        "uniseq" to uniseq
-                                    )
-                                ),
-                                "prompt" to "[聊天记录]",
-                                "ver" to "0.0.0.5",
-                                "view" to "contact"
-                            ),
-                            "resid" to resid
-                        )
-                    )
-                ).json, fromId, 3
-            ).getOrElse { return logic(it.message ?: "", echo) }
+            val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt)
+                .getOrElse { return logic(it.message ?: "", echo) }
 
             return ok(
-                ForwardMessageResult(
+                SendForwardMessageResult(
                     msgId = result.msgHashId,
-                    forwardId = resid
+                    resId = message.data["id"] as String
                 ), echo = echo
             )
         }.onFailure {
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt
index 5ddbe16c..bac2caa5 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt
@@ -6,12 +6,19 @@ import moe.fuqiuluo.shamrock.remote.action.IActionHandler
 import moe.fuqiuluo.symbols.OneBotHandler
 
 @OneBotHandler("send_group_forward_msg")
-internal object SendGroupForwardMessage: IActionHandler() {
+internal object SendGroupForwardMessage : IActionHandler() {
     override suspend fun internalHandle(session: ActionSession): String {
         val groupId = session.getLong("group_id")
+        val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
         return if (session.isArray("messages")) {
             val messages = session.getArray("messages")
-            SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), messages, echo = session.echo)
+            SendForwardMessage(
+                MsgConstant.KCHATTYPEGROUP,
+                groupId.toString(),
+                messages = messages,
+                retryCnt = retryCnt,
+                echo = session.echo
+            )
         } else {
             logic("未知格式合并转发消息", session.echo)
         }
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt
index 4c2c4367..6c9ae7cd 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt
@@ -21,7 +21,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
 import moe.fuqiuluo.symbols.OneBotHandler
 
 @OneBotHandler("send_msg", ["send_message"])
-internal object SendMessage: IActionHandler() {
+internal object SendMessage : IActionHandler() {
     override suspend fun internalHandle(session: ActionSession): String {
         val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
         try {
@@ -40,28 +40,61 @@ internal object SendMessage: IActionHandler() {
                     return noParam("detail_type/message_type", session.echo)
                 }
             }
-            val peerId = when(chatType) {
-                MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam("group_id", session.echo)
-                MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo)
+            val peerId = when (chatType) {
+                MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam(
+                    "group_id",
+                    session.echo
+                )
+
+                MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id")
+                    ?: return noParam("user_id", session.echo)
+
                 else -> error("unknown chat type: $chatType")
             }.toString()
-            val fromId = when(chatType) {
-                MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id") ?: return noParam("group_id", session.echo)
+            val fromId = when (chatType) {
+                MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id")
+                    ?: return noParam("group_id", session.echo)
+
                 MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo)
                 else -> error("unknown chat type: $chatType")
             }.toString()
-            val retryCnt = session.getIntOrNull("retry_cnt")
+            val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
             val recallDuration = session.getLongOrNull("recall_duration")
             return if (session.isString("message")) {
                 val autoEscape = session.getBooleanOrDefault("auto_escape", false)
                 val message = session.getString("message")
-                invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId, retryCnt = retryCnt ?: 5, recallDuration = recallDuration)
+                invoke(
+                    chatType,
+                    peerId,
+                    message,
+                    autoEscape,
+                    echo = session.echo,
+                    fromId = fromId,
+                    retryCnt = retryCnt,
+                    recallDuration = recallDuration
+                )
             } else if (session.isArray("message")) {
                 val message = session.getArray("message")
-                invoke(chatType, peerId, message, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration)
+                invoke(
+                    chatType,
+                    peerId,
+                    message,
+                    session.echo,
+                    fromId = fromId,
+                    retryCnt = retryCnt,
+                    recallDuration = recallDuration
+                )
             } else {
                 val message = session.getObject("message")
-                invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration)
+                invoke(
+                    chatType,
+                    peerId,
+                    listOf(message).jsonArray,
+                    session.echo,
+                    fromId = fromId,
+                    retryCnt = retryCnt,
+                    recallDuration = recallDuration
+                )
             }
         } catch (e: ParamsException) {
             return noParam(e.message!!, session.echo)
@@ -82,14 +115,16 @@ internal object SendMessage: IActionHandler() {
         echo: JsonElement = EmptyJsonString
     ): String {
         val result = if (autoEscape) {
-            MsgSvc.sendToAio(chatType, peerId, listOf(
-                mapOf(
-                    "type" to "text",
-                    "data" to mapOf(
-                        "text" to message
+            MsgSvc.sendToAio(
+                chatType, peerId, listOf(
+                    mapOf(
+                        "type" to "text",
+                        "data" to mapOf(
+                            "text" to message
+                        )
                     )
-                )
-            ).json, fromId = fromId, retryCnt)
+                ).json, fromId = fromId, retryCnt
+            )
         } else {
             val msg = MessageHelper.decodeCQCode(message)
             if (msg.isEmpty()) {
@@ -98,44 +133,54 @@ internal object SendMessage: IActionHandler() {
             } else {
                 MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId, retryCnt)
             }
-        }
-        if (result.isFailure) {
-            return logic(result.exceptionOrNull()?.message ?: "", echo)
-        }
-        val sendMsgResult = result.getOrThrow()
-        if (sendMsgResult.msgHashId <= 0) {
+        }.getOrElse{ return logic(it.message ?: "", echo)}
+        if (result.msgHashId <= 0) {
             return logic("send message failed", echo = echo)
         }
-        recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) }
-        return ok(MessageResult(
-            msgId = sendMsgResult.msgHashId,
-            time = (sendMsgResult.msgTime * 0.001).toLong()
-        ), echo = echo)
+        if (recallDuration != null) {
+            GlobalScope.launch(Dispatchers.Default) {
+                delay(recallDuration)
+                MsgSvc.recallMsg(result.msgHashId)
+            }
+        }
+        return ok(
+            MessageResult(
+                msgId = result.msgHashId,
+                time = (result.msgTime * 0.001).toLong()
+            ), echo = echo
+        )
     }
 
     // 消息段格式消息
     suspend operator fun invoke(
-        chatType: Int, peerId: String, message: JsonArray, echo: JsonElement = EmptyJsonString, fromId: String = peerId, retryCnt: Int, recallDuration: Long?,
+        chatType: Int,
+        peerId: String,
+        message: JsonArray,
+        echo: JsonElement = EmptyJsonString,
+        fromId: String = peerId,
+        retryCnt: Int,
+        recallDuration: Long?,
     ): String {
         //if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
         //    return logic("contact is not found", echo = echo)
         //}
-        val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) }
+        val result = MsgSvc.sendToAio(chatType, peerId, message, fromId, retryCnt)
+            .getOrElse { return logic(it.message ?: "", echo) }
         if (result.msgHashId <= 0) {
             return logic("send message failed", echo = echo)
         }
-        recallDuration?.let { autoRecall(result.msgHashId, it) }
-        return ok(MessageResult(
-            msgId = result.msgHashId,
-            time = (result.msgTime * 0.001).toLong()
-        ), echo)
-    }
-
-    private fun autoRecall(msgHash: Int, duration: Long) {
-        GlobalScope.launch(Dispatchers.Default) {
-            delay(duration)
-            MsgSvc.recallMsg(msgHash)
+        if (recallDuration != null) {
+            GlobalScope.launch(Dispatchers.Default) {
+                delay(recallDuration)
+                MsgSvc.recallMsg(result.msgHashId)
+            }
         }
+        return ok(
+            MessageResult(
+                msgId = result.msgHashId,
+                time = (result.msgTime * 0.001).toLong()
+            ), echo
+        )
     }
 
     override val requiredParams: Array<String> = arrayOf("message")
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt
index e4c70975..e857043f 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt
@@ -9,9 +9,18 @@ import moe.fuqiuluo.symbols.OneBotHandler
 internal object SendPrivateForwardMessage : IActionHandler() {
     override suspend fun internalHandle(session: ActionSession): String {
         val userId = session.getLong("user_id")
+        val groupId = session.getLongOrNull("group_id")
+        val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
         return if (session.isArray("messages")) {
             val messages = session.getArray("messages")
-            SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId.toString(), messages, echo = session.echo)
+            SendForwardMessage(
+                MsgConstant.KCHATTYPEC2C,
+                userId.toString(),
+                groupId?.toString() ?: userId.toString(),
+                messages,
+                retryCnt,
+                echo = session.echo
+            )
         } else {
             logic("未知格式合并转发消息", session.echo)
         }
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt
new file mode 100644
index 00000000..73667b65
--- /dev/null
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt
@@ -0,0 +1,97 @@
+package moe.fuqiuluo.shamrock.remote.action.handlers
+
+import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
+import kotlinx.serialization.json.*
+import moe.fuqiuluo.qqinterface.servlet.MsgSvc
+import moe.fuqiuluo.shamrock.helper.MessageHelper
+import moe.fuqiuluo.shamrock.helper.ParamsException
+import moe.fuqiuluo.shamrock.remote.action.ActionSession
+import moe.fuqiuluo.shamrock.remote.action.IActionHandler
+import moe.fuqiuluo.shamrock.remote.service.data.UploadForwardMessageResult
+import moe.fuqiuluo.shamrock.tools.*
+import moe.fuqiuluo.symbols.OneBotHandler
+
+@OneBotHandler("upload_multi_message")
+internal object UploadMultiMessage : IActionHandler() {
+    override suspend fun internalHandle(session: ActionSession): String {
+        val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
+        try {
+            val chatType = detailType?.let {
+                MessageHelper.obtainMessageTypeByDetailType(it)
+            } ?: run {
+                if (session.has("user_id")) {
+                    if (session.has("group_id")) {
+                        MsgConstant.KCHATTYPETEMPC2CFROMGROUP
+                    } else {
+                        MsgConstant.KCHATTYPEC2C
+                    }
+                } else if (session.has("group_id")) {
+                    MsgConstant.KCHATTYPEGROUP
+                } else {
+                    return noParam("detail_type/message_type", session.echo)
+                }
+            }
+            val peerId = when (chatType) {
+                MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam(
+                    "group_id",
+                    session.echo
+                )
+
+                MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id")
+                    ?: return noParam("user_id", session.echo)
+
+                else -> error("unknown chat type: $chatType")
+            }.toString()
+            val fromId = when (chatType) {
+                MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id")
+                    ?: return noParam("group_id", session.echo)
+
+                MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam(
+                    "user_id",
+                    session.echo
+                )
+
+                else -> error("unknown chat type: $chatType")
+            }.toString()
+            val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
+            return if (session.isArray("messages")) {
+                val messages = session.getArray("messages")
+                invoke(chatType, peerId, fromId, messages, retryCnt, echo = session.echo)
+            } else {
+                logic("未知格式合并转发消息", session.echo)
+            }
+        } catch (e: ParamsException) {
+            return noParam(e.message!!, session.echo)
+        } catch (e: Throwable) {
+            return logic(e.message ?: e.toString(), session.echo)
+        }
+    }
+
+    suspend operator fun invoke(
+        chatType: Int,
+        peerId: String,
+        fromId: String = peerId,
+        messages: JsonArray,
+        retryCnt: Int,
+        echo: JsonElement = EmptyJsonString
+    ): String {
+        kotlin.runCatching {
+            val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt)
+                .getOrElse { return logic(it.message ?: "", echo) }
+
+            return ok(
+                UploadForwardMessageResult(
+                    resId = message.data["id"] as String,
+                    filename = message.data["filename"] as String,
+                    summary = message.data["summary"] as String,
+                    desc = message.data["desc"] as String
+                ), echo = echo
+            )
+        }.onFailure {
+            return error("合并转发消息失败: $it", echo)
+        }
+        return logic("合并转发消息失败(unknown error)", echo)
+    }
+
+    override val requiredParams: Array<String> = arrayOf("messages")
+}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt
index 5470606b..694fcb9f 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt
@@ -44,49 +44,6 @@ fun Routing.messageAction() {
         }
     }
 
-    route("/send_group_forward_(msg|message)".toRegex()) {
-        post {
-            val groupId = fetchPostOrNull("group_id")
-            val messages = fetchPostJsonArray("messages")
-            call.respondText(
-                SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId ?: "", messages),
-                ContentType.Application.Json
-            )
-        }
-        get {
-            respond(false, Status.InternalHandlerError, "Not support GET method")
-        }
-    }
-
-    route("/send_private_forward_(msg|message)".toRegex()) {
-        post {
-            val userId = fetchPostOrNull("user_id")
-            val messages = fetchPostJsonArray("messages")
-            call.respondText(
-                SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: "", messages),
-                ContentType.Application.Json
-            )
-        }
-        get {
-            respond(false, Status.InternalHandlerError, "Not support GET method")
-        }
-    }
-
-    route("/send_forward_(msg|message)".toRegex()) {
-        post {
-            val userId = fetchPostOrNull("user_id")
-            val groupId = fetchPostOrNull("group_id")
-            val messages = fetchPostJsonArray("messages")
-            call.respondText(
-                SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: groupId ?: "", messages),
-                ContentType.Application.Json
-            )
-        }
-        get {
-            respond(false, Status.InternalHandlerError, "Not support GET method")
-        }
-    }
-
     getOrPost("/get_forward_msg") {
         val id = fetchOrThrow("id")
         call.respondText(GetForwardMsg(id), ContentType.Application.Json)
@@ -144,7 +101,7 @@ fun Routing.messageAction() {
         get {
             val msgType = fetchGetOrThrow("message_type")
             val message = fetchGetOrThrow("message")
-            val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3
+            val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 5
             val autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false
             val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
 
@@ -288,11 +245,11 @@ fun Routing.messageAction() {
         post {
             val userId = fetchPostOrThrow("user_id")
             val groupId = fetchPostOrNull("group_id")
-            val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
-            val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
 
             val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
-            val fromId = groupId ?: userId
+
+            val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
+            val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
             val recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull()
 
             val result = if (isJsonData()) {
@@ -302,8 +259,9 @@ fun Routing.messageAction() {
                         peerId = userId,
                         message = fetchPostJsonString("message"),
                         autoEscape = autoEscape,
-                        fromId = fromId,
-                        retryCnt = retryCnt, recallDuration = recallDuration
+                        fromId = groupId ?: userId,
+                        retryCnt = retryCnt,
+                        recallDuration = recallDuration
                     )
                 } else {
                     SendMessage(
@@ -312,7 +270,7 @@ fun Routing.messageAction() {
                         message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray(
                             "message"
                         ),
-                        fromId = groupId ?: userId ?: "",
+                        fromId = groupId ?: userId,
                         retryCnt = retryCnt,
                         recallDuration = recallDuration
                     )
@@ -323,12 +281,96 @@ fun Routing.messageAction() {
                     peerId = userId,
                     message = fetchPostOrThrow("message"),
                     autoEscape = autoEscape,
-                    fromId = fromId,
-                    retryCnt = retryCnt, recallDuration = recallDuration
+                    fromId = groupId ?: userId,
+                    retryCnt = retryCnt,
+                    recallDuration = recallDuration
                 )
             }
 
             call.respondText(result, ContentType.Application.Json)
         }
     }
+
+    route("/upload_multi_(msg|message)".toRegex()) {
+        post {
+            val msgType = fetchPostOrThrow("message_type")
+            val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
+            val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
+
+            val userId = fetchPostOrNull("user_id")
+            val groupId = fetchPostOrNull("group_id")
+            val messages = fetchPostJsonArray("messages")
+            call.respondText(
+                UploadMultiMessage(
+                    chatType,
+                    if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
+                    groupId ?: userId ?: "",
+                    messages,
+                    retryCnt
+                ),
+                ContentType.Application.Json
+            )
+        }
+        get {
+            respond(false, Status.InternalHandlerError, "Not support GET method")
+        }
+    }
+
+    route("/send_forward_(msg|message)".toRegex()) {
+        post {
+            val msgType = fetchPostOrThrow("message_type")
+            val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
+            val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
+
+            val userId = fetchPostOrNull("user_id")
+            val groupId = fetchPostOrNull("group_id")
+            val messages = fetchPostJsonArray("messages")
+            call.respondText(
+                SendForwardMessage(
+                    chatType,
+                    if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
+                    groupId ?: userId ?: "",
+                    messages,
+                    retryCnt
+                ),
+                ContentType.Application.Json
+            )
+        }
+        get {
+            respond(false, Status.InternalHandlerError, "Not support GET method")
+        }
+    }
+
+    route("/send_private_forward_(msg|message)".toRegex()) {
+        post {
+            val userId = fetchPostOrThrow("user_id")
+            val groupId = fetchPostOrNull("group_id")
+
+            val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
+            val messages = fetchPostJsonArray("messages")
+            call.respondText(
+                SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, groupId ?: userId, messages, retryCnt),
+                ContentType.Application.Json
+            )
+        }
+        get {
+            respond(false, Status.InternalHandlerError, "Not support GET method")
+        }
+    }
+
+    route("/send_group_forward_(msg|message)".toRegex()) {
+        post {
+            val groupId = fetchPostOrThrow("group_id")
+
+            val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
+            val messages = fetchPostJsonArray("messages")
+            call.respondText(
+                SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages = messages, retryCnt = retryCnt),
+                ContentType.Application.Json
+            )
+        }
+        get {
+            respond(false, Status.InternalHandlerError, "Not support GET method")
+        }
+    }
 }
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt
index 7dca35c6..91937299 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt
@@ -9,10 +9,20 @@ internal data class MessageResult(
     @SerialName("message_id") val msgId: Int,
     @SerialName("time") val time: Long
 )
+
+@Serializable
+internal data class UploadForwardMessageResult(
+    @SerialName("res_id") val resId: String,
+    @SerialName("filename") val filename: String,
+    @SerialName("summary") val summary: String,
+    @SerialName("desc") val desc: String,
+)
+
 @Serializable
-internal data class ForwardMessageResult(
+internal data class SendForwardMessageResult(
     @SerialName("message_id") val msgId: Int,
-    @SerialName("forward_id") val forwardId: String
+    @SerialName("res_id") val resId: String,
+    @SerialName("forward_id") val forwardId: String = resId
 )
 
 @Serializable
@@ -20,6 +30,7 @@ internal data class MessageDetail(
     @SerialName("time") val time: Int,
     @SerialName("message_type") val msgType: String,
     @SerialName("message_id") val msgId: Int,
+    @SerialName("message_id_qq") val qqMsgId: Long,
     @SerialName("message_seq") val msgSeq: Long,
     @SerialName("real_id") val realId: Long,
     @SerialName("sender") val sender: MessageSender,
@@ -29,6 +40,11 @@ internal data class MessageDetail(
     @SerialName("target_id") val targetId: Long = 0,
 )
 
+@Serializable
+internal data class GetForwardMsgResult(
+    @SerialName("messages") val msgs: List<MessageDetail>
+)
+
 @Serializable
 internal data class MessageSender(
     @SerialName("user_id") val userId: Long,
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt
index 987b49b2..1c5d4bbb 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt
@@ -69,7 +69,6 @@ internal object PrimitiveListener {
                 528 -> when (subType) {
                     35 -> onFriendApply(msgTime, push.clientInfo!!, body)
                     39 -> onCardChange(msgTime, body)
-                    // invite
                     68 -> onGroupApply(msgTime, contentHead, body)
                     138 -> onC2CRecall(msgTime, body)
                     290 -> onC2CPoke(msgTime, body)
@@ -79,7 +78,7 @@ internal object PrimitiveListener {
                     12 -> onGroupBan(msgTime, body)
                     16 -> onGroupUniqueTitleChange(msgTime, body)
                     17 -> onGroupRecall(msgTime, body)
-                    20 -> onGroupPokeAndGroupSign(msgTime, body)
+                    20 -> onGroupCommonTips(msgTime, body)
                     21 -> onEssenceMessage(msgTime, push.clientInfo, body)
                 }
             }
@@ -281,7 +280,7 @@ internal object PrimitiveListener {
     }
 
 
-    private suspend fun onGroupPokeAndGroupSign(time: Long, body: MsgBody) {
+    private suspend fun onGroupCommonTips(time: Long, body: MsgBody) {
         val event = runCatching {
             body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>()
         }.getOrElse {
diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt
index 7e202750..f04a36a7 100644
--- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt
+++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt
@@ -52,6 +52,7 @@ val Collection<Any?>.json: JsonArray
                     is Number -> arrayList.add(it.json)
                     is String -> arrayList.add(it.json)
                     is Boolean -> arrayList.add(it.json)
+                    is Pair<*, *> -> arrayList.add(mapOf(it as Pair<String, Any?>).json)
                     is Map<*, *> -> arrayList.add((it as Map<String, Any?>).json)
                     is Collection<*> -> arrayList.add((it as Collection<Any?>).json)
                     else -> error("unknown array type: ${it::class.java}")