diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
index 5d68aef24..fb47841fc 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
@@ -1,15 +1,26 @@
 package net.mamoe.mirai.qqandroid
 
+import kotlinx.io.core.readBytes
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.FriendNameRemark
 import net.mamoe.mirai.data.PreviousNameList
 import net.mamoe.mirai.data.Profile
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.message.data.NotOnlineImageFromFile
+import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
+import net.mamoe.mirai.qqandroid.network.highway.Highway
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.CSDataHighwayHead
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
+import net.mamoe.mirai.qqandroid.network.protocol.packet.withUse
+import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
 import net.mamoe.mirai.utils.ExternalImage
 import net.mamoe.mirai.utils.MiraiInternalAPI
+import net.mamoe.mirai.utils.cryptor.contentToString
 import net.mamoe.mirai.utils.getValue
+import net.mamoe.mirai.utils.io.PlatformSocket
+import net.mamoe.mirai.utils.io.toUHexString
 import net.mamoe.mirai.utils.unsafeWeakRef
 import kotlin.coroutines.CoroutineContext
 
@@ -116,7 +127,73 @@ internal class GroupImpl(
     }
 
     override suspend fun uploadImage(image: ExternalImage): Image {
-        TODO("not implemented")
-    }
+        bot.network.run {
+            val response: ImgStore.GroupPicUp.Response = ImgStore.GroupPicUp(
+                bot.client,
+                uin = bot.uin,
+                groupCode = id,
+                md5 = image.md5,
+                size = image.inputSize,
+                picWidth = image.width,
+                picHeight = image.height,
+                picType = image.imageType,
+                filename = image.filename
+            ).sendAndExpect()
 
+            when (response) {
+                is ImgStore.GroupPicUp.Response.Failed -> error("upload group image failed with reason ${response.message}")
+                is ImgStore.GroupPicUp.Response.FileExists -> {
+                    val resourceId = image.calculateImageResourceId()
+                    return NotOnlineImageFromFile(
+                        resourceId = resourceId,
+                        md5 = response.fileInfo.fileMd5,
+                        filepath = resourceId,
+                        fileLength = response.fileInfo.fileSize.toInt(),
+                        height = response.fileInfo.fileHeight,
+                        width = response.fileInfo.fileWidth,
+                        imageType = response.fileInfo.fileType
+                    )
+                }
+                is ImgStore.GroupPicUp.Response.RequireUpload -> {
+
+                    val socket = PlatformSocket()
+                    socket.connect(response.uploadIpList.first().toIpV4AddressString().also { println("serverIp=$it") }, response.uploadPortList.first())
+                   // socket.use {
+                    socket.send(
+                            Highway.RequestDataTrans(
+                                uin = bot.uin,
+                                command = "PicUp.DataUp",
+                                buildVer = bot.client.buildVer,
+                                uKey = response.uKey,
+                                data = image.input,
+                                dataSize = image.inputSize.toInt(),
+                                md5 = image.md5,
+                                sequenceId = bot.client.nextHighwayDataTransSequenceId()
+                            )
+                        )
+                  //  }
+
+                    //0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00
+                    socket.read().withUse {
+                        readByte()
+                        val headLength = readInt()
+                        val bodyLength = readInt()
+                        val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength)
+                        println(proto.contentToString())
+                        println(readBytes(bodyLength).toUHexString())
+                    }
+                    val resourceId = image.calculateImageResourceId()
+                    return NotOnlineImageFromFile(
+                        resourceId = resourceId,
+                        md5 = image.md5,
+                        filepath = resourceId,
+                        fileLength = image.inputSize.toInt(),
+                        height = image.height,
+                        width = image.width,
+                        imageType = image.imageType
+                    )
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
index e19f490ca..074118e57 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidClient.kt
@@ -95,7 +95,8 @@ internal open class QQAndroidClient(
 
     var openAppId: Long = 715019303L
 
-    val apkVersionName: ByteArray = "8.2.0".toByteArray()
+    val apkVersionName: ByteArray get() = "8.2.0".toByteArray()
+    val buildVer: String get() = "8.2.0.1296"
 
     private val messageSequenceId: AtomicInt = atomic(0)
     internal fun atomicNextMessageSequenceId(): Int = messageSequenceId.getAndAdd(2)
@@ -103,6 +104,9 @@ internal open class QQAndroidClient(
     private val requestPacketRequestId: AtomicInt = atomic(1921334513)
     internal fun nextRequestPacketRequestId(): Int = requestPacketRequestId.getAndAdd(2)
 
+    private val highwayDataTransSequenceId: AtomicInt = atomic(87017)
+    internal fun nextHighwayDataTransSequenceId(): Int = highwayDataTransSequenceId.getAndAdd(2)
+
     val appClientVersion: Int = 0
 
     var networkType: NetworkType = NetworkType.WIFI
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt
new file mode 100644
index 000000000..c7092324b
--- /dev/null
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/Codec.kt
@@ -0,0 +1,129 @@
+package net.mamoe.mirai.qqandroid.network.highway
+
+import io.ktor.client.HttpClient
+import io.ktor.client.request.post
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.URLProtocol
+import io.ktor.http.content.OutgoingContent
+import io.ktor.http.userAgent
+import kotlinx.coroutines.io.ByteWriteChannel
+import kotlinx.io.core.*
+import kotlinx.io.pool.useInstance
+import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.CSDataHighwayHead
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+import net.mamoe.mirai.utils.io.ByteArrayPool
+
+@Suppress("SpellCheckingInspection")
+internal suspend inline fun HttpClient.postImage(
+    htcmd: String,
+    uin: Long,
+    groupcode: Long?,
+    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.toString()
+
+            if (groupcode != null) parameters["groupcode"] = groupcode.toString()
+
+            parameters["term"] = "pc"
+            parameters["ver"] = "5603"
+            parameters["filesize"] = inputSize.toString()
+            parameters["range"] = 0.toString()
+            parameters["ukey"] = uKeyHex
+
+            userAgent("QQClient")
+        }
+
+        body = object : OutgoingContent.WriteChannelContent() {
+            override val contentType: ContentType = ContentType.Image.Any
+            override val contentLength: Long = inputSize
+
+            override suspend fun writeTo(channel: ByteWriteChannel) {
+                ByteArrayPool.useInstance { buffer: ByteArray ->
+                    var size: Int
+                    while (imageInput.readAvailable(buffer).also { size = it } != 0) {
+                        channel.writeFully(buffer, 0, size)
+                    }
+                }
+            }
+        }
+    } == HttpStatusCode.OK
+} finally {
+    imageInput.close()
+}
+
+object Highway {
+    fun RequestDataTrans(
+        uin: Long,
+        command: String,
+        sequenceId: Int,
+        buildVer: String,
+        appId: Int = 537062845,
+        dataFlag: Int = 4096,
+        commandId: Int = 2,
+        localId: Int = 2052,
+
+        uKey: ByteArray,
+        data: Input,
+        dataSize: Int,
+        md5: ByteArray
+    ): ByteReadPacket {
+        val dataHighwayHead = CSDataHighwayHead.DataHighwayHead(
+            version = 1,
+            uin = uin.toString(),
+            command = command,
+            seq = sequenceId,
+            retryTimes = 0,
+            appid = appId,
+            dataflag = dataFlag,
+            commandId = commandId,
+            buildVer = buildVer,
+            localeId = localId
+        )
+        val segHead = CSDataHighwayHead.SegHead(
+            datalength = dataSize,
+            filesize = dataSize.toLong() and 0xFFffFFff,
+            serviceticket = uKey,
+            md5 = md5,
+            fileMd5 = md5
+        )
+        return Codec.buildC2SData(dataHighwayHead, segHead, EMPTY_BYTE_ARRAY, null, data, dataSize)
+    }
+
+    private object Codec {
+        fun buildC2SData(
+            dataHighwayHead: CSDataHighwayHead.DataHighwayHead,
+            segHead: CSDataHighwayHead.SegHead,
+            extendInfo: ByteArray,
+            loginSigHead: CSDataHighwayHead.LoginSigHead?,
+            body: Input,
+            bodySize: Int
+        ): ByteReadPacket {
+            val head = CSDataHighwayHead.ReqDataHighwayHead(
+                msgBasehead = dataHighwayHead,
+                msgSeghead = segHead,
+                reqExtendinfo = extendInfo,
+                msgLoginSigHead = loginSigHead
+            ).toByteArray(CSDataHighwayHead.ReqDataHighwayHead.serializer())
+
+            return buildPacket {
+                writeByte(40)
+                writeInt(head.size)
+                writeInt(bodySize)
+                writeFully(head)
+                body.copyTo(this)
+                writeByte(41)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Cmd0x388.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Cmd0x388.kt
new file mode 100644
index 000000000..e6ea3f23f
--- /dev/null
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Cmd0x388.kt
@@ -0,0 +1,279 @@
+package net.mamoe.mirai.qqandroid.network.protocol.data.proto
+
+import kotlinx.serialization.SerialId
+import kotlinx.serialization.Serializable
+import net.mamoe.mirai.qqandroid.io.ProtoBuf
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+
+@Serializable
+class Cmd0x388 : ProtoBuf {
+    @Serializable
+    class DelImgReq(
+        @SerialId(1) val srcUin: Long = 0L,
+        @SerialId(2) val dstUin: Long = 0L,
+        @SerialId(3) val reqTerm: Int = 0,
+        @SerialId(4) val reqPlatformType: Int = 0,
+        @SerialId(5) val buType: Int = 0,
+        @SerialId(6) val buildVer: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(7) val fileResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(8) val picWidth: Int = 0,
+        @SerialId(9) val picHeight: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class DelImgRsp(
+        @SerialId(1) val result: Int = 0,
+        @SerialId(2) val failMsg: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val fileResid: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class ExpRoamExtendInfo(
+        @SerialId(1) val resid: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class ExpRoamPicInfo(
+        @SerialId(1) val shopFlag: Int = 0,
+        @SerialId(2) val pkgId: Int = 0,
+        @SerialId(3) val picId: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class ExtensionCommPicTryUp(
+        @SerialId(1) val bytesExtinfo: List<ByteArray>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class ExtensionExpRoamTryUp(
+        @SerialId(1) val msgExproamPicInfo: List<ExpRoamPicInfo>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class GetImgUrlReq(
+        @SerialId(1) val groupCode: Long = 0L,
+        @SerialId(2) val dstUin: Long = 0L,
+        @SerialId(3) val fileid: Long = 0L,
+        @SerialId(4) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val urlFlag: Int = 0,
+        @SerialId(6) val urlType: Int = 0,
+        @SerialId(7) val reqTerm: Int = 0,
+        @SerialId(8) val reqPlatformType: Int = 0,
+        @SerialId(9) val innerIp: Int = 0,
+        @SerialId(10) val buType: Int = 0,
+        @SerialId(11) val buildVer: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(12) val fileId: Long = 0L,
+        @SerialId(13) val fileSize: Long = 0L,
+        @SerialId(14) val originalPic: Int = 0,
+        @SerialId(15) val retryReq: Int = 0,
+        @SerialId(16) val fileHeight: Int = 0,
+        @SerialId(17) val fileWidth: Int = 0,
+        @SerialId(18) val picType: Int = 0,
+        @SerialId(19) val picUpTimestamp: Int = 0,
+        @SerialId(20) val reqTransferType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class GetImgUrlRsp(
+        @SerialId(1) val fileid: Long = 0L,
+        @SerialId(2) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val result: Int = 0,
+        @SerialId(4) val failMsg: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val msgImgInfo: ImgInfo? = null,
+        @SerialId(6) val bytesThumbDownUrl: List<ByteArray>? = null,
+        @SerialId(7) val bytesOriginalDownUrl: List<ByteArray>? = null,
+        @SerialId(8) val bytesBigDownUrl: List<ByteArray>? = null,
+        @SerialId(9) val uint32DownIp: List<Int>? = null,
+        @SerialId(10) val uint32DownPort: List<Int>? = null,
+        @SerialId(11) val downDomain: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(12) val thumbDownPara: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(13) val originalDownPara: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(14) val bigDownPara: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(15) val fileId: Long = 0L,
+        @SerialId(16) val autoDownType: Int = 0,
+        @SerialId(17) val uint32OrderDownType: List<Int>? = null,
+        @SerialId(19) val bigThumbDownPara: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(20) val httpsUrlFlag: Int = 0,
+        @SerialId(26) val msgDownIp6: List<IPv6Info>? = null,
+        @SerialId(27) val clientIp6: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class GetPttUrlReq(
+        @SerialId(1) val groupCode: Long = 0L,
+        @SerialId(2) val dstUin: Long = 0L,
+        @SerialId(3) val fileid: Long = 0L,
+        @SerialId(4) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val reqTerm: Int = 0,
+        @SerialId(6) val reqPlatformType: Int = 0,
+        @SerialId(7) val innerIp: Int = 0,
+        @SerialId(8) val buType: Int = 0,
+        @SerialId(9) val buildVer: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(10) val fileId: Long = 0L,
+        @SerialId(11) val fileKey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(12) val codec: Int = 0,
+        @SerialId(13) val buId: Int = 0,
+        @SerialId(14) val reqTransferType: Int = 0,
+        @SerialId(15) val isAuto: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class GetPttUrlRsp(
+        @SerialId(1) val fileid: Long = 0L,
+        @SerialId(2) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val result: Int = 0,
+        @SerialId(4) val failMsg: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val bytesDownUrl: List<ByteArray>? = null,
+        @SerialId(6) val uint32DownIp: List<Int>? = null,
+        @SerialId(7) val uint32DownPort: List<Int>? = null,
+        @SerialId(8) val downDomain: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(9) val downPara: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(10) val fileId: Long = 0L,
+        @SerialId(11) val transferType: Int = 0,
+        @SerialId(12) val allowRetry: Int = 0,
+        @SerialId(26) val msgDownIp6: List<IPv6Info>? = null,
+        @SerialId(27) val clientIp6: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(28) val strDomain: String = ""
+    ) : ProtoBuf
+
+    @Suppress("ArrayInDataClass")
+    @Serializable
+    data class ImgInfo(
+        @SerialId(1) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val fileType: Int = 0,
+        @SerialId(3) val fileSize: Long = 0L,
+        @SerialId(4) val fileWidth: Int = 0,
+        @SerialId(5) val fileHeight: Int = 0
+    ) : ProtoBuf {
+        override fun toString(): String {
+            return "ImgInfo(fileMd5=${fileMd5.contentToString()}, fileType=$fileType, fileSize=$fileSize, fileWidth=$fileWidth, fileHeight=$fileHeight)"
+        }
+    }
+
+    @Serializable
+    class IPv6Info(
+        @SerialId(1) val ip6: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val port: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class PicSize(
+        @SerialId(1) val original: Int = 0,
+        @SerialId(2) val thumb: Int = 0,
+        @SerialId(3) val high: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class ReqBody(
+        @SerialId(1) val netType: Int = 0,
+        @SerialId(2) val subcmd: Int = 0,
+        @SerialId(3) val msgTryupImgReq: List<TryUpImgReq>? = null,
+        @SerialId(4) val msgGetimgUrlReq: List<GetImgUrlReq>? = null,
+        @SerialId(5) val msgTryupPttReq: List<TryUpPttReq>? = null,
+        @SerialId(6) val msgGetpttUrlReq: List<GetPttUrlReq>? = null,
+        @SerialId(7) val commandId: Int = 0,
+        @SerialId(8) val msgDelImgReq: List<DelImgReq>? = null,
+        @SerialId(1001) val extension: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class RspBody(
+        @SerialId(1) val clientIp: Int = 0,
+        @SerialId(2) val subcmd: Int = 0,
+        @SerialId(3) val msgTryupImgRsp: List<TryUpImgRsp>? = null,
+        @SerialId(4) val msgGetimgUrlRsp: List<GetImgUrlRsp>? = null,
+        @SerialId(5) val msgTryupPttRsp: List<TryUpPttRsp>? = null,
+        @SerialId(6) val msgGetpttUrlRsp: List<GetPttUrlRsp>? = null,
+        @SerialId(7) val msgDelImgRsp: List<DelImgRsp>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class TryUpImgReq(
+        @SerialId(1) val groupCode: Long = 0L,
+        @SerialId(2) val srcUin: Long = 0L,
+        @SerialId(3) val fileId: Long = 0L,
+        @SerialId(4) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val fileSize: Long = 0L,
+        @SerialId(6) val fileName: String ="",
+        @SerialId(7) val srcTerm: Int = 0,
+        @SerialId(8) val platformType: Int = 0,
+        @SerialId(9) val buType: Int = 0,
+        @SerialId(10) val picWidth: Int = 0,
+        @SerialId(11) val picHeight: Int = 0,
+        @SerialId(12) val picType: Int = 0,
+        @SerialId(13) val buildVer: String = "",
+        @SerialId(14) val innerIp: Int = 0,
+        @SerialId(15) val appPicType: Int = 0,
+        @SerialId(16) val originalPic: Int = 0,
+        @SerialId(17) val fileIndex: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(18) val dstUin: Long = 0L,
+        @SerialId(19) val srvUpload: Int = 0,
+        @SerialId(20) val transferUrl: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class TryUpImgRsp(
+        @SerialId(1) val fileId: Long = 0L,
+        @SerialId(2) val result: Int = 0,
+        @SerialId(3) val failMsg: String = "",
+        @SerialId(4) val boolFileExit: Boolean = false,
+        @SerialId(5) val msgImgInfo: ImgInfo? = null,
+        @SerialId(6) val uint32UpIp: List<Int>? = null,
+        @SerialId(7) val uint32UpPort: List<Int>? = null,
+        @SerialId(8) val upUkey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(9) val fileid: Long = 0L,
+        @SerialId(10) val upOffset: Long = 0L,
+        @SerialId(11) val blockSize: Long = 0L,
+        @SerialId(12) val boolNewBigChan: Boolean = false,
+        @SerialId(26) val msgUpIp6: List<IPv6Info>? = null,
+        @SerialId(27) val clientIp6: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(1001) val msgInfo4busi: TryUpInfo4Busi? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class TryUpInfo4Busi(
+        @SerialId(1) val downDomain: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val thumbDownUrl: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val originalDownUrl: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(4) val bigDownUrl: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val fileResid: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class TryUpPttReq(
+        @SerialId(1) val groupCode: Long = 0L,
+        @SerialId(2) val srcUin: Long = 0L,
+        @SerialId(3) val fileId: Long = 0L,
+        @SerialId(4) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val fileSize: Long = 0L,
+        @SerialId(6) val fileName: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(7) val srcTerm: Int = 0,
+        @SerialId(8) val platformType: Int = 0,
+        @SerialId(9) val buType: Int = 0,
+        @SerialId(10) val buildVer: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(11) val innerIp: Int = 0,
+        @SerialId(12) val voiceLength: Int = 0,
+        @SerialId(13) val boolNewUpChan: Boolean = false,
+        @SerialId(14) val codec: Int = 0,
+        @SerialId(15) val voiceType: Int = 0,
+        @SerialId(16) val buId: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class TryUpPttRsp(
+        @SerialId(1) val fileId: Long = 0L,
+        @SerialId(2) val result: Int = 0,
+        @SerialId(3) val failMsg: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(4) val boolFileExit: Boolean = false,
+        @SerialId(5) val uint32UpIp: List<Int>? = null,
+        @SerialId(6) val uint32UpPort: List<Int>? = null,
+        @SerialId(7) val upUkey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(8) val fileid: Long = 0L,
+        @SerialId(9) val upOffset: Long = 0L,
+        @SerialId(10) val blockSize: Long = 0L,
+        @SerialId(11) val fileKey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(12) val channelType: Int = 0,
+        @SerialId(26) val msgUpIp6: List<IPv6Info>? = null,
+        @SerialId(27) val clientIp6: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt
new file mode 100644
index 000000000..c75f8a867
--- /dev/null
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Highway.kt
@@ -0,0 +1,454 @@
+package net.mamoe.mirai.qqandroid.network.protocol.data.proto
+
+import kotlinx.serialization.SerialId
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoNumberType
+import kotlinx.serialization.protobuf.ProtoType
+import net.mamoe.mirai.qqandroid.io.ProtoBuf
+import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
+
+@Serializable
+class BdhExtinfo : ProtoBuf {
+    @Serializable
+    class CommFileExtReq(
+        @SerialId(1) val actionType: Int = 0,
+        @SerialId(2) val uuid: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class CommFileExtRsp(
+        @SerialId(1) val int32Retcode: Int = 0,
+        @SerialId(2) val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class PicInfo(
+        @SerialId(1) val idx: Int = 0,
+        @SerialId(2) val size: Int = 0,
+        @SerialId(3) val binMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(4) val type: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class QQVoiceExtReq(
+        @SerialId(1) val qid: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val fmt: Int = 0,
+        @SerialId(3) val rate: Int = 0,
+        @SerialId(4) val bits: Int = 0,
+        @SerialId(5) val channel: Int = 0,
+        @SerialId(6) val pinyin: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class QQVoiceExtRsp(
+        @SerialId(1) val qid: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val int32Retcode: Int = 0,
+        @SerialId(3) val msgResult: List<QQVoiceResult>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class QQVoiceResult(
+        @SerialId(1) val text: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val pinyin: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val source: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class ShortVideoReqExtInfo(
+        @SerialId(1) val cmd: Int = 0,
+        @SerialId(2) val sessionId: Long = 0L,
+        @SerialId(3) val msgThumbinfo: PicInfo? = null,
+        @SerialId(4) val msgVideoinfo: VideoInfo? = null,
+        @SerialId(5) val msgShortvideoSureReq: ShortVideoSureReqInfo? = null,
+        @SerialId(6) val boolIsMergeCmdBeforeData: Boolean = false
+    ) : ProtoBuf
+
+    @Serializable
+    class ShortVideoRspExtInfo(
+        @SerialId(1) val cmd: Int = 0,
+        @SerialId(2) val sessionId: Long = 0L,
+        @SerialId(3) val int32Retcode: Int = 0,
+        @SerialId(4) val errinfo: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val msgThumbinfo: PicInfo? = null,
+        @SerialId(6) val msgVideoinfo: VideoInfo? = null,
+        @SerialId(7) val msgShortvideoSureRsp: ShortVideoSureRspInfo? = null,
+        @SerialId(8) val retryFlag: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class ShortVideoSureReqInfo(
+        @SerialId(1) val fromuin: Long = 0L,
+        @SerialId(2) val chatType: Int = 0,
+        @SerialId(3) val touin: Long = 0L,
+        @SerialId(4) val groupCode: Long = 0L,
+        @SerialId(5) val clientType: Int = 0,
+        @SerialId(6) val msgThumbinfo: PicInfo? = null,
+        @SerialId(7) val msgMergeVideoinfo: List<VideoInfo>? = null,
+        @SerialId(8) val msgDropVideoinfo: List<VideoInfo>? = null,
+        @SerialId(9) val businessType: Int = 0,
+        @SerialId(10) val subBusinessType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class ShortVideoSureRspInfo(
+        @SerialId(1) val fileid: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val ukey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val msgVideoinfo: VideoInfo? = null,
+        @SerialId(4) val mergeCost: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class StoryVideoExtReq : ProtoBuf
+
+    @Serializable
+    class StoryVideoExtRsp(
+        @SerialId(1) val int32Retcode: Int = 0,
+        @SerialId(2) val msg: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val cdnUrl: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(4) val fileKey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(5) val fileId: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class UploadPicExtInfo(
+        @SerialId(1) val fileResid: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val thumbDownloadUrl: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class VideoInfo(
+        @SerialId(1) val idx: Int = 0,
+        @SerialId(2) val size: Int = 0,
+        @SerialId(3) val binMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(4) val format: Int = 0,
+        @SerialId(5) val resLen: Int = 0,
+        @SerialId(6) val resWidth: Int = 0,
+        @SerialId(7) val time: Int = 0,
+        @SerialId(8) val starttime: Long = 0L,
+        @SerialId(9) val isAudio: Int = 0
+    ) : ProtoBuf
+}
+
+@Serializable
+class CSDataHighwayHead : ProtoBuf {
+    @Serializable
+    class C2CCommonExtendinfo(
+        @SerialId(1) val infoId: Int = 0,
+        @SerialId(2) val msgFilterExtendinfo: FilterExtendinfo? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class DataHighwayHead(
+        @SerialId(1) val version: Int = 0,
+        @SerialId(2) val uin: String = "",
+        @SerialId(3) val command: String = "",
+        @SerialId(4) val seq: Int = 0,
+        @SerialId(5) val retryTimes: Int = 0,
+        @SerialId(6) val appid: Int = 0,
+        @SerialId(7) val dataflag: Int = 0,
+        @SerialId(8) val commandId: Int = 0,
+        @SerialId(9) val buildVer: String = "",
+        @SerialId(10) val localeId: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class DataHole(
+        @SerialId(1) val begin: Long = 0L,
+        @SerialId(2) val end: Long = 0L
+    ) : ProtoBuf
+
+    @Serializable
+    class FilterExtendinfo(
+        @SerialId(1) val filterFlag: Int = 0,
+        @SerialId(2) val msgImageFilterRequest: ImageFilterRequest? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class FilterStyle(
+        @SerialId(1) val styleId: Int = 0,
+        @SerialId(2) val styleName: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class ImageFilterRequest(
+        @SerialId(1) val sessionId: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val clientIp: Int = 0,
+        @SerialId(3) val uin: Long = 0L,
+        @SerialId(4) val style: FilterStyle? = null,
+        @SerialId(5) val width: Int = 0,
+        @SerialId(6) val height: Int = 0,
+        @SerialId(7) val imageData: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class ImageFilterResponse(
+        @SerialId(1) val retCode: Int = 0,
+        @SerialId(2) val imageData: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val costTime: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class LoginSigHead(
+        @SerialId(1) val loginsigType: Int = 0,
+        @SerialId(2) val loginsig: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class NewServiceTicket(
+        @SerialId(1) val signature: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val ukey: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+
+    @Serializable
+    class PicInfoExt(
+        @SerialId(1) val picWidth: Int = 0,
+        @SerialId(2) val picHeight: Int = 0,
+        @SerialId(3) val picFlag: Int = 0,
+        @SerialId(4) val busiType: Int = 0,
+        @SerialId(5) val srcTerm: Int = 0,
+        @SerialId(6) val platType: Int = 0,
+        @SerialId(7) val netType: Int = 0,
+        @SerialId(8) val imgType: Int = 0,
+        @SerialId(9) val appPicType: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class PicRspExtInfo(
+        @SerialId(1) val skey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val clientIp: Int = 0,
+        @SerialId(3) val upOffset: Long = 0L,
+        @SerialId(4) val blockSize: Long = 0L
+    ) : ProtoBuf
+
+    @Serializable
+    class QueryHoleRsp(
+        @SerialId(1) val result: Int = 0,
+        @SerialId(2) val dataHole: List<DataHole>? = null,
+        @SerialId(3) val boolCompFlag: Boolean = false
+    ) : ProtoBuf
+
+    @Serializable
+    class ReqDataHighwayHead(
+        @SerialId(1) val msgBasehead: DataHighwayHead? = null,
+        @SerialId(2) val msgSeghead: SegHead? = null,
+        @SerialId(3) val reqExtendinfo: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(4) val timestamp: Long = 0L,
+        @SerialId(5) val msgLoginSigHead: LoginSigHead? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class RspBody(
+        @SerialId(1) val msgQueryHoleRsp: QueryHoleRsp? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class RspDataHighwayHead(
+        @SerialId(1) val msgBasehead: DataHighwayHead? = null,
+        @SerialId(2) val msgSeghead: SegHead? = null,
+        @SerialId(3) val errorCode: Int = 0,
+        @SerialId(4) val allowRetry: Int = 0,
+        @SerialId(5) val cachecost: Int = 0,
+        @SerialId(6) val htcost: Int = 0,
+        @SerialId(7) val rspExtendinfo: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(8) val timestamp: Long = 0L,
+        @SerialId(9) val range: Long = 0L,
+        @SerialId(10) val isReset: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class SegHead(
+        @SerialId(1) val serviceid: Int = 0,
+        @SerialId(2) val filesize: Long = 0L,
+        @SerialId(3) val dataoffset: Long = 0L,
+        @SerialId(4) val datalength: Int = 0,
+        @SerialId(5) val rtcode: Int = 0,
+        @SerialId(6) val serviceticket: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(7) val flag: Int = 0,
+        @SerialId(8) val md5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(9) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(10) val cacheAddr: Int = 0,
+        @SerialId(11) val queryTimes: Int = 0,
+        @SerialId(12) val updateCacheip: Int = 0
+    ) : ProtoBuf
+}
+
+@Serializable
+class HwConfigPersistentPB : ProtoBuf {
+    @Serializable
+    class HwConfigItemPB(
+        @SerialId(1) val ingKey: String = "",
+        @SerialId(2) val endPointList: List<HwEndPointPB>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class HwConfigPB(
+        @SerialId(1) val configItemList: List<HwConfigItemPB>? = null,
+        @SerialId(2) val netSegConfList: List<HwNetSegConfPB>? = null,
+        @SerialId(3) val shortVideoNetConf: List<HwNetSegConfPB>? = null,
+        @SerialId(4) val configItemListIp6: List<HwConfigItemPB>? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class HwEndPointPB(
+        @SerialId(1) val ingHost: String = "",
+        @SerialId(2) val int32Port: Int = 0,
+        @SerialId(3) val int64Timestampe: Long = 0L
+    ) : ProtoBuf
+
+    @Serializable
+    class HwNetSegConfPB(
+        @SerialId(1) val int64NetType: Long = 0L,
+        @SerialId(2) val int64SegSize: Long = 0L,
+        @SerialId(3) val int64SegNum: Long = 0L,
+        @SerialId(4) val int64CurConnNum: Long = 0L
+    ) : ProtoBuf
+}
+
+@Serializable
+class HwSessionInfoPersistentPB : ProtoBuf {
+    @Serializable
+    class HwSessionInfoPB(
+        @SerialId(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val sessionKey: ByteArray = EMPTY_BYTE_ARRAY
+    ) : ProtoBuf
+}
+
+@Serializable
+class Subcmd0x501 : ProtoBuf {
+    @Serializable
+    class ReqBody(
+        @SerialId(1281) val msgSubcmd0x501ReqBody: SubCmd0x501ReqBody? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class RspBody(
+        @SerialId(1281) val msgSubcmd0x501RspBody: SubCmd0x501Rspbody? = null
+    ) : ProtoBuf
+
+    @Serializable
+    class SubCmd0x501ReqBody(
+        @SerialId(1) val uin: Long = 0L,
+        @SerialId(2) val idcId: Int = 0,
+        @SerialId(3) val appid: Int = 0,
+        @SerialId(4) val loginSigType: Int = 0,
+        @SerialId(5) val loginSigTicket: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(6) val requestFlag: Int = 0,
+        @SerialId(7) val uint32ServiceTypes: List<Int>? = null,
+        @SerialId(8) val bid: Int = 0,
+        @SerialId(9) val term: Int = 0,
+        @SerialId(10) val plat: Int = 0,
+        @SerialId(11) val net: Int = 0,
+        @SerialId(12) val caller: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    class SubCmd0x501Rspbody(
+        @SerialId(1) val httpconnSigSession: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(2) val sessionKey: ByteArray = EMPTY_BYTE_ARRAY,
+        @SerialId(3) val msgHttpconnAddrs: List<SrvAddrs>? = null,
+        @SerialId(4) val preConnection: Int = 0,
+        @SerialId(5) val csConn: Int = 0,
+        @SerialId(6) val msgIpLearnConf: IpLearnConf? = null,
+        @SerialId(7) val msgDynTimeoutConf: DynTimeOutConf? = null,
+        @SerialId(8) val msgOpenUpConf: OpenUpConf? = null,
+        @SerialId(9) val msgDownloadEncryptConf: DownloadEncryptConf? = null,
+        @SerialId(10) val msgShortVideoConf: ShortVideoConf? = null,
+        @SerialId(11) val msgPtvConf: PTVConf? = null
+    ) : ProtoBuf {
+        @Serializable
+        class DownloadEncryptConf(
+            @SerialId(1) val boolEnableEncryptRequest: Boolean = false,
+            @SerialId(2) val boolEnableEncryptedPic: Boolean = false,
+            @SerialId(3) val ctrlFlag: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class DynTimeOutConf(
+            @SerialId(1) val tbase2g: Int = 0,
+            @SerialId(2) val tbase3g: Int = 0,
+            @SerialId(3) val tbase4g: Int = 0,
+            @SerialId(4) val tbaseWifi: Int = 0,
+            @SerialId(5) val torg2g: Int = 0,
+            @SerialId(6) val torg3g: Int = 0,
+            @SerialId(7) val torg4g: Int = 0,
+            @SerialId(8) val torgWifi: Int = 0,
+            @SerialId(9) val maxTimeout: Int = 0,
+            @SerialId(10) val enableDynTimeout: Int = 0,
+            @SerialId(11) val maxTimeout2g: Int = 0,
+            @SerialId(12) val maxTimeout3g: Int = 0,
+            @SerialId(13) val maxTimeout4g: Int = 0,
+            @SerialId(14) val maxTimeoutWifi: Int = 0,
+            @SerialId(15) val hbTimeout2g: Int = 0,
+            @SerialId(16) val hbTimeout3g: Int = 0,
+            @SerialId(17) val hbTimeout4g: Int = 0,
+            @SerialId(18) val hbTimeoutWifi: Int = 0,
+            @SerialId(19) val hbTimeoutDefault: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class Ip6Addr(
+            @SerialId(1) val type: Int = 0,
+            @SerialId(2) val ip6: ByteArray = EMPTY_BYTE_ARRAY,
+            @SerialId(3) val port: Int = 0,
+            @SerialId(4) val area: Int = 0,
+            @SerialId(5) val sameIsp: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class IpAddr(
+            @SerialId(1) val type: Int = 0,
+            @ProtoType(ProtoNumberType.FIXED) @SerialId(2) val ip: Int = 0,
+            @SerialId(3) val port: Int = 0,
+            @SerialId(4) val area: Int = 0,
+            @SerialId(5) val sameIsp: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class IpLearnConf(
+            @SerialId(1) val refreshCachedIp: Int = 0,
+            @SerialId(2) val enableIpLearn: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class NetSegConf(
+            @SerialId(1) val netType: Int = 0,
+            @SerialId(2) val segsize: Int = 0,
+            @SerialId(3) val segnum: Int = 0,
+            @SerialId(4) val curconnnum: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class OpenUpConf(
+            @SerialId(1) val boolEnableOpenup: Boolean = false,
+            @SerialId(2) val preSendSegnum: Int = 0,
+            @SerialId(3) val preSendSegnum3g: Int = 0,
+            @SerialId(4) val preSendSegnum4g: Int = 0,
+            @SerialId(5) val preSendSegnumWifi: Int = 0
+        ) : ProtoBuf
+
+        @Serializable
+        class PTVConf(
+            @SerialId(1) val channelType: Int = 0,
+            @SerialId(2) val msgNetsegconf: List<NetSegConf>? = null,
+            @SerialId(3) val boolOpenHardwareCodec: Boolean = false
+        ) : ProtoBuf
+
+        @Serializable
+        class ShortVideoConf(
+            @SerialId(1) val channelType: Int = 0,
+            @SerialId(2) val msgNetsegconf: List<NetSegConf>? = null,
+            @SerialId(3) val boolOpenHardwareCodec: Boolean = false,
+            @SerialId(4) val boolSendAheadSignal: Boolean = false
+        ) : ProtoBuf
+
+        @Serializable
+        class SrvAddrs(
+            @SerialId(1) val serviceType: Int = 0,
+            @SerialId(2) val msgAddrs: List<IpAddr>? = null,
+            @SerialId(3) val fragmentSize: Int = 0,
+            @SerialId(4) val msgNetsegconf: List<NetSegConf>? = null,
+            @SerialId(5) val msgAddrsV6: List<Ip6Addr>? = null
+        ) : ProtoBuf
+    }
+}
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
index 23575f51d..bc5c59c2d 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
@@ -5,7 +5,9 @@ import kotlinx.io.pool.useInstance
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.event.Subscribable
 import net.mamoe.mirai.qqandroid.QQAndroidBot
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImageUpPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.OnlinePush
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
@@ -119,6 +121,9 @@ internal object KnownPacketFactories {
         FriendList.GetFriendGroupList,
         FriendList.GetTroopListSimplify,
         FriendList.GetTroopMemberList,
+        ImgStore.GroupPicUp,
+        ImageUpPacket,
+        LongConn.OffPicDown,
         TroopManagement.EditNametag,
         TroopManagement.Mute,
         TroopManagement.MuteAll
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageDownPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageDownPacket.kt
deleted file mode 100644
index 118c27470..000000000
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageDownPacket.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image
-
-import kotlinx.io.core.ByteReadPacket
-import kotlinx.io.core.writeFully
-import net.mamoe.mirai.data.Packet
-import net.mamoe.mirai.qqandroid.QQAndroidBot
-import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
-import net.mamoe.mirai.qqandroid.network.QQAndroidClient
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352Packet
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.GetImgUrlReq
-import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
-import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
-import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
-import net.mamoe.mirai.qqandroid.network.protocol.packet.writeSsoPacket
-
-internal object ImageDownPacket : OutgoingPacketFactory<ImageDownPacket.ImageDownPacketResponse>("LongConn.OffPicDown") {
-
-    operator fun invoke(client: QQAndroidClient, req: GetImgUrlReq): OutgoingPacket {
-        // TODO: 2020/1/24 测试: bodyType, subAppId
-        return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
-            writeSsoPacket(client, subAppId = 0, commandName = commandName, sequenceId = it) {
-                val data = ProtoBufWithNullableSupport.dump(
-                    Cmd0x352Packet.serializer(),
-                    Cmd0x352Packet.createByImageRequest(req)
-                )
-                writeInt(data.size + 4)
-                writeFully(data)
-            }
-        }
-    }
-
-    override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): ImageDownPacketResponse {
-        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
-    }
-
-
-    sealed class ImageDownPacketResponse : Packet {
-        object Success : ImageDownPacketResponse()
-    }
-
-
-}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageUpPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageUpPacket.kt
index 6172e3915..803ee9c34 100644
--- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageUpPacket.kt
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageUpPacket.kt
@@ -10,22 +10,19 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352Packet
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.UploadImgReq
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
-import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
-import net.mamoe.mirai.qqandroid.network.protocol.packet.writeSsoPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
 
 internal object ImageUpPacket : OutgoingPacketFactory<ImageUpPacket.ImageUpPacketResponse>("LongConn.OffPicUp") {
 
     operator fun invoke(client: QQAndroidClient, req: UploadImgReq): OutgoingPacket {
         // TODO: 2020/1/24 测试: bodyType, subAppId
-        return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
-            writeSsoPacket(client, subAppId = 0, commandName = commandName, sequenceId = it) {
-                val data = ProtoBufWithNullableSupport.dump(
-                    Cmd0x352Packet.serializer(),
-                    Cmd0x352Packet.createByImageRequest(req)
-                )
-                writeInt(data.size + 4)
-                writeFully(data)
-            }
+        return buildOutgoingUniPacket(client) {
+            val data = ProtoBufWithNullableSupport.dump(
+                Cmd0x352Packet.serializer(),
+                Cmd0x352Packet.createByImageRequest(req)
+            )
+            writeInt(data.size + 4)
+            writeFully(data)
         }
     }
 
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImgStore.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImgStore.kt
new file mode 100644
index 000000000..fc6e85e99
--- /dev/null
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImgStore.kt
@@ -0,0 +1,101 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image
+
+import io.ktor.client.HttpClient
+import kotlinx.io.core.ByteReadPacket
+import net.mamoe.mirai.data.Packet
+import net.mamoe.mirai.qqandroid.QQAndroidBot
+import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.writeProtoBuf
+import net.mamoe.mirai.qqandroid.network.QQAndroidClient
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x388
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
+import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
+
+internal class ImgStore {
+    object GroupPicUp : OutgoingPacketFactory<GroupPicUp.Response>("ImgStore.GroupPicUp") {
+
+        operator fun invoke(
+            client: QQAndroidClient,
+            uin: Long,
+            groupCode: Long,
+            md5: ByteArray,
+            size: Long,
+            picWidth: Int,
+            picHeight: Int,
+            picType: Int = 1000,
+            fileId: Long = 0,
+            filename: String,
+            srcTerm: Int = 5,
+            platformType: Int = 9,
+            buType: Int = 1,
+            appPicType: Int = 1006,
+            originalPic: Int = 0
+        ): OutgoingPacket = buildOutgoingUniPacket(client) {
+            writeProtoBuf(
+                Cmd0x388.ReqBody.serializer(),
+                Cmd0x388.ReqBody(
+                    netType = 3, // wifi
+                    subcmd = 1,
+                    msgTryupImgReq = listOf(
+                        Cmd0x388.TryUpImgReq(
+                            groupCode = groupCode,
+                            srcUin = uin,
+                            fileMd5 = md5,
+                            fileSize = size,
+                            fileId = fileId,
+                            fileName = filename,
+                            picWidth = picWidth,
+                            picHeight = picHeight,
+                            picType = picType,
+                            appPicType = appPicType,
+                            buildVer = client.buildVer,
+                            srcTerm = srcTerm,
+                            platformType = platformType,
+                            originalPic = originalPic,
+                            buType = buType
+                        )
+                    )
+                )
+            )
+        }
+
+        sealed class Response : Packet {
+            class FileExists(
+                val fileId: Long,
+                val fileInfo: Cmd0x388.ImgInfo
+            ) : Response() {
+                override fun toString(): String {
+                    return "FileExists(fileId=$fileId, fileInfo=$fileInfo)"
+                }
+            }
+
+            class RequireUpload(
+                val fileId: Long,
+                val uKey: ByteArray,
+                val uploadIpList: List<Int>,
+                val uploadPortList: List<Int>
+            ) : Response() {
+                override fun toString(): String {
+                    return "RequireUpload(fileId=$fileId, uKey=${uKey.contentToString()})"
+                }
+            }
+
+            data class Failed(
+                val resultCode: Int,
+                val message: String
+            ) : Response()
+        }
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+            val resp0 = readProtoBuf(Cmd0x388.RspBody.serializer())
+            resp0.msgTryupImgRsp ?: error("cannot find `msgTryupImgRsp` from `Cmd0x388.RspBody`")
+            val resp = resp0.msgTryupImgRsp.first()
+            return when {
+                resp.result != 0 -> Response.Failed(resultCode = resp.result, message = resp.failMsg)
+                resp.boolFileExit -> Response.FileExists(fileId = resp.fileid, fileInfo = resp.msgImgInfo!!)
+                else -> Response.RequireUpload(fileId = resp.fileid, uKey = resp.upUkey, uploadIpList = resp.uint32UpIp!!, uploadPortList = resp.uint32UpPort!!)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/LongConn.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/LongConn.kt
new file mode 100644
index 000000000..69d1ace68
--- /dev/null
+++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/LongConn.kt
@@ -0,0 +1,42 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image
+
+import kotlinx.io.core.ByteReadPacket
+import kotlinx.io.core.writeFully
+import net.mamoe.mirai.data.Packet
+import net.mamoe.mirai.qqandroid.QQAndroidBot
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
+import net.mamoe.mirai.qqandroid.network.QQAndroidClient
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352Packet
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.GetImgUrlReq
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
+import net.mamoe.mirai.qqandroid.network.protocol.packet.buildLoginOutgoingPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.writeSsoPacket
+
+internal class LongConn {
+
+    object OffPicDown : OutgoingPacketFactory<OffPicDown.ImageDownPacketResponse>("LongConn.OffPicDown"){
+        operator fun invoke(client: QQAndroidClient, req: GetImgUrlReq): OutgoingPacket {
+            // TODO: 2020/1/24 测试: bodyType, subAppId
+            return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
+                writeSsoPacket(client, subAppId = 0, commandName = commandName, sequenceId = it) {
+                    val data = ProtoBufWithNullableSupport.dump(
+                        Cmd0x352Packet.serializer(),
+                        Cmd0x352Packet.createByImageRequest(req)
+                    )
+                    writeInt(data.size + 4)
+                    writeFully(data)
+                }
+            }
+        }
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): ImageDownPacketResponse {
+            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
+        }
+
+
+        sealed class ImageDownPacketResponse : Packet {
+            object Success : ImageDownPacketResponse()
+        }
+    }
+}
\ No newline at end of file
diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt
index e8d888ce9..fa20540c9 100644
--- a/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt
+++ b/mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt
@@ -6,7 +6,7 @@ import java.io.File
 
 fun main() {
     println(
-        File("""/Users/jiahua.liu/Desktop/QQAndroid-F/app/src/main/java/tencent/im/group/group_label/""")
+        File("""/Users/jiahua.liu/Desktop/QQAndroid-F/app/src/main/java/tencent/im/s2c/msgtype0x210/submsgtype0xc7/bussinfo/mutualmark""")
             .generateUnarrangedClasses().toMutableList().arrangeClasses().joinToString("\n\n")
     )
 }
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
index 89976370a..1e524b609 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
@@ -31,8 +31,6 @@ interface Group : Contact, CoroutineScope {
 
     /**
      * 在 [Group] 实例创建的时候查询一次. 并与事件同步事件更新
-     *
-     * **注意**: 获得的列表仅为这一时刻的成员列表的镜像. 它将不会被更新
      */
     val members: ContactList<Member>
 
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 a5c0f436f..73cbfa60e 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
@@ -5,6 +5,7 @@ package net.mamoe.mirai.message.data
 import kotlinx.serialization.Serializable
 
 sealed class Image : Message {
+    abstract val filepath: String
     abstract val md5: ByteArray
 
     abstract override fun toString(): String
@@ -15,7 +16,7 @@ sealed class Image : Message {
 }
 
 abstract class CustomFace : Image() {
-    abstract val filepath: String
+    abstract override val filepath: String
     abstract val fileId: Int
     abstract val serverIp: Int
     abstract val serverPort: Int
@@ -106,7 +107,7 @@ data class CustomFaceFromFile(
 abstract class NotOnlineImage : Image() {
     abstract val resourceId: String
     abstract override val md5: ByteArray
-    abstract val filepath: String
+    abstract override val filepath: String
     abstract val fileLength: Int
     abstract val height: Int
     abstract val width: Int
diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt
index a0d5d2617..8824c85c5 100644
--- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt
+++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/ExternalImage.kt
@@ -7,18 +7,10 @@ import kotlinx.io.core.Input
 import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.QQ
-import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.message.data.Image
+import net.mamoe.mirai.message.data.sendTo
 import net.mamoe.mirai.utils.io.toUHexString
 
-@Suppress("FunctionName")
-fun ExternalImage(
-    width: Int,
-    height: Int,
-    md5: ByteArray,
-    format: String,
-    data: ByteReadPacket
-): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining)
-
 /**
  * 外部图片. 图片数据还没有读取到内存.
  *
@@ -33,19 +25,53 @@ class ExternalImage(
     val md5: ByteArray,
     imageFormat: String,
     val input: Input,
-    val inputSize: Long
+    val inputSize: Long,
+    val filename: String
 ) {
-    private val format: String
-
-    init {
-        if (imageFormat == "JPEG" || imageFormat == "jpeg") {//必须转换
-            this.format = "jpg"
-        } else {
-            this.format = imageFormat
-        }
+    companion object {
+        operator fun invoke(
+            width: Int,
+            height: Int,
+            md5: ByteArray,
+            format: String,
+            data: ByteReadPacket,
+            filename: String
+        ): ExternalImage = ExternalImage(width, height, md5, format, data, data.remaining, filename)
     }
 
+    private val format: String = when (val it =imageFormat.toLowerCase()) {
+        "jpeg" -> "jpg" //必须转换
+        else -> it
+    }
+
+    /**
+     *
+     * ImgType:
+     *  JPG:    1000
+     *  PNG:    1001
+     *  WEBP:   1002
+     *  BMP:    1005
+     *  GIG:    2000
+     *  APNG:   2001
+     *  SHARPP: 1004
+     */
+    val imageType: Int
+        get() = when (format){
+            "jpg" -> 1000
+            "png" -> 1001
+            "webp" -> 1002
+            "bmp" -> 1005
+            "gig" -> 2000
+            "apng" -> 2001
+            "sharpp" -> 1004
+            else -> 1000 // unsupported, just make it jpg
+        }
+
     override fun toString(): String = "[ExternalImage(${width}x$height $format)]"
+
+    fun calculateImageResourceId(): String {
+        return "{${md5[0..3]}-${md5[4..5]}-${md5[6..7]}-${md5[8..9]}-${md5[10..15]}}.$format"
+    }
 }
 
 /**
diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt
index ddeccb0d1..d254520e9 100644
--- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt
+++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/ExternalImageJvm.kt
@@ -12,6 +12,7 @@ import kotlinx.io.core.copyTo
 import kotlinx.io.errors.IOException
 import kotlinx.io.streams.asInput
 import kotlinx.io.streams.asOutput
+import net.mamoe.mirai.utils.io.getRandomString
 import java.awt.image.BufferedImage
 import java.io.File
 import java.io.InputStream
@@ -44,7 +45,7 @@ fun BufferedImage.toExternalImage(formatName: String = "gif"): ExternalImage {
         })
     }
 
-    return ExternalImage(width, height, digest.digest(), formatName, buffer)
+    return ExternalImage(width, height, digest.digest(), formatName, buffer, getRandomString(10) + "." + formatName)
 }
 
 suspend inline fun BufferedImage.suspendToExternalImage(): ExternalImage = withContext(IO) { toExternalImage() }
@@ -66,7 +67,8 @@ fun File.toExternalImage(): ExternalImage {
         md5 = this.inputStream().use { it.md5() },
         imageFormat = image.formatName,
         input = this.inputStream().asInput(IoBuffer.Pool),
-        inputSize = this.length()
+        inputSize = this.length(),
+        filename = this.name
     )
 }
 
diff --git a/mirai-demos/mirai-demo-gentleman/build.gradle b/mirai-demos/mirai-demo-gentleman/build.gradle
index 606d90473..8aa4702ff 100644
--- a/mirai-demos/mirai-demo-gentleman/build.gradle
+++ b/mirai-demos/mirai-demo-gentleman/build.gradle
@@ -15,9 +15,12 @@ dependencies {
     implementation 'org.jsoup:jsoup:1.12.1'
 }
 
-mainClassName = "demo.gentleman.MainKt"
+run{
+    standardInput = System.in
+    mainClassName = "demo.gentleman.MainKt"
+}
 compileKotlin {
     kotlinOptions {
         freeCompilerArgs = ["-XXLanguage:+InlineClasses"]
     }
-}
\ No newline at end of file
+}