diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt
index 0a5f6d91c..919b13ae3 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt
@@ -43,6 +43,9 @@ private constructor(val target: Long, val display: String) :
     override fun contentToString(): String = this.display
 
     companion object Key : Message.Key<At> {
+        override val typeName: String
+            get() = "At"
+
         /**
          * 构造一个 [At], 仅供内部使用, 否则可能造成消息无法发出的问题.
          */
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt
index 3357a5b51..1df5075bd 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt
@@ -33,6 +33,8 @@ object AtAll :
 
     @SinceMirai("0.31.2")
     const val display = displayA
+    override val typeName: String
+        get() = "AtAll"
 
     @Suppress("SpellCheckingInspection")
     override fun toString(): String = "[mirai:atall]"
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt
index 9f0ed43c5..3f60b9fd0 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Face.kt
@@ -32,6 +32,9 @@ class Face private constructor(val id: Int, private val stringValue: String) :
      */
     @Suppress("SpellCheckingInspection", "unused")
     companion object IdList : Message.Key<Face> {
+        override val typeName: String
+            get() = "Face"
+
         const val unknown: Int = 0xff
         const val jingya: Int = 0
         const val piezui: Int = 1
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt
index 76d36985b..5db564603 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt
@@ -27,7 +27,10 @@ import kotlin.jvm.JvmSynthetic
  */
 @SinceMirai("0.31.0")
 sealed class HummerMessage : MessageContent {
-    companion object Key : Message.Key<HummerMessage>
+    companion object Key : Message.Key<HummerMessage> {
+        override val typeName: String
+            get() = "HummerMessage"
+    }
     // has service type etc.
 }
 
@@ -47,6 +50,9 @@ class PokeMessage @MiraiInternalAPI(message = "使用伴生对象中的常量")
     val id: Int
 ) : HummerMessage() {
     companion object Types : Message.Key<PokeMessage> {
+        override val typeName: String
+            get() = "PokeMessage"
+
         /** 戳一戳 */
         @JvmField
         val Poke = PokeMessage(1, -1)
@@ -130,6 +136,9 @@ sealed class FlashImage : MessageContent, HummerMessage() {
         operator fun invoke(imageId: String): FlashImage {
             return invoke(Image(imageId))
         }
+
+        override val typeName: String
+            get() = "FlashImage"
     }
 
     /**
@@ -170,7 +179,10 @@ inline fun FriendImage.flash(): FriendFlashImage = FlashImage(this) as FriendFla
  */
 @SinceMirai("0.33.0")
 class GroupFlashImage(override val image: GroupImage) : FlashImage() {
-    companion object Key : Message.Key<GroupFlashImage>
+    companion object Key : Message.Key<GroupFlashImage> {
+        override val typeName: String
+            get() = "GroupFlashImage"
+    }
 }
 
 /**
@@ -178,5 +190,8 @@ class GroupFlashImage(override val image: GroupImage) : FlashImage() {
  */
 @SinceMirai("0.33.0")
 class FriendFlashImage(override val image: FriendImage) : FlashImage() {
-    companion object Key : Message.Key<FriendFlashImage>
+    companion object Key : Message.Key<FriendFlashImage> {
+        override val typeName: String
+            get() = "FriendFlashImage"
+    }
 }
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
index 20785d8d8..a680235c7 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
@@ -33,7 +33,10 @@ import kotlin.jvm.JvmSynthetic
  * @see Image.flash 转换普通图片为闪照
  */
 interface Image : Message, MessageContent {
-    companion object Key : Message.Key<Image>
+    companion object Key : Message.Key<Image> {
+        override val typeName: String
+            get() = "Image"
+    }
 
     /**
      * 图片的 id.
@@ -92,7 +95,10 @@ sealed class AbstractImage : Image {
  * 一般由 [Contact.uploadImage] 得到
  */
 interface OnlineImage : Image {
-    companion object Key : Message.Key<OnlineImage>
+    companion object Key : Message.Key<OnlineImage> {
+        override val typeName: String
+            get() = "OnlineImage"
+    }
 
     /**
      * 原图下载链接. 包含域名
@@ -124,7 +130,10 @@ suspend fun Image.queryUrl(): String {
  * 一般由 [Contact.uploadImage] 得到
  */
 interface OfflineImage : Image {
-    companion object Key : Message.Key<OfflineImage>
+    companion object Key : Message.Key<OfflineImage> {
+        override val typeName: String
+            get() = "OfflineImage"
+    }
 }
 
 /**
@@ -149,7 +158,10 @@ suspend fun OfflineImage.queryUrl(): String {
 // CustomFace
 @OptIn(MiraiInternalAPI::class)
 sealed class GroupImage : AbstractImage() {
-    companion object Key : Message.Key<GroupImage>
+    companion object Key : Message.Key<GroupImage> {
+        override val typeName: String
+            get() = "GroupImage"
+    }
 
     abstract val filepath: String
     abstract val fileId: Int
@@ -222,7 +234,10 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage
  */ // NotOnlineImage
 @OptIn(MiraiInternalAPI::class)
 sealed class FriendImage : AbstractImage() {
-    companion object Key : Message.Key<FriendImage>
+    companion object Key : Message.Key<FriendImage> {
+        override val typeName: String
+            get() = "FriendImage"
+    }
 
     abstract val resourceId: String
     abstract val md5: ByteArray
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt
index 03dc6410d..9a2222a4c 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt
@@ -26,9 +26,9 @@ import kotlin.jvm.JvmSynthetic
  * - [MessageMetadata] 消息元数据, 包括: [消息来源][MessageSource]
  * - [MessageContent] 单个消息, 包括: [纯文本][PlainText], [@群员][At], [@全体成员][AtAll] 等.
  * - [CombinedMessage] 通过 [plus] 连接的两个消息. 可通过 [asMessageChain] 转换为 [MessageChain]
- * - [MessageChain] 不可变消息链, 即 [List] 形式链接的多个 [Message] 实例.
+ * - [MessageChain] 不可变消息链, 链表形式链接的多个 [SingleMessage] 实例.
  *
- * **在 Kotlin 使用 [Message]**
+ * #### 在 Kotlin 使用 [Message]:
  * 这与使用 [String] 的使用非常类似.
  *
  * 比较 [Message] 与 [String] (使用 infix [Message.eq]):
@@ -72,7 +72,13 @@ interface Message {
      *
      * @param M 指代持有这个 Key 的消息类型
      */
-    interface Key<out M : Message>
+    interface Key<out M : Message> {
+        /**
+         * 此 [Key] 指代的 [Message] 类型名. 一般为 `class.simpleName`
+         */
+        @SinceMirai("0.34.0")
+        val typeName: String
+    }
 
     infix fun eq(other: Message): Boolean = this.toString() == other.toString()
 
@@ -105,7 +111,7 @@ interface Message {
         if (this is ConstrainSingle<*> && tail is ConstrainSingle<*>
             && this.key == tail.key
         ) {
-            return CombinedMessage(EmptyMessageChain, this)
+            return CombinedMessage(EmptyMessageChain, tail)
         }
         return CombinedMessage(left = this, tail = tail)
     }
@@ -119,6 +125,8 @@ interface Message {
      * [FriendImage]: "[mirai:image:/f8f1ab55-bf8e-4236-b55e-955848d7069f]"
      * [PokeMessage]: "[mirai:poke:1,-1]"
      * [MessageChain]: 直接无间隔地连接所有元素.
+     *
+     * @see contentToString
      */
     override fun toString(): String
 
@@ -141,17 +149,23 @@ interface Message {
     operator fun plus(another: CharSequence): CombinedMessage = this.followedBy(another.toString().toMessage())
 }
 
+@JvmSynthetic
 @Suppress("UNCHECKED_CAST")
 suspend inline fun <C : Contact> Message.sendTo(contact: C): MessageReceipt<C> {
     return contact.sendMessage(this) as MessageReceipt<C>
 }
 
 fun Message.repeat(count: Int): MessageChain {
+    if (this is ConstrainSingle<*>) {
+        // fast-path
+        return SingleMessageChainImpl(this)
+    }
     return buildMessageChain(count) {
         add(this@repeat)
     }
 }
 
+@JvmSynthetic
 inline operator fun Message.times(count: Int): MessageChain = this.repeat(count)
 
 interface SingleMessage : Message, CharSequence, Comparable<String>
@@ -168,13 +182,13 @@ interface MessageMetadata : SingleMessage {
 }
 
 /**
- * 约束一个 [MessageChain] 中只存在这一种类型的元素. 新元素将会替换旧元素, 但不会保持原顺序.
+ * 约束一个 [MessageChain] 中只存在这一种类型的元素. 新元素将会替换旧元素, 保持原顺序.
  *
  * **MiraiExperimentalAPI**: 此 API 可能在将来版本修改
  */
 @SinceMirai("0.34.0")
 @MiraiExperimentalAPI
-interface ConstrainSingle<M : Message> {
+interface ConstrainSingle<M : Message> : SingleMessage {
     val key: Message.Key<M>
 }
 
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
index 457706a46..17d5c595f 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
@@ -26,7 +26,7 @@ import kotlin.reflect.KProperty
 
 /**
  * 消息链.
- * 它的一般实现为 [MessageChainImplByIterable] 或 [MessageChainImplBySequence],
+ * 它的一般实现为 [MessageChainImplByCollection] 或 [MessageChainImplBySequence],
  * 替代 `null` 情况的实现为 [NullMessageChain],
  * 空的实现为 [EmptyMessageChain]
  *
@@ -170,7 +170,7 @@ fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key)
 @JvmSynthetic
 @Suppress("UNCHECKED_CAST")
 inline fun <M : Message> MessageChain.first(key: Message.Key<M>): M =
-    firstOrNull(key) ?: throw NoSuchElementException("no such element: $key")
+    firstOrNull(key) ?: throw NoSuchElementException("Message type $key not found in chain $this")
 
 /**
  * 获取第一个 [M] 类型的 [Message] 实例
@@ -264,7 +264,8 @@ fun Message.asMessageChain(): MessageChain = when (this) {
  * 直接将 [this] 委托为一个 [MessageChain]
  */
 @JvmSynthetic
-inline fun Collection<SingleMessage>.asMessageChain(): MessageChain = MessageChainImplByCollection(this)
+fun Collection<SingleMessage>.asMessageChain(): MessageChain =
+    MessageChainImplByCollection(this.constrainSingleMessages())
 
 /**
  * 将 [this] [扁平化后][flatten] 委托为一个 [MessageChain]
@@ -277,7 +278,8 @@ inline fun Collection<Message>.asMessageChain(): MessageChain = MessageChainImpl
  * 直接将 [this] 委托为一个 [MessageChain]
  */
 @JvmSynthetic
-inline fun Iterable<SingleMessage>.asMessageChain(): MessageChain = MessageChainImplByIterable(this)
+fun Iterable<SingleMessage>.asMessageChain(): MessageChain =
+    MessageChainImplByCollection(this.constrainSingleMessages())
 
 @JvmSynthetic
 inline fun MessageChain.asMessageChain(): MessageChain = this // 避免套娃
@@ -386,6 +388,7 @@ fun Message.flatten(): Sequence<SingleMessage> {
     }
 }
 
+@JvmSynthetic // make Java user happier with less methods
 fun CombinedMessage.flatten(): Sequence<SingleMessage> {
     // already constrained single.
     if (this.isFlat()) {
@@ -394,6 +397,7 @@ fun CombinedMessage.flatten(): Sequence<SingleMessage> {
     } else return this.asSequence().flatten()
 }
 
+@JvmSynthetic // make Java user happier with less methods
 inline fun MessageChain.flatten(): Sequence<SingleMessage> = this.asSequence() // fast path
 
 // endregion converters
@@ -402,7 +406,15 @@ inline fun MessageChain.flatten(): Sequence<SingleMessage> = this.asSequence() /
 /**
  * 不含任何元素的 [MessageChain]
  */
-object EmptyMessageChain : MessageChain by MessageChainImplByCollection(emptyList())
+object EmptyMessageChain : MessageChain, Iterator<SingleMessage> {
+    override fun contains(sub: String): Boolean = sub.isEmpty()
+    override val size: Int get() = 0
+    override fun toString(): String = ""
+    override fun contentToString(): String = ""
+    override fun iterator(): Iterator<SingleMessage> = this
+    override fun hasNext(): Boolean = false
+    override fun next(): SingleMessage = throw NoSuchElementException("EmptyMessageChain is empty.")
+}
 
 /**
  * Null 的 [MessageChain].
@@ -427,22 +439,45 @@ object NullMessageChain : MessageChain {
 // region implementations
 
 
-/**
- * 使用 [Iterable] 作为委托的 [MessageChain]
- */
-@PublishedApi
-internal class MessageChainImplByIterable constructor(
-    private val delegate: Iterable<SingleMessage>
-) : Message, Iterable<SingleMessage>, MessageChain {
-    override val size: Int by lazy { delegate.count() }
-    override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
-    private var toStringTemp: String? = null
-    override fun toString(): String =
-        toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it }
+@Suppress("DuplicatedCode") // we don't have pattern matching
+@OptIn(MiraiExperimentalAPI::class)
+internal fun Sequence<SingleMessage>.constrainSingleMessages(): List<SingleMessage> {
+    val list = ArrayList<SingleMessage>(4)
+    val singleList = ArrayList<Message.Key<*>?>(4)
 
-    override fun contentToString(): String = toString()
+    for (singleMessage in this) {
+        if (singleMessage is ConstrainSingle<*>) {
+            val key = singleMessage.key
+            val index = singleList.indexOf(key)
+            if (index != -1) {
+                list[index] = singleMessage
+                continue
+            } else {
+                singleList.add(list.size, key)
+            }
+        }
+        list.add(singleMessage)
+    }
+    return list
+}
 
-    override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
+@Suppress("DuplicatedCode") // we don't have pattern matching
+@OptIn(MiraiExperimentalAPI::class)
+internal fun Iterable<SingleMessage>.constrainSingleMessages(): List<SingleMessage> {
+    val list = ArrayList<SingleMessage>()
+
+    for (singleMessage in this) {
+        if (singleMessage is ConstrainSingle<*>) {
+            val key = singleMessage.key
+            val index = list.indexOfFirst { it is ConstrainSingle<*> && it.key == key }
+            if (index != -1) {
+                list[index] = singleMessage
+                continue
+            }
+        }
+        list.add(singleMessage)
+    }
+    return list
 }
 
 /**
@@ -450,7 +485,7 @@ internal class MessageChainImplByIterable constructor(
  */
 @PublishedApi
 internal class MessageChainImplByCollection constructor(
-    private val delegate: Collection<SingleMessage>
+    private val delegate: Collection<SingleMessage> // 必须 constrainSingleMessages, 且为 immutable
 ) : Message, Iterable<SingleMessage>, MessageChain {
     override val size: Int get() = delegate.size
     override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
@@ -458,7 +493,10 @@ internal class MessageChainImplByCollection constructor(
     override fun toString(): String =
         toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it }
 
-    override fun contentToString(): String = toString()
+    private var contentToStringTemp: String? = null
+    override fun contentToString(): String =
+        contentToStringTemp ?: this.delegate.joinToString("") { it.contentToString() }.also { contentToStringTemp = it }
+
 
     override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
 }
@@ -468,20 +506,24 @@ internal class MessageChainImplByCollection constructor(
  */
 @PublishedApi
 internal class MessageChainImplBySequence constructor(
-    delegate: Sequence<SingleMessage>
+    delegate: Sequence<SingleMessage> // 可以有重复 ConstrainSingle
 ) : Message, Iterable<SingleMessage>, MessageChain {
     override val size: Int by lazy { collected.size }
 
     /**
      * [Sequence] 可能只能消耗一遍, 因此需要先转为 [List]
      */
-    private val collected: List<SingleMessage> by lazy { delegate.toList() }
+    private val collected: List<SingleMessage> by lazy { delegate.constrainSingleMessages() }
     override fun iterator(): Iterator<SingleMessage> = collected.iterator()
     private var toStringTemp: String? = null
     override fun toString(): String =
         toStringTemp ?: this.collected.joinToString("") { it.toString() }.also { toStringTemp = it }
 
-    override fun contentToString(): String = toString()
+    private var contentToStringTemp: String? = null
+    override fun contentToString(): String =
+        contentToStringTemp ?: this.collected.joinToString("") { it.contentToString() }
+            .also { contentToStringTemp = it }
+
 
     override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) }
 }
@@ -495,7 +537,7 @@ internal class SingleMessageChainImpl constructor(
 ) : Message, Iterable<SingleMessage>, MessageChain {
     override val size: Int get() = 1
     override fun toString(): String = this.delegate.toString()
-    override fun contentToString(): String = this.delegate.toString()
+    override fun contentToString(): String = this.delegate.contentToString()
     override fun iterator(): Iterator<SingleMessage> = iterator { yield(delegate) }
     override operator fun contains(sub: String): Boolean = sub in delegate
 }
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
index 71873a84e..ec8b63724 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
@@ -42,7 +42,10 @@ import kotlin.jvm.JvmSynthetic
  */
 @SinceMirai("0.33.0")
 sealed class MessageSource : Message, MessageMetadata {
-    companion object Key : Message.Key<MessageSource>
+    companion object Key : Message.Key<MessageSource> {
+        override val typeName: String
+            get() = "MessageSource"
+    }
 
     /**
      * 所属 [Bot]
@@ -105,7 +108,10 @@ sealed class MessageSource : Message, MessageMetadata {
 @SinceMirai("0.33.0")
 @OptIn(MiraiExperimentalAPI::class)
 sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessageSource> {
-    companion object Key : Message.Key<OnlineMessageSource>
+    companion object Key : Message.Key<OnlineMessageSource> {
+        override val typeName: String
+            get() = "OnlineMessageSource"
+    }
 
     override val key: Message.Key<OnlineMessageSource> get() = Key
 
@@ -131,7 +137,10 @@ sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessag
      * 由 [机器人主动发送消息][Contact.sendMessage] 产生的 [MessageSource]
      */
     sealed class Outgoing : OnlineMessageSource() {
-        companion object Key : Message.Key<Outgoing>
+        companion object Key : Message.Key<Outgoing> {
+            override val typeName: String
+                get() = "OnlineMessageSource.Outgoing"
+        }
 
         abstract override val sender: Bot
         abstract override val target: Contact
@@ -140,7 +149,10 @@ sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessag
         final override val targetId: Long get() = target.id
 
         abstract class ToFriend : Outgoing() {
-            companion object Key : Message.Key<ToFriend>
+            companion object Key : Message.Key<ToFriend> {
+                override val typeName: String
+                    get() = "OnlineMessageSource.Outgoing.ToFriend"
+            }
 
             abstract override val target: QQ
             final override val subject: QQ get() = target
@@ -148,7 +160,10 @@ sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessag
         }
 
         abstract class ToGroup : Outgoing() {
-            companion object Key : Message.Key<ToGroup>
+            companion object Key : Message.Key<ToGroup> {
+                override val typeName: String
+                    get() = "OnlineMessageSource.Outgoing.ToGroup"
+            }
 
             abstract override val target: Group
             final override val subject: Group get() = target
@@ -160,7 +175,10 @@ sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessag
      * 接收到的一条消息的 [MessageSource]
      */
     sealed class Incoming : OnlineMessageSource() {
-        companion object Key : Message.Key<Incoming>
+        companion object Key : Message.Key<Incoming> {
+            override val typeName: String
+                get() = "OnlineMessageSource.Incoming"
+        }
 
         abstract override val sender: QQ // out QQ
         abstract override val target: Bot
@@ -169,7 +187,10 @@ sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessag
         final override val targetId: Long get() = target.id
 
         abstract class FromFriend : Incoming() {
-            companion object Key : Message.Key<FromFriend>
+            companion object Key : Message.Key<FromFriend> {
+                override val typeName: String
+                    get() = "OnlineMessageSource.Incoming.FromFriend"
+            }
 
             abstract override val sender: QQ
             final override val subject: QQ get() = sender
@@ -177,7 +198,10 @@ sealed class OnlineMessageSource : MessageSource(), ConstrainSingle<OnlineMessag
         }
 
         abstract class FromGroup : Incoming() {
-            companion object Key : Message.Key<FromGroup>
+            companion object Key : Message.Key<FromGroup> {
+                override val typeName: String
+                    get() = "OnlineMessageSource.Incoming.FromGroup"
+            }
 
             abstract override val sender: Member
             final override val subject: Group get() = sender.group
@@ -224,7 +248,10 @@ inline fun MessageSource.recallIn(
  */
 @SinceMirai("0.33.0")
 abstract class OfflineMessageSource : MessageSource() {
-    companion object Key : Message.Key<OfflineMessageSource>
+    companion object Key : Message.Key<OfflineMessageSource> {
+        override val typeName: String
+            get() = "OfflineMessageSource"
+    }
 
     enum class Kind {
         GROUP,
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt
index 3ce2b2661..11ec8289c 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt
@@ -42,7 +42,10 @@ class PlainText(val stringValue: String) :
         return stringValue.hashCode()
     }
 
-    companion object Key : Message.Key<PlainText>
+    companion object Key : Message.Key<PlainText> {
+        override val typeName: String
+            get() = "PlainText"
+    }
 }
 
 /**
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/QuoteReply.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/QuoteReply.kt
index 6c6310ae1..e19fce819 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/QuoteReply.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/QuoteReply.kt
@@ -21,6 +21,8 @@ import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmSynthetic
 
 
 /**
@@ -34,8 +36,10 @@ import kotlin.jvm.JvmName
 @OptIn(MiraiExperimentalAPI::class)
 @SinceMirai("0.33.0")
 class QuoteReply(val source: MessageSource) : Message, MessageMetadata, ConstrainSingle<QuoteReply> {
-    // TODO: 2020/4/4 Metadata or Content?
-    companion object Key : Message.Key<QuoteReply>
+    companion object Key : Message.Key<QuoteReply> {
+        override val typeName: String
+            get() = "QuoteReply"
+    }
 
     override val key: Message.Key<QuoteReply> get() = Key
 
@@ -43,8 +47,10 @@ class QuoteReply(val source: MessageSource) : Message, MessageMetadata, Constrai
     override fun contentToString(): String = ""
 }
 
+@JvmSynthetic
 suspend inline fun QuoteReply.recall() = this.source.recall()
 
+@JvmOverloads
 inline fun QuoteReply.recallIn(
     millis: Long,
     coroutineContext: CoroutineContext = EmptyCoroutineContext
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
index 8574d641d..e8159a540 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/RichMessage.kt
@@ -36,14 +36,6 @@ interface RichMessage : MessageContent {
     @SinceMirai("0.30.0")
     companion object Templates : Message.Key<RichMessage> {
 
-        /**
-         * 合并转发.
-         */
-        @MiraiExperimentalAPI
-        fun mergedForward(): Nothing {
-            TODO()
-        }
-
         /**
          * 长消息.
          *
@@ -100,6 +92,9 @@ interface RichMessage : MessageContent {
                     }
                 }
             }
+
+        override val typeName: String
+            get() = "RichMessage"
     }
 
     val content: String
@@ -118,7 +113,10 @@ interface RichMessage : MessageContent {
 @SinceMirai("0.27.0")
 @OptIn(MiraiExperimentalAPI::class)
 class JsonMessage(override val content: String) : RichMessage {
-    companion object Key : Message.Key<JsonMessage>
+    companion object Key : Message.Key<JsonMessage> {
+        override val typeName: String
+            get() = "JsonMessage"
+    }
 
     // serviceId = 1
     override fun toString(): String = "[mirai:json:$content]"
@@ -133,7 +131,10 @@ class JsonMessage(override val content: String) : RichMessage {
 @SinceMirai("0.27.0")
 class LightApp constructor(override val content: String) : RichMessage {
 
-    companion object Key : Message.Key<LightApp>
+    companion object Key : Message.Key<LightApp> {
+        override val typeName: String
+            get() = "LightApp"
+    }
 
     override fun toString(): String = "[mirai:app:$content]"
 }
@@ -147,7 +148,10 @@ class LightApp constructor(override val content: String) : RichMessage {
 @SinceMirai("0.27.0")
 @OptIn(MiraiExperimentalAPI::class)
 class XmlMessage constructor(override val content: String) : RichMessage {
-    companion object Key : Message.Key<XmlMessage>
+    companion object Key : Message.Key<XmlMessage> {
+        override val typeName: String
+            get() = "XmlMessage"
+    }
 
     // override val serviceId: Int get() = 60
 
@@ -161,7 +165,10 @@ class XmlMessage constructor(override val content: String) : RichMessage {
 @MiraiExperimentalAPI
 @MiraiInternalAPI
 class LongMessage(override val content: String, val resId: String) : RichMessage {
-    companion object Key : Message.Key<XmlMessage>
+    companion object Key : Message.Key<XmlMessage> {
+        override val typeName: String
+            get() = "LongMessage"
+    }
 
     // serviceId = 35
     override fun toString(): String = "[mirai:long:$content]"
diff --git a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt
new file mode 100644
index 000000000..f9c44fc1b
--- /dev/null
+++ b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.message.data
+
+import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertSame
+
+
+internal class ConstrainSingleTest {
+
+    @OptIn(MiraiExperimentalAPI::class)
+    internal class TestMessage : ConstrainSingle<TestMessage>, Any() {
+        companion object Key : Message.Key<TestMessage> {
+            override val typeName: String
+                get() = "TestMessage"
+        }
+
+        override fun toString(): String = super.toString()
+
+        override fun contentToString(): String {
+            TODO("Not yet implemented")
+        }
+
+        override val key: Message.Key<TestMessage>
+            get() = Key
+        override val length: Int
+            get() = TODO("Not yet implemented")
+
+        override fun get(index: Int): Char {
+            TODO("Not yet implemented")
+        }
+
+        override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
+            TODO("Not yet implemented")
+        }
+
+        override fun compareTo(other: String): Int {
+            TODO("Not yet implemented")
+        }
+    }
+
+    @OptIn(MiraiExperimentalAPI::class)
+    @Test
+    fun testConstrainSingleInPlus() {
+        val new = TestMessage()
+        val combined = TestMessage() + new
+
+        assertEquals(combined.left, EmptyMessageChain)
+        assertSame(combined.tail, new)
+    }
+
+    @Test // net.mamoe.mirai/message/data/MessageChain.kt:441
+    fun testConstrainSingleInSequence() {
+        val last = TestMessage()
+        val sequence: Sequence<SingleMessage> = sequenceOf(
+            TestMessage(),
+            TestMessage(),
+            last
+        )
+
+        val result = sequence.constrainSingleMessages()
+        assertEquals(result.count(), 1)
+        assertSame(result.single(), last)
+    }
+
+    @Test // net.mamoe.mirai/message/data/MessageChain.kt:441
+    fun testConstrainSingleOrderInSequence() {
+        val last = TestMessage()
+        val sequence: Sequence<SingleMessage> = sequenceOf(
+            TestMessage(), // last should replace here
+            PlainText("test"),
+            TestMessage(),
+            last
+        )
+
+        val result = sequence.constrainSingleMessages()
+        assertEquals(result.count(), 2)
+        assertSame(result.first(), last)
+    }
+}
\ No newline at end of file