diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/FriendImage.kt
similarity index 50%
rename from mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt
rename to mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/FriendImage.kt
index a7cffe76c..ba8945999 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/UploadImage.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/FriendImage.kt
@@ -1,17 +1,11 @@
-@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "NO_REFLECTION_IN_CLASS_PATH")
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
 
 package net.mamoe.mirai.network.protocol.tim.packet.action
 
-import io.ktor.client.HttpClient
 import io.ktor.client.request.get
-import io.ktor.client.request.post
-import io.ktor.http.HttpStatusCode
-import io.ktor.http.URLProtocol
-import io.ktor.http.userAgent
-import kotlinx.coroutines.withContext
 import kotlinx.io.charsets.Charsets
 import kotlinx.io.core.*
-import net.mamoe.mirai.contact.*
+import net.mamoe.mirai.contact.QQ
 import net.mamoe.mirai.message.ImageId
 import net.mamoe.mirai.message.requireLength
 import net.mamoe.mirai.network.BotNetworkHandler
@@ -22,50 +16,9 @@ import net.mamoe.mirai.network.qqAccount
 import net.mamoe.mirai.qqAccount
 import net.mamoe.mirai.utils.ExternalImage
 import net.mamoe.mirai.utils.Http
-import net.mamoe.mirai.utils.assertUnreachable
-import net.mamoe.mirai.utils.configureBody
 import net.mamoe.mirai.utils.io.*
 import net.mamoe.mirai.withSession
-import kotlin.coroutines.coroutineContext
 
-/**
- * 图片文件过大
- */
-class OverFileSizeMaxException : IllegalStateException()
-
-/**
- * 上传群图片
- * 挂起直到上传完成或失败
- *
- * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
- *
- * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
- */
-suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {
-    val userContext = coroutineContext
-    val response = GroupImagePacket.RequestImageId(bot.qqAccount, internalId, image, sessionKey).sendAndExpect<GroupImageResponse>()
-
-    withContext(userContext) {
-        when (response) {
-            is GroupImageUKey -> Http.postImage(
-                htcmd = "0x6ff0071",
-                uin = bot.qqAccount,
-                groupId = GroupId(id),
-                imageInput = image.input,
-                inputSize = image.inputSize,
-                uKeyHex = response.uKey.toUHexString("")
-            )
-
-            is GroupImageAlreadyExists -> {
-            }
-
-            is GroupImageOverFileSizeMax -> throw OverFileSizeMaxException()
-            else -> assertUnreachable()
-        }
-    }
-
-    return image.groupImageId
-}
 
 /**
  * 上传图片
@@ -97,93 +50,6 @@ suspend fun QQ.uploadImage(image: ExternalImage): ImageId = bot.withSession {
         }.await()
 }
 
-@Suppress("SpellCheckingInspection")
-internal suspend inline fun HttpClient.postImage(
-    htcmd: String,
-    uin: UInt,
-    groupId: GroupId?,
-    imageInput: Input,
-    inputSize: Long,
-    uKeyHex: String
-): Boolean = try {
-    post<HttpStatusCode> {
-        url {
-            protocol = URLProtocol.HTTP
-            host = "htdata2.qq.com"
-            path("cgi-bin/httpconn")
-
-            parameters["htcmd"] = htcmd
-            parameters["uin"] = uin.toLong().toString()
-
-            if (groupId != null) parameters["groupcode"] = groupId.value.toLong().toString()
-
-            parameters["term"] = "pc"
-            parameters["ver"] = "5603"
-            parameters["filesize"] = inputSize.toString()
-            parameters["range"] = 0.toString()
-            parameters["ukey"] = uKeyHex
-
-            userAgent("QQClient")
-        }
-
-        configureBody(inputSize, imageInput)
-    } == HttpStatusCode.OK
-} finally {
-    imageInput.close()
-}
-
-/*
-/**
- * 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00
- */
-@Deprecated("Useless packet")
-@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME)
-@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
-object SubmitImageFilenamePacket : PacketFactory {
-    operator fun invoke(
-        bot: UInt,
-        target: UInt,
-        filename: String,
-        sessionKey: SessionKey
-    ): OutgoingPacket = buildOutgoingPacket {
-        writeQQ(bot)
-        writeFully(TIMProtocol.fixVer2)//?
-        //writeHex("04 00 00 00 01 2E 01 00 00 69 35")
-
-        encryptAndWrite(sessionKey) {
-            writeByte(0x01)
-            writeQQ(bot)
-            writeQQ(target)
-            writeZero(2)
-            writeUByte(0x02u)
-            writeRandom(1)
-            writeHex("00 0A 00 01 00 01")
-            val name = "UserDataImage:$filename"
-            writeShort(name.length.toShort())
-            writeStringUtf8(name)
-            writeHex("00 00")
-            writeRandom(2)//这个也与是哪个好友有关?
-            writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01")//35  02? 最后这个值是与是哪个好友有关
-
-            //this.debugPrintThis("SubmitImageFilenamePacket")
-        }
-
-        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1A 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
-        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1B 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
-        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1C 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 29 37 42 53 4B 48 32 44 35 54 51 28 5A 35 7D 35 24 56 5D 32 35 49 4E 2E 6A 70 67 00 00 03 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
-    }
-
-    @PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
-    class Response {
-        override fun decode() = with(input) {
-            require(readBytes().contentEquals(expecting))
-        }
-
-        companion object {
-            private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00)
-        }
-    }
-}*/
 
 // region FriendImageResponse
 
@@ -224,44 +90,6 @@ object FriendImageOverFileSizeMax : FriendImageResponse {
 
 // endregion
 
-// regiion GroupImageResponse
-interface GroupImageResponse : EventPacket
-
-/**
- * 图片数据地址.
- */
-// TODO: 2019/11/15 应该为 inline class, 但 kotlin 有 bug
-data class GroupImageLink(inline val value: String) : GroupImageResponse {
-    suspend fun downloadAsByteArray(): ByteArray = download().readBytes()
-    suspend fun download(): ByteReadPacket = Http.get(value)
-
-    override fun toString(): String = "GroupImageLink($value)"
-}
-
-/**
- * 访问 HTTP API 时使用的 uKey
- */
-inline class GroupImageUKey(inline val uKey: ByteArray) : GroupImageResponse {
-    override fun toString(): String = "GroupImageUKey(uKey=${uKey.toUHexString()})"
-}
-
-/**
- * 图片 ID 已存在
- * 发送消息时使用的 id
- */
-object GroupImageAlreadyExists : GroupImageResponse {
-    override fun toString(): String = "GroupImageAlreadyExists"
-}
-
-/**
- * 超过文件大小上限
- */
-object GroupImageOverFileSizeMax : GroupImageResponse {
-    override fun toString(): String = "GroupImageOverFileSizeMax"
-
-}
-// endregion
-
 /**
  * 请求上传图片. 将发送图片的 md5, size, width, height.
  * 服务器返回以下之一:
@@ -324,7 +152,7 @@ object FriendImagePacket : SessionPacketFactory<FriendImageResponse>() {
         imageId: ImageId
     ): OutgoingPacket {
         imageId.requireLength()
-        require(imageId.value.length == 37) { "ImageId.value.length must == 37" }
+        require(imageId.value.length == 37) { "ImageId.value.length must == 37 but given length=${imageId.value.length} value=${imageId.value}" }
 
         // 00 00 00 07 00 00 00
         // [4B]
@@ -500,185 +328,3 @@ object FriendImagePacket : SessionPacketFactory<FriendImageResponse>() {
         }
     }
 }
-
-
-/**
- * 获取 Image Id 和上传用的一个 uKey
- */
-@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID)
-@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
-object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
-    @Suppress("FunctionName")
-    fun RequestImageId(
-        bot: UInt,
-        groupInternalId: GroupInternalId,
-        image: ExternalImage,
-        sessionKey: SessionKey
-    ): OutgoingPacket = buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) {
-        writeHex("00 00 00 07 00 00")
-
-        writeShortLVPacket(lengthOffset = { it - 7 }) {
-            writeByte(0x08)
-            writeHex("01 12 03 98 01 01 10 01 1A")
-            //                             02 10 02 22
-
-            writeUVarIntLVPacket(lengthOffset = { it }) {
-                writeTUVarint(0x08u, groupInternalId.value)
-                writeTUVarint(0x10u, bot)
-                writeTV(0x1800u)
-
-                writeUByte(0x22u)
-                writeUByte(0x10u)
-                writeFully(image.md5)
-
-                writeTUVarint(0x28u, image.inputSize.toUInt())
-                writeUVarIntLVPacket(tag = 0x32u) {
-                    writeTV(0x5B_00u)
-                    writeTV(0x40_00u)
-                    writeTV(0x33_00u)
-                    writeTV(0x48_00u)
-                    writeTV(0x5F_00u)
-                    writeTV(0x58_00u)
-                    writeTV(0x46_00u)
-                    writeTV(0x51_00u)
-                    writeTV(0x45_00u)
-                    writeTV(0x51_00u)
-                    writeTV(0x40_00u)
-                    writeTV(0x24_00u)
-                    writeTV(0x4F_00u)
-                }
-                writeTV(0x38_01u)
-                writeTV(0x48_01u)
-                writeTUVarint(0x50u, image.width.toUInt())
-                writeTUVarint(0x58u, image.height.toUInt())
-                writeTV(0x60_04u)//这个似乎会变 有时候是02, 有时候是03
-                writeTByteArray(0x6Au, value0x6A)
-
-                writeTV(0x70_00u)
-                writeTV(0x78_03u)
-                writeTV(0x80_01u)
-                writeUByte(0u)
-            }
-        }
-
-    }
-
-    @Suppress("FunctionName")
-    fun RequestImageLink(
-        bot: UInt,
-        sessionKey: SessionKey,
-        imageId: ImageId
-    ): OutgoingPacket {
-        imageId.requireLength()
-        require(imageId.value.length == 37) { "ImageId.value.length must == 37" }
-
-        // 00 00 00 07 00 00 00
-        // [4B]
-        // 08
-        // 01 12
-        // 03 98
-        // 01 02
-        // 08 02
-        //
-        // 1A [47]
-        // 08 [A2 FF 8C F0 03] UVarInt
-        // 10 [DD F1 92 B7 07] UVarInt
-        // 1A [25] 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66
-        // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01
-
-
-        // 00 00 00 07 00 00 00
-        // [4B]
-        // 08
-        // 01 12
-        // 03 98
-        // 01 02
-        // 08 02
-        //
-        // 1A
-        // [47]
-        // 08 [A2 FF 8C F0 03]
-        // 10 [A6 A7 F1 EA 02]
-        // 1A [25] 2F 39 61 31 66 37 31 36 32 2D 38 37 30 38 2D 34 39 30 38 2D 38 31 63 30 2D 66 34 63 64 66 33 35 63 38 64 37 65
-        // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01
-
-
-        return buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) {
-            writeHex("00 00 00 07 00 00")
-
-            writeUShort(0x004Bu)
-
-            writeUByte(0x08u)
-            writeTV(0x01_12u)
-            writeTV(0x03_98u)
-            writeTV(0x01_02u)
-            writeTV(0x08_02u)
-
-            writeUByte(0x1Au)
-            writeUByte(0x47u)
-            writeTUVarint(0x08u, bot)
-            writeTUVarint(0x10u, bot)
-            writeTLV(0x1Au, imageId.value.toByteArray(Charsets.ISO_8859_1))
-            writeHex("20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01")
-        }
-    }
-
-    private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u)
-
-    override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupImageResponse {
-        discardExact(6)//00 00 00 05 00 00
-
-        val length = remaining - 128 - 14
-        if (length < 0) {
-            return if (readUShort().toUInt() == 0x0025u) GroupImageOverFileSizeMax else GroupImageAlreadyExists
-        }
-
-        discardExact(length)
-        return GroupImageUKey(readBytes(128))
-    }
-
-    // 下载图片
-    // 00 00 00 05 00 00
-    // [02 46]
-    // 12 03
-    // 98 01 02
-    // 08 9B A4 D4 9A 0A 10 02 22 BB 04
-    // 08 92 A8 B2 D3 0A
-    // 12 [10] EB 1A 34 01 8F 1E B4 73 39 34 F0 65 68 80 A7 52
-    // 18 00
-    // 48 BD EE 92 CD 01
-    // 48 BD EE 92 E5 01
-    // 48 B4 C3 A9 E8 06
-    // 48 BA F6 D7 5C
-    // 48 EF BC A4 DC 07
-    // 50 50 50 50 50 50 50 50 50 50
-    // 5A [0D] 67 63 68 61 74 2E 71 70 69 63 2E 63 6E // gchat.qpic.cn
-    // 缩略图 62 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 2F 31 39 38 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // 原图 6A [75] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // 普通 72 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 2F 37 32 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // 78 00
-    // 80 01 03
-    // 9A 01 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 32 38 35 39 32 34 32 35 31 34 2D 45 42 31 41 33 34 30 31 38 46 31 45 42 34 37 33 33 39 33 34 46 30 36 35 36 38 38 30 41 37 35 32 2F 34 30 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // A0 01 00
-
-    // 00 00 00 05 00 00
-    // [02 46]
-    // 12 03 98 01 02 08 9B A4 D4 9A 0A 10 02 22 BB 04
-    // 08 D1 F1 CE FD 0B
-    // 12 [10] 5F D4 03 6D 59 36 18 FD 49 3D 4C 97 53 02 BA D8
-    // 18 00
-    // 48 BD EE 92 8D 05
-    // 48 BD EE 92 C5 01
-    // 48 B4 C3 A9 E8 06
-    // 48 BA F6 D7 5C
-    // 48 EF BC A4 DC 07
-    // 50 50 50 50 50 50 50 50 50 50
-    // 5A [0D] 67 63 68 61 74 2E 71 70 69 63 2E 63 6E
-    // 62 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 2F 31 39 38 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // 6A [75] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 2F 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // 72 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 2F 37 32 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // 78 00
-    // 80 01 03
-    // 9A 01 [77] 2F 67 63 68 61 74 70 69 63 5F 6E 65 77 2F 38 31 34 37 37 37 32 33 30 2F 38 31 34 37 37 37 32 33 30 2D 33 32 31 36 32 32 36 35 31 33 2D 35 46 44 34 30 33 36 44 35 39 33 36 31 38 46 44 34 39 33 44 34 43 39 37 35 33 30 32 42 41 44 38 2F 34 30 30 3F 76 75 69 6E 3D 31 30 34 30 34 30 30 32 39 30 26 74 65 72 6D 3D 32 35 35 26 73 72 76 76 65 72 3D 32 36 39 33 33
-    // A0 01 00
-}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupImage.kt
new file mode 100644
index 000000000..1e794428b
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupImage.kt
@@ -0,0 +1,224 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "RUNTIME_ANNOTATION_NOT_SUPPORTED")
+
+package net.mamoe.mirai.network.protocol.tim.packet.action
+
+import kotlinx.coroutines.withContext
+import kotlinx.io.charsets.Charsets
+import kotlinx.io.core.*
+import kotlinx.serialization.SerialId
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.GroupId
+import net.mamoe.mirai.contact.GroupInternalId
+import net.mamoe.mirai.contact.withSession
+import net.mamoe.mirai.message.ImageId
+import net.mamoe.mirai.message.requireLength
+import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.network.protocol.tim.TIMProtocol
+import net.mamoe.mirai.network.protocol.tim.packet.*
+import net.mamoe.mirai.network.protocol.tim.packet.event.EventPacket
+import net.mamoe.mirai.qqAccount
+import net.mamoe.mirai.utils.ExternalImage
+import net.mamoe.mirai.utils.Http
+import net.mamoe.mirai.utils.assertUnreachable
+import net.mamoe.mirai.utils.io.*
+import kotlin.coroutines.coroutineContext
+
+
+/**
+ * 上传群图片
+ * 挂起直到上传完成或失败
+ *
+ * 在 JVM 下, `SendImageUtilsJvm.kt` 内有多个捷径函数
+ *
+ * @throws OverFileSizeMaxException 如果文件过大, 服务器拒绝接收时
+ */
+suspend fun Group.uploadImage(image: ExternalImage): ImageId = withSession {
+    val userContext = coroutineContext
+    val response = GroupImagePacket.RequestImageId(bot.qqAccount, internalId, image, sessionKey).sendAndExpect<GroupImageResponse>()
+
+    withContext(userContext) {
+        when (response) {
+            is ImageUploadInfo -> response.uKey?.let {
+                Http.postImage(
+                    htcmd = "0x6ff0071",
+                    uin = bot.qqAccount,
+                    groupId = GroupId(id),
+                    imageInput = image.input,
+                    inputSize = image.inputSize,
+                    uKeyHex = it.toUHexString("")
+                )
+            }
+
+            // TODO: 2019/11/17 超过大小的情况
+            //is Overfile -> throw OverFileSizeMaxException()
+            else -> assertUnreachable()
+        }
+    }
+
+    return image.groupImageId
+}
+
+interface GroupImageResponse : EventPacket
+
+// endregion
+
+@Serializable
+data class ImageDownloadInfo(
+    @SerialId(11) val host: String,
+
+    @SerialId(12) val thumbnail: String,
+    @SerialId(13) val original: String,
+    @SerialId(14) val compressed: String
+) : GroupImageResponse
+
+@Serializable
+class ImageUploadInfo(
+    @SerialId(8) val uKey: ByteArray? = null
+) : GroupImageResponse {
+    override fun toString(): String = "ImageUploadInfo(uKey=${uKey?.toUHexString()})"
+}
+
+/**
+ * 获取 Image Id 和上传用的一个 uKey
+ */
+@AnnotatedId(KnownPacketId.GROUP_IMAGE_ID)
+@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
+object GroupImagePacket : SessionPacketFactory<GroupImageResponse>() {
+    @Suppress("FunctionName")
+    fun RequestImageId(
+        bot: UInt,
+        groupInternalId: GroupInternalId,
+        image: ExternalImage,
+        sessionKey: SessionKey
+    ): OutgoingPacket = buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) {
+        writeHex("00 00 00 07 00 00")
+
+        writeShortLVPacket(lengthOffset = { it - 7 }) {
+            writeByte(0x08)
+            writeHex("01 12 03 98 01 01 10 01 1A")
+            //                             02 10 02 22
+
+            writeUVarIntLVPacket(lengthOffset = { it }) {
+                writeTUVarint(0x08u, groupInternalId.value)
+                writeTUVarint(0x10u, bot)
+                writeTV(0x1800u)
+
+                writeUByte(0x22u)
+                writeUByte(0x10u)
+                writeFully(image.md5)
+
+                writeTUVarint(0x28u, image.inputSize.toUInt())
+                writeUVarIntLVPacket(tag = 0x32u) {
+                    writeTV(0x5B_00u)
+                    writeTV(0x40_00u)
+                    writeTV(0x33_00u)
+                    writeTV(0x48_00u)
+                    writeTV(0x5F_00u)
+                    writeTV(0x58_00u)
+                    writeTV(0x46_00u)
+                    writeTV(0x51_00u)
+                    writeTV(0x45_00u)
+                    writeTV(0x51_00u)
+                    writeTV(0x40_00u)
+                    writeTV(0x24_00u)
+                    writeTV(0x4F_00u)
+                }
+                writeTV(0x38_01u)
+                writeTV(0x48_01u)
+                writeTUVarint(0x50u, image.width.toUInt())
+                writeTUVarint(0x58u, image.height.toUInt())
+                writeTV(0x60_04u)//这个似乎会变 有时候是02, 有时候是03
+                writeTByteArray(0x6Au, value0x6A)
+
+                writeTV(0x70_00u)
+                writeTV(0x78_03u)
+                writeTV(0x80_01u)
+                writeUByte(0u)
+            }
+        }
+
+    }
+
+    @Suppress("FunctionName")
+    fun RequestImageLink(
+        bot: UInt,
+        sessionKey: SessionKey,
+        imageId: ImageId
+    ): OutgoingPacket {
+        imageId.requireLength()
+        require(imageId.value.length == 37) { "ImageId.value.length must == 37" }
+
+        // 00 00 00 07 00 00 00
+        // [4B]
+        // 08
+        // 01 12
+        // 03 98
+        // 01 02
+        // 08 02
+        //
+        // 1A [47]
+        // 08 [A2 FF 8C F0 03] UVarInt
+        // 10 [DD F1 92 B7 07] UVarInt
+        // 1A [25] 2F 38 65 32 63 32 38 62 64 2D 35 38 61 31 2D 34 66 37 30 2D 38 39 61 31 2D 65 37 31 39 66 63 33 30 37 65 65 66
+        // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01
+
+
+        // 00 00 00 07 00 00 00
+        // [4B]
+        // 08 01
+        // 12 03
+        // 98 01 02
+        // 08 02
+        //
+        // 1A
+        // [47]
+        // 08 [A2 FF 8C F0 03]
+        // 10 [A6 A7 F1 EA 02]
+        // 1A [25] 2F 39 61 31 66 37 31 36 32 2D 38 37 30 38 2D 34 39 30 38 2D 38 31 63 30 2D 66 34 63 64 66 33 35 63 38 64 37 65
+        // 20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01
+
+        return buildSessionPacket(bot, sessionKey, version = TIMProtocol.version0x04) {
+            writeHex("00 00 00 07 00 00")
+
+            writeUShort(0x004Bu)
+
+            writeUByte(0x08u)
+            writeTV(0x01_12u)
+            writeTV(0x03_98u)
+            writeTV(0x01_02u)
+            writeTV(0x08_02u)
+
+            writeUByte(0x1Au)
+            writeUByte(0x47u)
+            writeTUVarint(0x08u, bot)
+            writeTUVarint(0x10u, bot)
+            writeTLV(0x1Au, imageId.value.toByteArray(Charsets.ISO_8859_1))
+            writeHex("20 02 30 04 38 20 40 FF 01 50 00 6A 05 32 36 39 33 33 78 01")
+        }
+    }
+
+    private val value0x6A: UByteArray = ubyteArrayOf(0x05u, 0x32u, 0x36u, 0x36u, 0x35u, 0x36u)
+
+    override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): GroupImageResponse {
+        discardExact(6)//00 00 00 05 00 00
+
+        discardExact(2) // 是 protobuf 的长度, 但是是错的
+        val bytes = readBytes()
+        // println(ByteReadPacket(bytes).readProtoMap())
+
+        @Serializable
+        data class GroupImageResponseProto(
+            @SerialId(3) val imageUploadInfoPacket: ImageUploadInfo? = null,
+            @SerialId(4) val imageDownloadInfo: ImageDownloadInfo? = null
+        )
+
+        val proto = ProtoBuf.load(GroupImageResponseProto.serializer(), bytes)
+        return when {
+            proto.imageUploadInfoPacket != null -> proto.imageUploadInfoPacket
+            proto.imageDownloadInfo != null -> proto.imageDownloadInfo
+            else -> assertUnreachable()
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt
new file mode 100644
index 000000000..52f49740a
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/HttpAPIs.kt
@@ -0,0 +1,48 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE")
+
+package net.mamoe.mirai.network.protocol.tim.packet.action
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.post
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.URLProtocol
+import io.ktor.http.userAgent
+import kotlinx.io.core.Input
+import net.mamoe.mirai.contact.GroupId
+import net.mamoe.mirai.utils.configureBody
+
+
+@Suppress("SpellCheckingInspection")
+internal suspend inline fun HttpClient.postImage(
+    htcmd: String,
+    uin: UInt,
+    groupId: GroupId?,
+    imageInput: Input,
+    inputSize: Long,
+    uKeyHex: String
+): Boolean = try {
+    post<HttpStatusCode> {
+        url {
+            protocol = URLProtocol.HTTP
+            host = "htdata2.qq.com"
+            path("cgi-bin/httpconn")
+
+            parameters["htcmd"] = htcmd
+            parameters["uin"] = uin.toLong().toString()
+
+            if (groupId != null) parameters["groupcode"] = groupId.value.toLong().toString()
+
+            parameters["term"] = "pc"
+            parameters["ver"] = "5603"
+            parameters["filesize"] = inputSize.toString()
+            parameters["range"] = 0.toString()
+            parameters["ukey"] = uKeyHex
+
+            userAgent("QQClient")
+        }
+
+        configureBody(inputSize, imageInput)
+    } == HttpStatusCode.OK
+} finally {
+    imageInput.close()
+}
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt
new file mode 100644
index 000000000..54ba01eeb
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/ImageUploadInfo.kt
@@ -0,0 +1,62 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS", "unused", "NO_REFLECTION_IN_CLASS_PATH")
+
+package net.mamoe.mirai.network.protocol.tim.packet.action
+
+/**
+ * 图片文件过大
+ */
+class OverFileSizeMaxException : IllegalStateException()
+
+/*
+/**
+ * 似乎没有必要. 服务器的返回永远都是 01 00 00 00 02 00 00
+ */
+@Deprecated("Useless packet")
+@AnnotatedId(KnownPacketId.SUBMIT_IMAGE_FILE_NAME)
+@PacketVersion(date = "2019.10.26", timVersion = "2.3.2 (21173)")
+object SubmitImageFilenamePacket : PacketFactory {
+    operator fun invoke(
+        bot: UInt,
+        target: UInt,
+        filename: String,
+        sessionKey: SessionKey
+    ): OutgoingPacket = buildOutgoingPacket {
+        writeQQ(bot)
+        writeFully(TIMProtocol.fixVer2)//?
+        //writeHex("04 00 00 00 01 2E 01 00 00 69 35")
+
+        encryptAndWrite(sessionKey) {
+            writeByte(0x01)
+            writeQQ(bot)
+            writeQQ(target)
+            writeZero(2)
+            writeUByte(0x02u)
+            writeRandom(1)
+            writeHex("00 0A 00 01 00 01")
+            val name = "UserDataImage:$filename"
+            writeShort(name.length.toShort())
+            writeStringUtf8(name)
+            writeHex("00 00")
+            writeRandom(2)//这个也与是哪个好友有关?
+            writeHex("00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2E 01")//35  02? 最后这个值是与是哪个好友有关
+
+            //this.debugPrintThis("SubmitImageFilenamePacket")
+        }
+
+        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1A 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
+        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1B 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 28 5A 53 41 58 40 57 4B 52 4A 5A 31 7E 33 59 4F 53 53 4C 4D 32 4B 49 2E 6A 70 67 00 00 06 E2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
+        //解密body=01 3E 03 3F A2 7C BC D3 C1 00 00 27 1C 00 0A 00 01 00 01 00 30 55 73 65 72 44 61 74 61 43 75 73 74 6F 6D 46 61 63 65 3A 31 5C 29 37 42 53 4B 48 32 44 35 54 51 28 5A 35 7D 35 24 56 5D 32 35 49 4E 2E 6A 70 67 00 00 03 73 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 2F 02
+    }
+
+    @PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
+    class Response {
+        override fun decode() = with(input) {
+            require(readBytes().contentEquals(expecting))
+        }
+
+        companion object {
+            private val expecting = byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00)
+        }
+    }
+}*/
+// regiion GroupImageResponse
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt
new file mode 100644
index 000000000..c0167c517
--- /dev/null
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/Proto.kt
@@ -0,0 +1,140 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE")
+
+package net.mamoe.mirai.utils
+
+import kotlinx.io.core.ByteReadPacket
+import kotlinx.io.core.readBytes
+import kotlinx.io.core.readUInt
+import kotlinx.io.core.readULong
+import net.mamoe.mirai.utils.io.UVarInt
+import net.mamoe.mirai.utils.io.readUVarInt
+import net.mamoe.mirai.utils.io.toUHexString
+
+// ProtoBuf utilities
+
+/*
+ * Type	Meaning	Used For
+ * 0	Varint	int32, int64, uint32, uint64, sint32, sint64, bool, enum
+ * 1	64-bit	fixed64, sfixed64, double
+ * 2	Length-delimi	string, bytes, embedded messages, packed repeated fields
+ * 3	Start group	Groups (deprecated)
+ * 4	End group	Groups (deprecated)
+ * 5	32-bit	fixed32, sfixed32, float
+ *
+ * https://www.jianshu.com/p/f888907adaeb
+ */
+
+@Suppress("FunctionName")
+fun ProtoFieldId(serializedId: UInt): ProtoFieldId = ProtoFieldId(protoFieldNumber(serializedId), protoType(serializedId))
+
+data class ProtoFieldId(
+    val fieldNumber: Int,
+    val type: ProtoType
+) {
+    override fun toString(): String = "$type $fieldNumber"
+}
+
+enum class ProtoType(val value: Byte, val typeName: String) {
+    /**
+     * int32, int64, uint32, uint64, sint32, sint64, bool, enum
+     */
+    VAR_INT(0x00, "varint"),
+
+    /**
+     * fixed64, sfixed64, double
+     */
+    BIT_64(0x01, " 64bit"),
+
+    /**
+     * string, bytes, embedded messages, packed repeated fields
+     */
+    LENGTH_DELIMI(0x02, "delimi"),
+
+    /**
+     * Groups (deprecated)
+     */
+    START_GROUP(0x03, "startg"),
+
+    /**
+     * Groups (deprecated)
+     */
+    END_GROUP(0x04, "  endg"),
+
+    /**
+     * fixed32, sfixed32, float
+     */
+    BIT_32(0x05, " 32bit"), ;
+
+    override fun toString(): String = this.typeName
+
+    companion object {
+        fun valueOf(value: Byte): ProtoType = values().firstOrNull { it.value == value } ?: error("Unknown ProtoId $value")
+    }
+}
+
+/**
+ * 由 ProtoBuf 序列化后的 id 得到类型
+ *
+ * serializedId = (fieldNumber << 3) | wireType
+ */
+fun protoType(number: UInt): ProtoType = ProtoType.valueOf(number.toInt().shl(29).ushr(29).toByte())
+
+/**
+ * ProtoBuf 序列化后的 id 转为序列前标记的 id
+ *
+ * serializedId = (fieldNumber << 3) | wireType
+ */
+fun protoFieldNumber(number: UInt): Int = number.toInt().ushr(3)
+
+
+class ProtoMap(map: MutableMap<ProtoFieldId, Any>) : MutableMap<ProtoFieldId, Any> by map {
+    override fun toString(): String {
+        return this.entries.joinToString(prefix = "ProtoMap(\n  ", postfix = "\n)", separator = "\n  ") {
+            "${it.key}=" + it.value.contentToString().replace("\n", """\n""")
+        }
+    }
+
+    /*
+    override fun put(key: ProtoFieldId, value: Any): Any? {
+        println("${key}=" + value.contentToString())
+        return null
+    }*/
+}
+
+fun Any.contentToString(): String = when (this) {
+    is UInt -> "0x" + this.toUHexString("") + "($this)"
+    is UByte -> "0x" + this.toUHexString() + "($this)"
+    is UShort -> "0x" + this.toUHexString("") + "($this)"
+    is ULong -> "0x" + this.toUHexString("") + "($this)"
+    is Int -> "0x" + this.toUHexString("") + "($this)"
+    is Byte -> "0x" + this.toUHexString() + "($this)"
+    is Short -> "0x" + this.toUHexString("") + "($this)"
+    is Long -> "0x" + this.toUHexString("") + "($this)"
+
+    is UVarInt -> "0x" + this.toUHexString("") + "($this)"
+
+    is Boolean -> if (this) "true" else "false"
+
+    is ByteArray -> this.toUHexString()// + " (${this.encodeToString()})"
+    else -> this.toString()
+}
+
+fun ByteReadPacket.readProtoMap(length: Long = this.remaining): ProtoMap {
+    val map = ProtoMap(mutableMapOf())
+
+
+    val expectingRemaining = this.remaining - length
+    while (this.remaining != expectingRemaining) {
+        val id = ProtoFieldId(readUVarInt())
+        map[id] = when (id.type) {
+            ProtoType.VAR_INT -> UVarInt(readUVarInt())
+            ProtoType.BIT_32 -> readUInt()
+            ProtoType.BIT_64 -> readULong()
+            ProtoType.LENGTH_DELIMI -> readBytes(readUVarInt().toInt())
+
+            ProtoType.START_GROUP -> error("unsupported")
+            ProtoType.END_GROUP -> error("unsupported")
+        }
+    }
+    return map
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt
index 9bb5c02d4..d8b64841a 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/TypeConvertion.kt
@@ -52,7 +52,7 @@ fun Long.toUHexString(separator: String = " "): String =
  */
 fun UByte.toByteArray(): ByteArray = byteArrayOf((this and 255u).toByte())
 
-fun UByte.toUHexString(): String = (this and 255u).toByte().toUHexString()
+fun UByte.toUHexString(): String = this.toByte().toUHexString()
 
 /**
  * 255u -> 00 00 00 FF
@@ -73,10 +73,17 @@ fun UInt.toUHexString(separator: String = " "): String = this.toByteArray().toUH
  * 转无符号十六进制表示, 并补充首位 `0`.
  * 转换结果示例: `FF`, `0E`
  */
-fun Byte.toUHexString(): String = this.toUByte().toString(16).toUpperCase().let {
-    if (it.length == 1) "0$it"
-    else it
-}
+fun Byte.toUHexString(): String = this.toUByte().fixToUHex()
+
+/**
+ * 转无符号十六进制表示, 并补充首位 `0`.
+ */
+fun Byte.fixToUHex(): String = this.toUByte().fixToUHex()
+
+/**
+ * 转无符号十六进制表示, 并补充首位 `0`.
+ */
+fun UByte.fixToUHex(): String = if (this.toInt() in 0..9) "0${this.toString(16).toUpperCase()}" else this.toString(16).toUpperCase()
 
 /**
  * 将无符号 Hex 转为 [ByteArray], 有根据 hex 的 [hashCode] 建立的缓存.
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt
index 8d47ff657..f67c52a75 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/io/Varint.kt
@@ -44,6 +44,9 @@ fun Input.readVarInt(): Int {
     return decodeZigZag32(this.readUVarInt())
 }
 
+inline class UVarInt(
+    val data: UInt
+)
 
 @JvmSynthetic
 fun Input.readUVarInt(): UInt {
@@ -82,6 +85,37 @@ fun Output.writeUVarLong(ulong: Long) {
     this.write0(ulong)
 }
 
+fun UVarInt.toByteArray(): ByteArray {
+    val list = mutableListOf<Byte>()
+    var value = this.data.toLong()
+    do {
+        var temp = (value and 127).toByte()
+        value = value ushr 7
+        if (value != 0L) {
+            temp = temp or 128.toByte()
+        }
+        list += temp
+    } while (value != 0L)
+    return list.toByteArray()
+}
+
+fun UVarInt.toUHexString(separator: String = " "): String = buildString {
+    var value = data.toLong()
+
+    var isFirst = true
+    do {
+        if (!isFirst) {
+            append(separator)
+        }
+        var temp = (value and 127).toByte()
+        value = value ushr 7
+        if (value != 0L) {
+            temp = temp or 128.toByte()
+        }
+        append(temp.toUByte().fixToUHex())
+        isFirst = false
+    } while (value != 0L)
+}
 
 private fun Output.write0(long: Long) {
     var value = long
@@ -101,7 +135,7 @@ private fun read(stream: Input, maxSize: Int): Long {
     var b = stream.readByte().toInt()
     while (b and 0x80 == 0x80) {
         value = value or ((b and 0x7F).toLong() shl size++ * 7)
-        require(size < maxSize) { "VarLong too bigger(expecting maxSize=$maxSize)" }
+        require(size < maxSize) { "VarLong too big(expecting maxSize=$maxSize)" }
         b = stream.readByte().toInt()
     }
 
diff --git a/mirai-debug/build.gradle.kts b/mirai-debug/build.gradle.kts
index 4374bdf56..c60f062c7 100644
--- a/mirai-debug/build.gradle.kts
+++ b/mirai-debug/build.gradle.kts
@@ -3,6 +3,7 @@ plugins {
     id("org.openjfx.javafxplugin") version "0.0.8"
     kotlin("jvm")
     java
+    id("kotlinx-serialization")
 }
 
 javafx {
@@ -15,14 +16,15 @@ application {
     mainClassName = "Application"
 }
 
-val kotlinVersion = rootProject.ext["kotlinVersion"].toString()
-val atomicFuVersion = rootProject.ext["atomicFuVersion"].toString()
-val coroutinesVersion = rootProject.ext["coroutinesVersion"].toString()
-val kotlinXIoVersion = rootProject.ext["kotlinXIoVersion"].toString()
-val coroutinesIoVersion = rootProject.ext["coroutinesIoVersion"].toString()
+val kotlinVersion: String by rootProject.ext
+val atomicFuVersion: String by rootProject.ext
+val coroutinesVersion: String by rootProject.ext
+val kotlinXIoVersion: String by rootProject.ext
+val coroutinesIoVersion: String by rootProject.ext
+val serializationVersion: String by rootProject.ext
 
-val klockVersion = rootProject.ext["klockVersion"].toString()
-val ktorVersion = rootProject.ext["ktorVersion"].toString()
+val klockVersion: String by rootProject.ext
+val ktorVersion: String by rootProject.ext
 
 kotlin {
     sourceSets {
@@ -32,20 +34,30 @@ kotlin {
     }
 }
 
+fun DependencyHandlerScope.kotlinx(id: String, version: String) {
+    implementation("org.jetbrains.kotlinx:$id:$version")
+}
+
+fun DependencyHandlerScope.ktor(id: String, version: String) {
+    implementation("io.ktor:$id:$version")
+}
+
 dependencies {
-    api(project(":mirai-core"))
+    implementation(project(":mirai-core"))
     runtimeOnly(files("../mirai-core/build/classes/kotlin/jvm/main")) // mpp targeting android limitation
 
-    api("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
+    implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
 
     implementation("org.pcap4j:pcap4j-distribution:1.8.2")
     implementation("no.tornado:tornadofx:1.7.17")
     compile(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-javafx", version = "1.3.2")
 
-    implementation("org.jetbrains.kotlin:kotlin-stdlib")
-    implementation("org.jetbrains.kotlinx:atomicfu:$atomicFuVersion")
-    implementation("org.jetbrains.kotlinx:kotlinx-io-jvm:$kotlinXIoVersion")
-    implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion")
-    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion")
-    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
+    kotlin("kotlin-stdlib", kotlinVersion)
+    kotlinx("atomicfu", atomicFuVersion)
+    kotlinx("kotlinx-io-jvm", kotlinXIoVersion)
+    kotlinx("kotlinx-io", kotlinXIoVersion)
+    kotlinx("kotlinx-coroutines-io", coroutinesIoVersion)
+    kotlinx("kotlinx-coroutines-core", coroutinesVersion)
+
+    kotlinx("kotlinx-serialization-runtime", serializationVersion)
 }
\ No newline at end of file
diff --git a/mirai-debug/src/main/kotlin/test/ProtoTest.kt b/mirai-debug/src/main/kotlin/test/ProtoTest.kt
new file mode 100644
index 000000000..3f9e43fe8
--- /dev/null
+++ b/mirai-debug/src/main/kotlin/test/ProtoTest.kt
@@ -0,0 +1,57 @@
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
+
+package test
+
+import kotlinx.serialization.ImplicitReflectionSerializer
+import kotlinx.serialization.SerialId
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoBuf
+import kotlinx.serialization.protobuf.ProtoNumberType
+import kotlinx.serialization.protobuf.ProtoType
+import kotlinx.serialization.serializer
+import net.mamoe.mirai.network.protocol.tim.packet.action.ImageUploadInfo
+import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.ProtoFieldId
+import net.mamoe.mirai.utils.io.hexToBytes
+import net.mamoe.mirai.utils.io.toUHexString
+import kotlin.reflect.KClass
+
+@Serializable
+data class ProtoTest(
+    @SerialId(1) val string: String,
+    @SerialId(1) val int: Int,
+    @SerialId(1) val boolean: Boolean,
+    @SerialId(1) val short: Short,
+    @SerialId(1) val byte: Byte,
+    @SerialId(1) @ProtoType(ProtoNumberType.FIXED) val fixedByte: Byte
+)
+
+@UseExperimental(MiraiInternalAPI::class)
+fun main() {
+    deserializeTest()
+    return
+
+    println(ProtoFieldId(0x12u))
+
+    intArrayOf(
+        0x5A,
+        0X62,
+        0X6A,
+        0X72
+    ).forEach {
+        println(it.toUShort().toUHexString() + " => " + ProtoFieldId(it.toUInt()))
+    }
+
+    println(ProtoBuf.dump(ProtoTest.serializer(), ProtoTest("ss", 1, false, 1, 1, 1)).toUHexString())
+}
+
+fun deserializeTest() {
+    val bytes =
+        ("08 00 10 00 20 01 2A 1E 0A 10 BF 83 4C 2B 67 47 41 8C 9F DD 6D 8C 8E 95 53 D6 10 04 18 E4 E0 54 20 B0 09 28 9E 0D 30 FB AE A6 F4 07 38 50 48 D8 92 9E CD 0A")
+            .hexToBytes()
+
+    println(ImageUploadInfo::class.loadFrom(bytes))
+}
+
+@UseExperimental(ImplicitReflectionSerializer::class)
+fun <T : Any> KClass<T>.loadFrom(protoBuf: ByteArray): T = ProtoBuf.load(this.serializer(), protoBuf)
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-gentleman/build.gradle b/mirai-demos/mirai-demo-gentleman/build.gradle
index de7c93b86..e85e6b802 100644
--- a/mirai-demos/mirai-demo-gentleman/build.gradle
+++ b/mirai-demos/mirai-demo-gentleman/build.gradle
@@ -6,12 +6,12 @@ dependencies {
     api project(":mirai-core")
     runtime files("../../mirai-core/build/classes/kotlin/jvm/main") // mpp targeting android limitation
     //runtime files("../../mirai-core/build/classes/atomicfu/jvm/main") // mpp targeting android limitation
-    api group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
-    api group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
+    implementation group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlinVersion
+    implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: coroutinesVersion
 
     implementation("org.jetbrains.kotlinx:kotlinx-io:$kotlinXIoVersion")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion")
-    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62'
+    implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.62'
     implementation 'org.jsoup:jsoup:1.12.1'
 }
 
diff --git a/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt b/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt
index 2499937ea..e25d13f94 100644
--- a/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt
+++ b/mirai-demos/mirai-demo-gentleman/src/main/kotlin/demo/gentleman/Main.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import net.mamoe.mirai.*
+import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.event.Subscribable
 import net.mamoe.mirai.event.subscribeAlways
 import net.mamoe.mirai.event.subscribeMessages
@@ -15,6 +16,7 @@ import net.mamoe.mirai.message.Image
 import net.mamoe.mirai.message.getValue
 import net.mamoe.mirai.message.sendAsImageTo
 import net.mamoe.mirai.network.protocol.tim.packet.event.FriendMessage
+import net.mamoe.mirai.network.protocol.tim.packet.event.GroupMessage
 import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess
 import java.io.File
 import java.util.*
@@ -65,7 +67,7 @@ suspend fun main() {
         }
 
         has<Image> {
-            if (this is FriendMessage) {
+            if (this is FriendMessage || (this is GroupMessage && this.permission == MemberPermission.OPERATOR)) {
                 withContext(IO) {
                     val image: Image by message
                     // 等同于 val image = message[Image]