diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api
index 872d255f3..366d22631 100644
--- a/binary-compatibility-validator/api/binary-compatibility-validator.api
+++ b/binary-compatibility-validator/api/binary-compatibility-validator.api
@@ -4661,6 +4661,41 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun toPlainText (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/PlainText;
 }
 
+public final class net/mamoe/mirai/message/data/MusicShare : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent {
+	public static final field Key Lnet/mamoe/mirai/message/data/MusicShare$Key;
+	public fun <init> (Lnet/mamoe/mirai/message/data/MusicType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+	public fun <init> (Lnet/mamoe/mirai/message/data/MusicType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+	public synthetic fun <init> (Lnet/mamoe/mirai/message/data/MusicType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public fun contentToString ()Ljava/lang/String;
+	public fun equals (Ljava/lang/Object;)Z
+	public final fun getBrief ()Ljava/lang/String;
+	public final fun getJumpUrl ()Ljava/lang/String;
+	public fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
+	public final fun getMusicUrl ()Ljava/lang/String;
+	public final fun getPictureUrl ()Ljava/lang/String;
+	public final fun getSummary ()Ljava/lang/String;
+	public final fun getTitle ()Ljava/lang/String;
+	public final fun getType ()Lnet/mamoe/mirai/message/data/MusicType;
+	public fun hashCode ()I
+	public fun toString ()Ljava/lang/String;
+}
+
+public final class net/mamoe/mirai/message/data/MusicShare$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+}
+
+public final class net/mamoe/mirai/message/data/MusicType : java/lang/Enum {
+	public static final field MiguMusic Lnet/mamoe/mirai/message/data/MusicType;
+	public static final field NeteaseCloudMusic Lnet/mamoe/mirai/message/data/MusicType;
+	public static final field QQMusic Lnet/mamoe/mirai/message/data/MusicType;
+	public final fun getAppId ()J
+	public final fun getPackageName ()Ljava/lang/String;
+	public final fun getPlatform ()I
+	public final fun getSdkVersion ()Ljava/lang/String;
+	public final fun getSignature ()Ljava/lang/String;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/MusicType;
+	public static fun values ()[Lnet/mamoe/mirai/message/data/MusicType;
+}
+
 public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/mamoe/mirai/message/data/MessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OfflineMessageSource$Key;
 	public fun <init> ()V
diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt
index 6cd9fe1cb..c1e8148bd 100644
--- a/buildSrc/src/main/kotlin/Versions.kt
+++ b/buildSrc/src/main/kotlin/Versions.kt
@@ -12,7 +12,7 @@
 import org.gradle.api.attributes.Attribute
 
 object Versions {
-    const val project = "2.1.0-dev-2"
+    const val project = "2.1.0-dev-3"
 
     const val kotlinCompiler = "1.4.21"
     const val kotlinStdlib = "1.4.21"
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt
new file mode 100644
index 000000000..a93403cee
--- /dev/null
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt
@@ -0,0 +1,132 @@
+/*
+ * 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
+ */
+
+@file:Suppress("unused")
+
+package net.mamoe.mirai.message.data
+
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+import net.mamoe.mirai.utils.MiraiInternalApi
+import net.mamoe.mirai.utils.safeCast
+
+/**
+ * QQ 互联通道音乐分享
+ *
+ * @since 2.1
+ */
+@MiraiExperimentalApi
+public class MusicShare @JvmOverloads constructor(
+    /**
+     * 音乐应用类型
+     */
+    public val type: MusicType,
+    /**
+     * 消息卡片标题
+     */
+    public val title: String,
+    /**
+     * 消息卡片内容
+     */
+    public val summary: String,
+    /**
+     * 点击卡片跳转网页 URL
+     */
+    public val jumpUrl: String,
+    /**
+     * 消息卡片图片 URL
+     */
+    public val pictureUrl: String,
+    /**
+     * 音乐文件 URL
+     */
+    public val musicUrl: String,
+    /**
+     * 在消息列表显示
+     */
+    public val brief: String = "[分享]$title",
+) : MessageContent, ConstrainSingle {
+
+    override val key: MessageKey<*> get() = Key
+
+    override fun contentToString(): String =
+        brief.takeIf { it.isNotBlank() } ?: "[分享]$title" // empty content is not accepted by `sendMessage`
+
+    // MusicShare(type=NeteaseCloudMusic, title='ファッション', summary='rinahamu/Yunomi', brief='', url='http://music.163.com/song/1338728297/?userid=324076307', pictureUrl='http://p2.music.126.net/y19E5SadGUmSR8SZxkrNtw==/109951163785855539.jpg', musicUrl='http://music.163.com/song/media/outer/url?id=1338728297&userid=324076307')
+    override fun toString(): String {
+        return "MusicShare(type=$type, title='$title', summary='$summary', brief='$brief', url='$jumpUrl', pictureUrl='$pictureUrl', musicUrl='$musicUrl')"
+    }
+
+
+    // don't make this class 'data' unless we made it stable.
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as MusicShare
+
+        if (type != other.type) return false
+        if (title != other.title) return false
+        if (summary != other.summary) return false
+        if (brief != other.brief) return false
+        if (jumpUrl != other.jumpUrl) return false
+        if (pictureUrl != other.pictureUrl) return false
+        if (musicUrl != other.musicUrl) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = type.hashCode()
+        result = 31 * result + title.hashCode()
+        result = 31 * result + summary.hashCode()
+        result = 31 * result + brief.hashCode()
+        result = 31 * result + jumpUrl.hashCode()
+        result = 31 * result + pictureUrl.hashCode()
+        result = 31 * result + musicUrl.hashCode()
+        return result
+    }
+
+
+    public companion object Key :
+        AbstractPolymorphicMessageKey<MessageContent, MusicShare>(MessageContent, { it.safeCast() })
+}
+
+/**
+ * @since 2.1
+ */
+public enum class MusicType constructor(
+    @MiraiInternalApi public val appId: Long,
+    @MiraiInternalApi public val platform: Int,
+    @MiraiInternalApi public val sdkVersion: String,
+    @MiraiInternalApi public val packageName: String,
+    @MiraiInternalApi public val signature: String
+) {
+    NeteaseCloudMusic(
+        100495085,
+        1,
+        "0.0.0",
+        "com.netease.cloudmusic",
+        "da6b069da1e2982db3e386233f68d76d"
+    ),
+    QQMusic(
+        100497308,
+        1,
+        "0.0.0",
+        "com.tencent.qqmusic",
+        "cbd27cd7c861227d013a25b2d10f0799"
+    ),
+    MiguMusic(
+        1101053067,
+        1,
+        "0.0.0",
+        "cmccwm.mobilemusic",
+        "6cdc72a439cef99a3418d2a78aa28c73"
+    )
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt
index 01ca79256..e4a98682f 100644
--- a/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt
@@ -17,13 +17,17 @@ import net.mamoe.mirai.contact.MessageTooLargeException
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.EventCancelledException
 import net.mamoe.mirai.event.events.GroupMessagePreSendEvent
+import net.mamoe.mirai.event.nextEventOrNull
 import net.mamoe.mirai.internal.MiraiImpl
 import net.mamoe.mirai.internal.forwardMessage
 import net.mamoe.mirai.internal.longMessage
 import net.mamoe.mirai.internal.message.*
+import net.mamoe.mirai.internal.network.Packet
+import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket
+import net.mamoe.mirai.internal.network.protocol.packet.chat.SendMessageMultiProtocol
 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
 import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
-import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
+import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.OnlinePushPbPushGroupMsg
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.currentTimeSeconds
@@ -111,61 +115,83 @@ private suspend fun GroupImpl.sendMessagePacket(
 
     val group = this
 
-    val source: OnlineMessageSourceToGroupImpl
+    var source: OnlineMessageSourceToGroupImpl? = null
 
     bot.network.run {
-        MessageSvcPbSendMsg.createToGroup(
-            bot.client,
-            group,
-            finalMessage,
+        SendMessageMultiProtocol.createToGroup(
+            bot.client, group, finalMessage,
             step == GroupMessageSendingStep.FRAGMENTED
         ) { source = it }.forEach { packet ->
-            packet.sendAndExpect<MessageSvcPbSendMsg.Response>().let { resp ->
-                if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
-                    return when (step) {
-                        GroupMessageSendingStep.FIRST -> {
-                            sendMessageImpl(
-                                originalMessage,
-                                transformedMessage,
-                                GroupMessageSendingStep.LONG_MESSAGE
-                            )
-                        }
-                        GroupMessageSendingStep.LONG_MESSAGE -> {
-                            sendMessageImpl(
-                                originalMessage,
-                                transformedMessage,
-                                GroupMessageSendingStep.FRAGMENTED
-                            )
 
-                        }
-                        else -> {
-                            throw MessageTooLargeException(
-                                group,
-                                originalMessage,
-                                finalMessage,
-                                "Message '${finalMessage.content.take(10)}' is too large."
-                            )
-                        }
-                    }.getOrThrow()
+            when (val resp = packet.sendAndExpect<Packet>()) {
+                is MessageSvcPbSendMsg.Response -> {
+                    if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
+                        return when (step) {
+                            GroupMessageSendingStep.FIRST -> {
+                                sendMessageImpl(
+                                    originalMessage,
+                                    transformedMessage,
+                                    GroupMessageSendingStep.LONG_MESSAGE
+                                )
+                            }
+                            GroupMessageSendingStep.LONG_MESSAGE -> {
+                                sendMessageImpl(
+                                    originalMessage,
+                                    transformedMessage,
+                                    GroupMessageSendingStep.FRAGMENTED
+                                )
+
+                            }
+                            else -> {
+                                throw MessageTooLargeException(
+                                    group,
+                                    originalMessage,
+                                    finalMessage,
+                                    "Message '${finalMessage.content.take(10)}' is too large."
+                                )
+                            }
+                        }.getOrThrow()
+                    }
+                    check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
+                        "Send group message failed: $resp"
+                    }
                 }
-                check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
-                    "Send group message failed: $resp"
+                is MusicSharePacket.Response -> {
+                    resp.pkg.checkSuccess("send group music share")
+
+                    val receipt: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt =
+                        nextEventOrNull(3000) { it.fromAppId == 3116 }
+                            ?: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt.EMPTY
+
+                    source = OnlineMessageSourceToGroupImpl(
+                        group,
+                        internalIds = intArrayOf(receipt.messageRandom),
+                        providedSequenceIds = intArrayOf(receipt.sequenceId),
+                        sender = bot,
+                        target = group,
+                        time = currentTimeSeconds().toInt(),
+                        originalMessage = finalMessage
+                    )
                 }
             }
         }
+
+        check(source != null) {
+            "Internal error: source is not initialized"
+        }
+
+        try {
+            source!!.ensureSequenceIdAvailable()
+        } catch (e: Exception) {
+            bot.network.logger.warning(
+                "Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
+                e
+
+            )
+        }
+
+        return MessageReceipt(source!!, group)
     }
-
-    try {
-        source.ensureSequenceIdAvailable()
-    } catch (e: Exception) {
-        bot.network.logger.warning(
-            "Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
-            e
-
-        )
-    }
-
-    return MessageReceipt(source, group)
 }
 
 private suspend fun GroupImpl.uploadGroupLongMessageHighway(
diff --git a/mirai-core/src/commonMain/kotlin/message/conversions.kt b/mirai-core/src/commonMain/kotlin/message/conversions.kt
index 035580c2c..c55e1e0ab 100644
--- a/mirai-core/src/commonMain/kotlin/message/conversions.kt
+++ b/mirai-core/src/commonMain/kotlin/message/conversions.kt
@@ -232,6 +232,12 @@ internal fun MessageChain.toRichTextElems(
                     )
                 )
             }
+            is MusicShare -> {
+                // 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT.
+                // 发送消息时会被特殊处理
+                transformOneMessage(PlainText(currentMessage.content))
+            }
+
             is ForwardMessage,
             is MessageSource, // mirai metadata only
             is RichMessage // already transformed above
@@ -510,7 +516,8 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(
                         else -> error("unknown compression flag=${element.lightApp.data[0]}")
                     }
                 }
-                list.add(LightApp(content))
+
+                list.add(LightApp(content).refine())
             }
             element.richMsg != null -> {
                 val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) {
diff --git a/mirai-core/src/commonMain/kotlin/message/lightApp.kt b/mirai-core/src/commonMain/kotlin/message/lightApp.kt
new file mode 100644
index 000000000..f50ed1673
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/message/lightApp.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.internal.message
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import net.mamoe.mirai.message.data.LightApp
+import net.mamoe.mirai.message.data.MusicShare
+import net.mamoe.mirai.message.data.MusicType
+import net.mamoe.mirai.message.data.SingleMessage
+
+private val json = Json {
+    ignoreUnknownKeys = true
+}
+
+internal fun LightApp.tryDeserialize(): LightAppStruct? {
+    return kotlin.runCatching {
+        json.decodeFromString(LightAppStruct.serializer(), this.content)
+    }.getOrNull()
+}
+
+/**
+ * 识别 app 内容, 如果有必要
+ */
+internal fun LightApp.refine(): SingleMessage {
+    val struct = tryDeserialize() ?: return this
+    struct.run {
+        if (meta.music != null) {
+            MusicType.values().find { it.appId.toInt() == meta.music.appid }?.let { musicType ->
+                meta.music.run {
+                    return MusicShare(
+                        type = musicType, title = title, summary = desc,
+                        jumpUrl = jumpUrl, pictureUrl = preview, musicUrl = musicUrl, brief = prompt
+                    )
+                }
+            }
+        }
+
+
+    }
+
+    return this
+}
+
+/*
+EXAMPLE LightAppStruct for MusicShare
+
+{
+  "app": "com.tencent.structmsg",
+  "config": {
+    "autosize": true,
+    "ctime": 1611339208,
+    "forward": true,
+    "token": "1f27c2b5687e0320549992a4652c8465",
+    "type": "normal"
+  },
+  "desc": "音乐",
+  "extra": {
+    "app_type": 1,
+    "appid": 100495085, // NeteaseCloudMusic
+    "uin": 123456789 // qq uin
+  },
+  "meta": {
+    "music": {
+      "action": "",
+      "android_pkg_name": "",
+      "app_type": 1,
+      "appid": 100495085,
+      "desc": "rinahamu/Yunomi",
+      "jumpUrl": "http://music.163.com/song/1338728297/?userid=324076307",
+      "musicUrl": "http://music.163.com/song/media/outer/url?id=1338728297&userid=324076307",
+      "preview": "http://p2.music.126.net/y19E5SadGUmSR8SZxkrNtw==/109951163785855539.jpg",
+      "sourceMsgId": "0",
+      "source_icon": "",
+      "source_url": "",
+      "tag": "网易云音乐",
+      "title": "ファッション"
+    }
+  },
+  "prompt": "[分享]ファッション",
+  "ver": "0.0.0.1",
+  "view": "music"
+}
+ */
+
+@Serializable
+internal data class LightAppStruct(
+    @SerialName("app")
+    val app: String = "",
+    @SerialName("config")
+    val config: Config = Config(),
+    @SerialName("desc")
+    val desc: String = "",
+    @SerialName("extra")
+    val extra: Extra = Extra(),
+    @SerialName("meta")
+    val meta: Meta = Meta(),
+    @SerialName("prompt")
+    val prompt: String = "",
+    @SerialName("ver")
+    val ver: String = "",
+    @SerialName("view")
+    val view: String = ""
+) {
+    @Serializable
+    data class Config(
+        @SerialName("autosize")
+        val autosize: Boolean = false,
+        @SerialName("ctime")
+        val ctime: Int = 0,
+        @SerialName("forward")
+        val forward: Boolean = false,
+        @SerialName("token")
+        val token: String = "",
+        @SerialName("type")
+        val type: String = ""
+    )
+
+    @Serializable
+    data class Extra(
+        @SerialName("app_type")
+        val appType: Int = 0,
+        @SerialName("appid")
+        val appid: Int = 0,
+        @SerialName("uin")
+        val uin: Int = 0
+    )
+
+    @Serializable
+    data class Meta(
+        @SerialName("music")
+        val music: Music? = null
+    ) {
+        @Serializable
+        data class Music(
+            @SerialName("action")
+            val action: String = "",
+            @SerialName("android_pkg_name")
+            val androidPkgName: String = "",
+            @SerialName("app_type")
+            val appType: Int = 0,
+            @SerialName("appid")
+            val appid: Int = 0,
+            @SerialName("desc")
+            val desc: String = "",
+            @SerialName("jumpUrl")
+            val jumpUrl: String = "",
+            @SerialName("musicUrl")
+            val musicUrl: String = "",
+            @SerialName("preview")
+            val preview: String = "",
+            @SerialName("source_icon")
+            val sourceIcon: String = "",
+            @SerialName("sourceMsgId")
+            val sourceMsgId: String = "",
+            @SerialName("source_url")
+            val sourceUrl: String = "",
+            @SerialName("tag")
+            val tag: String = "",
+            @SerialName("title")
+            val title: String = ""
+        )
+    }
+}
diff --git a/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt
index 7791f9138..57571b8da 100644
--- a/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt
@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai.internal.message
 
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -136,11 +137,12 @@ internal class OnlineMessageSourceToTempImpl(
 @Serializable(OnlineMessageSourceToGroupImpl.Serializer::class)
 internal class OnlineMessageSourceToGroupImpl(
     coroutineScope: CoroutineScope,
-    override val internalIds: IntArray,
+    override val internalIds: IntArray, // aka random
     override val time: Int,
     override val originalMessage: MessageChain,
     override val sender: Bot,
-    override val target: Group
+    override val target: Group,
+    providedSequenceIds: IntArray? = null,
 ) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceInternal {
     object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToGroup")
 
@@ -150,7 +152,7 @@ internal class OnlineMessageSourceToGroupImpl(
         get() = sender
     override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
 
-    private val sequenceIdDeferred: Deferred<IntArray?> = run {
+    private val sequenceIdDeferred: Deferred<IntArray?> = providedSequenceIds?.let { CompletableDeferred(it) } ?: run {
         val multi = mutableMapOf<Int, Int>()
         coroutineScope.asyncFromEventOrNull<SendGroupMessageReceipt, IntArray>(
             timeoutMillis = 3000L * this@OnlineMessageSourceToGroupImpl.internalIds.size
diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt
index a5e6f682e..7730ad4ea 100644
--- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt
+++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt
@@ -162,6 +162,7 @@ internal open class QQAndroidClient(
 
     val apkVersionName: ByteArray get() = protocol.ver.toByteArray() //"8.4.18".toByteArray()
     val buildVer: String get() = "8.4.18.4810" // 8.2.0.1296 // 8.4.8.4810 // 8.2.7.4410
+    val clientVersion: String = "android ${protocol.ver}" // android 8.5.0
 
     val buildTime: Long get() = protocol.buildTime
     val sdkVersion: String get() = protocol.sdkVer
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt
index 4ca282e55..c2fbf5276 100644
--- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2020 Mamoe Technologies and contributors.
+ * Copyright 2019-2021 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.
@@ -95,6 +95,13 @@ internal class MsgComm : ProtoBuf {
         @ProtoNumber(7) var msgUid: Long = 0L,
         @ProtoNumber(8) @JvmField val c2cTmpMsgHead: C2CTmpMsgHead? = null,
         @ProtoNumber(9) @JvmField val groupInfo: GroupInfo? = null,
+        /**
+         * 1: 群消息 by pc tim
+         * 1001: 群消息 sent by android phone
+         *
+         *
+         * 3116: music share, ANDROID_PHONE 发送
+         */
         @ProtoNumber(10) @JvmField val fromAppid: Int = 0,
         @ProtoNumber(11) @JvmField val fromInstid: Int = 0,
         @ProtoNumber(12) @JvmField val userActive: Int = 0,
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt
index a964025f7..34331649c 100644
--- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt
@@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.protocol.data.proto
 
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.protobuf.ProtoNumber
+import net.mamoe.mirai.internal.network.Packet
 import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.internal.utils.io.ProtoBuf
 
@@ -962,7 +963,13 @@ internal class OidbSso : ProtoBuf {
         @ProtoNumber(4) @JvmField val bodybuffer: ByteArray = EMPTY_BYTE_ARRAY,
         @ProtoNumber(5) @JvmField val errorMsg: String = "",
         @ProtoNumber(6) @JvmField val clientVersion: String = ""
-    ) : ProtoBuf
+    ) : ProtoBuf, Packet {
+        fun checkSuccess(actionName: String) {
+            check(result == 0) {
+                "${actionName.capitalize()} failed. result=$result, errorMsg=$errorMsg"
+            }
+        }
+    }
 }
 
 @Serializable
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OidbCmd0xb77.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OidbCmd0xb77.kt
new file mode 100644
index 000000000..2ec8f4268
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OidbCmd0xb77.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2019-2021 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
+ */
+
+@file:Suppress("unused", "SpellCheckingInspection")
+
+package net.mamoe.mirai.internal.network.protocol.data.proto
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoNumber
+import net.mamoe.mirai.internal.network.Packet
+import net.mamoe.mirai.internal.utils.io.ProtoBuf
+
+@Serializable
+internal class OidbCmd0xb77 : ProtoBuf {
+    @Serializable
+    internal class ArkJsonBody(
+        @JvmField @ProtoNumber(10) val jsonStr: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ArkMsgBody(
+        @JvmField @ProtoNumber(1) val app: String = "",
+        @JvmField @ProtoNumber(2) val view: String = "",
+        @JvmField @ProtoNumber(3) val prompt: String = "",
+        @JvmField @ProtoNumber(4) val ver: String = "",
+        @JvmField @ProtoNumber(5) val desc: String = "",
+        @JvmField @ProtoNumber(6) val featureId: Int = 0,
+        @JvmField @ProtoNumber(10) val meta: String = "",
+        @JvmField @ProtoNumber(11) val metaUrl1: String = "",
+        @JvmField @ProtoNumber(12) val metaUrl2: String = "",
+        @JvmField @ProtoNumber(13) val metaUrl3: String = "",
+        @JvmField @ProtoNumber(14) val metaText1: String = "",
+        @JvmField @ProtoNumber(15) val metaText2: String = "",
+        @JvmField @ProtoNumber(16) val metaText3: String = "",
+        @JvmField @ProtoNumber(20) val config: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ArkV1MsgBody(
+        @JvmField @ProtoNumber(1) val app: String = "",
+        @JvmField @ProtoNumber(2) val view: String = "",
+        @JvmField @ProtoNumber(3) val prompt: String = "",
+        @JvmField @ProtoNumber(4) val ver: String = "",
+        @JvmField @ProtoNumber(5) val desc: String = "",
+        @JvmField @ProtoNumber(6) val featureId: Int = 0,
+        @JvmField @ProtoNumber(10) val meta: String = "",
+        @JvmField @ProtoNumber(11) val items: List<TemplateItem> = emptyList(),
+        @JvmField @ProtoNumber(20) val config: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ClientInfo(
+        @JvmField @ProtoNumber(1) val platform: Int = 0,
+        @JvmField @ProtoNumber(2) val sdkVersion: String = "",
+        @JvmField @ProtoNumber(3) val androidPackageName: String = "",
+        @JvmField @ProtoNumber(4) val androidSignature: String = "",
+        @JvmField @ProtoNumber(5) val iosBundleId: String = "",
+        @JvmField @ProtoNumber(6) val pcSign: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ImageInfo(
+        @JvmField @ProtoNumber(1) val md5: String = "",
+        @JvmField @ProtoNumber(2) val uuid: String = "",
+        @JvmField @ProtoNumber(3) val imgType: Int = 0,
+        @JvmField @ProtoNumber(4) val fileSize: Int = 0,
+        @JvmField @ProtoNumber(5) val width: Int = 0,
+        @JvmField @ProtoNumber(6) val height: Int = 0,
+        @JvmField @ProtoNumber(7) val original: Int = 0,
+        @JvmField @ProtoNumber(101) val fileId: Int = 0,
+        @JvmField @ProtoNumber(102) val serverIp: Int = 0,
+        @JvmField @ProtoNumber(103) val serverPort: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    internal class MiniAppMsgBody(
+        @JvmField @ProtoNumber(1) val miniAppAppid: Long = 0L,
+        @JvmField @ProtoNumber(2) val miniAppPath: String = "",
+        @JvmField @ProtoNumber(3) val webPageUrl: String = "",
+        @JvmField @ProtoNumber(4) val miniAppType: Int = 0,
+        @JvmField @ProtoNumber(5) val title: String = "",
+        @JvmField @ProtoNumber(6) val desc: String = "",
+        @JvmField @ProtoNumber(10) val jsonStr: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ReqBody(
+        @JvmField @ProtoNumber(1) val appid: Long = 0L,
+        @JvmField @ProtoNumber(2) val appType: Int = 0,
+        @JvmField @ProtoNumber(3) val msgStyle: Int = 0,
+        @JvmField @ProtoNumber(4) val senderUin: Long = 0L,
+        @JvmField @ProtoNumber(5) val clientInfo: ClientInfo? = null,
+        // @JvmField @ProtoNumber(6) val textMsg: String? = null,
+        @JvmField @ProtoNumber(7) val extInfo: ExtInfo? = null,
+        @JvmField @ProtoNumber(10) val sendType: Int = 0,
+        @JvmField @ProtoNumber(11) val recvUin: Long = 0L,
+        @JvmField @ProtoNumber(12) val richMsgBody: RichMsgBody? = null,
+        @JvmField @ProtoNumber(13) val arkMsgBody: ArkMsgBody? = null,
+        // @JvmField @ProtoNumber(14) val recvOpenid: String? = null, // don't be ""
+        @JvmField @ProtoNumber(15) val arkv1MsgBody: ArkV1MsgBody? = null,
+        @JvmField @ProtoNumber(16) val arkJsonBody: ArkJsonBody? = null,
+        @JvmField @ProtoNumber(17) val xmlMsgBody: XmlMsgBody? = null,
+        @JvmField @ProtoNumber(18) val miniAppMsgBody: MiniAppMsgBody? = null
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ExtInfo(
+        @ProtoNumber(1) @JvmField val customFeatureId: List<Int> = emptyList(),
+        @ProtoNumber(2) @JvmField val apnsWording: String = "",
+        @ProtoNumber(3) @JvmField val groupSaveDbFlag: Int = 0,
+        @ProtoNumber(4) @JvmField val receiverAppId: Int = 0,
+        @ProtoNumber(5) @JvmField val msgSeq: Long = 0L,
+    ) : ProtoBuf
+
+    @Serializable
+    internal class RichMsgBody(
+        @JvmField @ProtoNumber(1) val usingArk: Boolean = false,
+        @JvmField @ProtoNumber(10) val title: String = "",
+        @JvmField @ProtoNumber(11) val summary: String = "",
+        @JvmField @ProtoNumber(12) val brief: String = "",
+        @JvmField @ProtoNumber(13) val url: String = "",
+        @JvmField @ProtoNumber(14) val pictureUrl: String = "",
+        @JvmField @ProtoNumber(15) val action: String = "",
+        @JvmField @ProtoNumber(16) val musicUrl: String = "",
+        @JvmField @ProtoNumber(21) val imageInfo: ImageInfo? = null
+    ) : ProtoBuf
+
+    @Serializable
+    internal class RspBody(
+        @JvmField @ProtoNumber(1) val wording: String = "",
+        @JvmField @ProtoNumber(2) val jumpResult: Int = 0,
+        @JvmField @ProtoNumber(3) val jumpUrl: String = "",
+        @JvmField @ProtoNumber(4) val level: Int = 0,
+        @JvmField @ProtoNumber(5) val subLevel: Int = 0,
+        @JvmField @ProtoNumber(6) val developMsg: String = ""
+    ) : ProtoBuf, Packet
+
+    @Serializable
+    internal class TemplateItem(
+        @JvmField @ProtoNumber(1) val key: String = "",
+        @JvmField @ProtoNumber(2) val type: Int = 0,
+        @JvmField @ProtoNumber(3) val value: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class XmlMsgBody(
+        @JvmField @ProtoNumber(11) val serviceId: Int = 0,
+        @JvmField @ProtoNumber(12) val xml: String = ""
+    ) : ProtoBuf
+}
+        
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt
index df64482d9..7196f09ff 100644
--- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt
@@ -157,6 +157,7 @@ internal object KnownPacketFactories {
         StrangerList.GetStrangerList,
         StrangerList.DelStranger,
         SummaryCard.ReqSummaryCard,
+        MusicSharePacket,
     )
 
     object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MusicSharePacket.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MusicSharePacket.kt
new file mode 100644
index 000000000..5559e2fc4
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MusicSharePacket.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.internal.network.protocol.packet.chat
+
+import kotlinx.io.core.ByteReadPacket
+import net.mamoe.mirai.internal.QQAndroidBot
+import net.mamoe.mirai.internal.network.Packet
+import net.mamoe.mirai.internal.network.QQAndroidClient
+import net.mamoe.mirai.internal.network.protocol.data.proto.OidbCmd0xb77
+import net.mamoe.mirai.internal.network.protocol.data.proto.OidbSso
+import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
+import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
+import net.mamoe.mirai.internal.utils.io.serialization.loadAs
+import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
+import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
+import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
+import net.mamoe.mirai.message.data.MessageSourceKind
+import net.mamoe.mirai.message.data.MusicShare
+
+internal object MusicSharePacket :
+    OutgoingPacketFactory<MusicSharePacket.Response>("OidbSvc.0xb77_9") {
+
+    class Response(
+        val pkg: OidbSso.OIDBSSOPkg,
+    ) : Packet {
+        val response by lazy {
+            pkg.bodybuffer.loadAs(OidbCmd0xb77.RspBody.serializer())
+        }
+
+        override fun toString(): String =
+            "MusicSharePacket.Response(success=${pkg.result == 0}, error=${pkg.errorMsg})"
+    }
+
+    override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+        return Response(readProtoBuf(OidbSso.OIDBSSOPkg.serializer()))
+    }
+
+    operator fun invoke(
+        client: QQAndroidClient,
+        musicShare: MusicShare,
+        targetUin: Long,
+        targetKind: MessageSourceKind
+    ) = buildOutgoingUniPacket(client) {
+        with(musicShare) {
+            val musicType = musicShare.type
+            writeProtoBuf(
+                OidbSso.OIDBSSOPkg.serializer(),
+                OidbSso.OIDBSSOPkg(
+                    command = 2935,
+                    serviceType = 9,
+                    clientVersion = client.clientVersion,
+                    bodybuffer = OidbCmd0xb77.ReqBody(
+                        appid = musicType.appId,
+                        appType = 1,
+                        msgStyle = if (jumpUrl.isNotBlank()) 4 else 0, // 有播放连接为4, 无播放连接为0
+                        clientInfo = OidbCmd0xb77.ClientInfo(
+                            platform = musicType.platform,
+                            sdkVersion = musicType.sdkVersion,
+                            androidPackageName = musicType.packageName,
+                            androidSignature = musicType.signature
+                        ),
+                        extInfo = OidbCmd0xb77.ExtInfo(
+                            msgSeq = 0
+                        ),
+                        sendType = when (targetKind) {
+                            MessageSourceKind.FRIEND -> 0
+                            MessageSourceKind.GROUP -> 1
+                            else -> error("Internal error: Unsupported targetKind $targetKind")
+                        },
+                        recvUin = targetUin,
+                        richMsgBody = OidbCmd0xb77.RichMsgBody(
+                            title = title,
+                            summary = summary,
+                            brief = brief,
+                            url = jumpUrl,
+                            pictureUrl = pictureUrl,
+                            musicUrl = musicUrl
+                        )
+                    ).toByteArray(OidbCmd0xb77.ReqBody.serializer())
+                )
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/SendMessageMultiProtocol.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/SendMessageMultiProtocol.kt
new file mode 100644
index 000000000..3ca15eefc
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/SendMessageMultiProtocol.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.internal.network.protocol.packet.chat
+
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.internal.contact.takeSingleContent
+import net.mamoe.mirai.internal.contact.uin
+import net.mamoe.mirai.internal.message.OnlineMessageSourceToGroupImpl
+import net.mamoe.mirai.internal.network.QQAndroidClient
+import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
+import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
+import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
+import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.MessageSourceKind
+import net.mamoe.mirai.message.data.MusicShare
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+internal object SendMessageMultiProtocol {
+    inline fun createToGroup(
+        client: QQAndroidClient,
+        group: Group,
+        message: MessageChain,
+        fragmented: Boolean,
+        crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit
+    ): List<OutgoingPacket> {
+        contract { callsInPlace(sourceCallback, InvocationKind.AT_MOST_ONCE) }
+
+        message.takeSingleContent<MusicShare>()?.let { musicShare ->
+            return listOf(MusicSharePacket(client, musicShare, group.uin, targetKind = MessageSourceKind.GROUP))
+        }
+
+        return MessageSvcPbSendMsg.createToGroup(client, group, message, fragmented, sourceCallback)
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt
index 2db861df0..88b34c54e 100644
--- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt
+++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt
@@ -42,11 +42,16 @@ import net.mamoe.mirai.utils.*
 internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("OnlinePush.PbPushGroupMsg") {
     internal class SendGroupMessageReceipt(
         val messageRandom: Int,
-        val sequenceId: Int
+        val sequenceId: Int,
+        val fromAppId: Int,
     ) : Packet, Event, Packet.NoLog, AbstractEvent() {
         override fun toString(): String {
             return "OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt(messageRandom=$messageRandom, sequenceId=$sequenceId)"
         }
+
+        companion object {
+            val EMPTY = SendGroupMessageReceipt(0, 0, 0)
+        }
     }
 
     @OptIn(ExperimentalStdlibApi::class)
@@ -61,11 +66,13 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
         if (isFromSelfAccount) {
             val messageRandom = pbPushMsg.msg.msgBody.richText.attr?.random ?: return null
 
-            if (bot.client.syncingController.pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom }) {
+            if (bot.client.syncingController.pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom }
+                || msgHead.fromAppid == 3116) {
                 // message sent by bot
                 return SendGroupMessageReceipt(
                     messageRandom,
-                    msgHead.msgSeq
+                    msgHead.msgSeq,
+                    msgHead.fromAppid
                 )
             }
             // else: sync form other device
diff --git a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt
index 51436ddda..ae71df0fb 100644
--- a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt
+++ b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt
@@ -19,9 +19,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor
 import net.mamoe.mirai.internal.network.protocol.data.jce.RequestDataVersion2
 import net.mamoe.mirai.internal.network.protocol.data.jce.RequestDataVersion3
 import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPacket
+import net.mamoe.mirai.internal.network.protocol.data.proto.OidbSso
 import net.mamoe.mirai.internal.utils.io.JceStruct
 import net.mamoe.mirai.internal.utils.io.ProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.tars.Tars
+import net.mamoe.mirai.internal.utils.soutv
 import net.mamoe.mirai.utils.read
 import net.mamoe.mirai.utils.readPacketExact
 import kotlin.contracts.InvocationKind
@@ -139,6 +141,14 @@ internal fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrate
     return KtProtoBuf.decodeFromByteArray(deserializer, this)
 }
 
+internal fun <T : ProtoBuf> ByteArray.loadOidb(deserializer: DeserializationStrategy<T>, log: Boolean = false): T {
+    val oidb = loadAs(OidbSso.OIDBSSOPkg.serializer())
+    if (log) {
+        oidb.soutv("OIDB")
+    }
+    return oidb.bodybuffer.loadAs(deserializer)
+}
+
 /**
  * load
  */