Parcourir la source

Message persistence

ryoii il y a 3 ans
Parent
commit
59ef5594ad

+ 6 - 3
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/action/group.kt

@@ -12,6 +12,7 @@ package net.mamoe.mirai.api.http.adapter.internal.action
 import net.mamoe.mirai.api.http.adapter.common.StateCode
 import net.mamoe.mirai.api.http.adapter.internal.dto.MemberDTO
 import net.mamoe.mirai.api.http.adapter.internal.dto.parameter.*
+import net.mamoe.mirai.api.http.spi.persistence.Context
 
 /**
  * 禁言所有人(需要相关权限)
@@ -65,9 +66,11 @@ internal suspend fun onQuit(dto: LongTargetDTO): StateCode {
 /**
  * 精华消息
  */
-internal suspend fun onSetEssence(essenceDTO: IntTargetDTO): StateCode {
-    val source = essenceDTO.session.sourceCache[essenceDTO.target]
-    return essenceDTO.session.bot.getGroup(source.target.id)?.run {
+internal suspend fun onSetEssence(essenceDTO: MessageIdDTO): StateCode {
+    // TODO fix
+    val context = Context(intArrayOf(essenceDTO.messageId), essenceDTO.session.bot.getGroupOrFail(essenceDTO.target))
+    val source = essenceDTO.session.sourceCache.getMessage(context)
+    return essenceDTO.session.bot.getGroup(essenceDTO.target)?.run {
         if (setEssenceMessage(source)) {
             StateCode.Success
         } else {

+ 30 - 14
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/action/message.kt

@@ -16,9 +16,13 @@ import net.mamoe.mirai.api.http.adapter.internal.convertor.toMessageChain
 import net.mamoe.mirai.api.http.adapter.internal.dto.*
 import net.mamoe.mirai.api.http.adapter.internal.dto.parameter.*
 import net.mamoe.mirai.api.http.context.session.Session
+import net.mamoe.mirai.api.http.spi.persistence.Context
 import net.mamoe.mirai.api.http.util.useStream
 import net.mamoe.mirai.api.http.util.useUrl
-import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.console.util.ConsoleExperimentalApi
+import net.mamoe.mirai.console.util.ContactUtils.getContact
+import net.mamoe.mirai.console.util.cast
+import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.message.data.Image.Key.queryUrl
@@ -29,8 +33,11 @@ import java.io.InputStream
 /**
  * 从缓存中通过 id 获取缓存消息
  */
-internal suspend fun onGetMessageFromId(dto: IntIdDTO): EventRestfulResult {
-    val source = dto.session.sourceCache[dto.id]
+@OptIn(ConsoleExperimentalApi::class)
+internal suspend fun onGetMessageFromId(dto: MessageIdDTO): EventRestfulResult {
+    val target = dto.session.bot.getContact(dto.target, false)
+    val context = Context(intArrayOf(dto.messageId), target)
+    val source = dto.session.sourceCache.getMessage(context)
 
     val packet = when (source) {
         is OnlineMessageSource.Outgoing.ToGroup -> GroupMessagePacketDTO(MemberDTO(source.target.botAsMember))
@@ -42,6 +49,13 @@ internal suspend fun onGetMessageFromId(dto: IntIdDTO): EventRestfulResult {
         is OnlineMessageSource.Incoming.FromFriend -> FriendMessagePacketDTO(QQDTO(source.sender))
         is OnlineMessageSource.Incoming.FromTemp -> TempMessagePacketDTO(MemberDTO(source.sender))
         is OnlineMessageSource.Incoming.FromStranger -> StrangerMessagePacketDTO(QQDTO(source.sender))
+        is OfflineMessageSource -> when(source.kind) {
+            MessageSourceKind.GROUP -> GroupMessagePacketDTO(MemberDTO(target.cast<Group>().getMemberOrFail(source.fromId)))
+            MessageSourceKind.FRIEND -> FriendMessagePacketDTO(QQDTO(target.cast<Friend>()))
+            // Maybe a bug
+            MessageSourceKind.TEMP -> TempMessagePacketDTO(MemberDTO(target.cast<Group>().getMemberOrFail(source.fromId)))
+            MessageSourceKind.STRANGER -> StrangerMessagePacketDTO(QQDTO(target.cast<Stranger>()))
+        }
     }
 
     packet.messageChain = messageChainOf(source, source.originalMessage)
@@ -70,7 +84,6 @@ private suspend fun <C : Contact> sendMessage(
  * 发送消息给好友
  */
 internal suspend fun onSendFriendMessage(sendDTO: SendDTO): SendRetDTO {
-    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache[q].quote() }
     val bot = sendDTO.session.bot
 
     fun findQQ(qq: Long): Contact = bot.getFriend(qq)
@@ -84,8 +97,9 @@ internal suspend fun onSendFriendMessage(sendDTO: SendDTO): SendRetDTO {
     }
 
     val cache = sendDTO.session.sourceCache
+    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache.getMessage(Context(intArrayOf(q), qq)).quote() }
     val receipt = sendMessage(quote, sendDTO.messageChain.toMessageChain(qq, cache), qq)
-    sendDTO.session.sourceCache.offer(receipt.source)
+    sendDTO.session.sourceCache.onMessage(receipt.source)
 
     return SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: -1)
 }
@@ -94,7 +108,6 @@ internal suspend fun onSendFriendMessage(sendDTO: SendDTO): SendRetDTO {
  * 发送消息到QQ群
  */
 internal suspend fun onSendGroupMessage(sendDTO: SendDTO): SendRetDTO {
-    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache[q].quote() }
     val bot = sendDTO.session.bot
 
     val group = when {
@@ -104,8 +117,9 @@ internal suspend fun onSendGroupMessage(sendDTO: SendDTO): SendRetDTO {
     }
 
     val cache = sendDTO.session.sourceCache
+    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache.getMessage(Context(intArrayOf(q), group)).quote() }
     val receipt = sendMessage(quote, sendDTO.messageChain.toMessageChain(group, cache), group)
-    sendDTO.session.sourceCache.offer(receipt.source)
+    sendDTO.session.sourceCache.onMessage(receipt.source)
 
     return SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: -1)
 }
@@ -114,7 +128,6 @@ internal suspend fun onSendGroupMessage(sendDTO: SendDTO): SendRetDTO {
  * 发送消息给临时会话
  */
 internal suspend fun onSendTempMessage(sendDTO: SendDTO): SendRetDTO {
-    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache[q].quote() }
     val bot = sendDTO.session.bot
 
     val member = when {
@@ -123,14 +136,14 @@ internal suspend fun onSendTempMessage(sendDTO: SendDTO): SendRetDTO {
     }
 
     val cache = sendDTO.session.sourceCache
+    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache.getMessage(Context(intArrayOf(q), member)).quote() }
     val receipt = sendMessage(quote, sendDTO.messageChain.toMessageChain(member, cache), member)
-    sendDTO.session.sourceCache.offer(receipt.source)
+    sendDTO.session.sourceCache.onMessage(receipt.source)
 
     return SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: -1)
 }
 
 internal suspend fun onSendOtherClientMessage(sendDTO: SendDTO): SendRetDTO {
-    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache[q].quote() }
     val bot = sendDTO.session.bot
 
     val client = when {
@@ -139,8 +152,9 @@ internal suspend fun onSendOtherClientMessage(sendDTO: SendDTO): SendRetDTO {
     }
 
     val cache = sendDTO.session.sourceCache
+    val quote = sendDTO.quote?.let { q -> sendDTO.session.sourceCache.getMessage(Context(intArrayOf(q), client)).quote() }
     val receipt = sendMessage(quote, sendDTO.messageChain.toMessageChain(client, cache), client)
-    sendDTO.session.sourceCache.offer(receipt.source)
+    sendDTO.session.sourceCache.onMessage(receipt.source)
 
     return SendRetDTO(messageId = receipt.source.ids.firstOrNull() ?: -1)
 }
@@ -160,7 +174,7 @@ internal suspend fun onSendImageMessage(sendDTO: SendImageDTO): StringListRestfu
     val ls = sendDTO.urls.map { url -> url.useUrl { contact.uploadImage(it) } }
     val receipt = contact.sendMessage(buildMessageChain { addAll(ls) })
 
-    sendDTO.session.sourceCache.offer(receipt.source)
+    sendDTO.session.sourceCache.onMessage(receipt.source)
     return StringListRestfulResult(data = ls.map { image -> image.imageId })
 }
 
@@ -203,8 +217,10 @@ internal suspend fun onUploadVoice(session: Session, stream: InputStream, type:
 /**
  * 消息撤回
  */
-internal suspend fun onRecall(recallDTO: IntTargetDTO): StateCode {
-    recallDTO.session.sourceCache[recallDTO.target].recall()
+@OptIn(ConsoleExperimentalApi::class)
+internal suspend fun onRecall(recallDTO: MessageIdDTO): StateCode {
+    recallDTO.session.sourceCache.getMessage(Context(intArrayOf(recallDTO.messageId),
+        recallDTO.session.bot.getContact(recallDTO.target, false))).recall()
     return StateCode.Success
 }
 

+ 6 - 6
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/convertor/convertor.kt

@@ -12,12 +12,12 @@ package net.mamoe.mirai.api.http.adapter.internal.convertor
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import net.mamoe.mirai.api.http.adapter.internal.dto.*
-import net.mamoe.mirai.api.http.context.cache.MessageSourceCache
+import net.mamoe.mirai.api.http.spi.persistence.Context
+import net.mamoe.mirai.api.http.spi.persistence.Persistence
 import net.mamoe.mirai.api.http.util.*
 import net.mamoe.mirai.contact.AudioSupported
 import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.contact.UserOrBot
 import net.mamoe.mirai.event.events.BotEvent
 import net.mamoe.mirai.event.events.MessageEvent
 import net.mamoe.mirai.message.code.MiraiCode
@@ -52,7 +52,7 @@ internal suspend fun BotEvent.toDTO(): EventDTO = when (this) {
 /**
  * 转换一条消息链
  */
-internal suspend fun MessageChainDTO.toMessageChain(contact: Contact, cache: MessageSourceCache): MessageChain {
+internal suspend fun MessageChainDTO.toMessageChain(contact: Contact, cache: Persistence): MessageChain {
     return buildMessageChain { [email protected] { it.toMessage(contact, cache)?.let(::add) } }
 }
 
@@ -60,7 +60,7 @@ internal suspend fun MessageChainDTO.toMessageChain(contact: Contact, cache: Mes
  * 转换一个具体的消息类型
  */
 @OptIn(MiraiInternalApi::class, MiraiExperimentalApi::class)
-internal suspend fun MessageDTO.toMessage(contact: Contact, cache: MessageSourceCache) = when (this) {
+internal suspend fun MessageDTO.toMessage(contact: Contact, cache: Persistence) = when (this) {
     is AtDTO -> (contact as Group).getOrFail(target).at()
     is AtAllDTO -> AtAll
     is FaceDTO -> when {
@@ -81,8 +81,8 @@ internal suspend fun MessageDTO.toMessage(contact: Contact, cache: MessageSource
     is ForwardMessageDTO -> buildForwardMessage(contact) {
         nodeList.forEach {
             if (it.messageId != null) {
-                cache.getOrDefault(it.messageId, null)?.apply {
-                    add(sender as UserOrBot, originalMessage, time)
+                cache.getMessageOrNull(Context(intArrayOf(it.messageId), contact))?.apply {
+                    add(fromId, "$fromId", originalMessage, time)
                 }
             } else if (it.senderId != null && it.senderName != null && it.messageChain != null) {
                 add(it.senderId, it.senderName, it.messageChain.toMessageChain(contact, cache), it.time ?: -1)

+ 6 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/internal/dto/parameter/message.kt

@@ -51,3 +51,9 @@ internal class UploadImageRetDTO(
 internal class UploadVoiceRetDTO(
     val voiceId: String,
 ) : DTO
+
+@Serializable
+internal data class MessageIdDTO(
+    val target: Long,
+    val messageId: Int,
+) : AuthedDTO()

+ 1 - 1
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/adapter/webhook/WebhookAdapter.kt

@@ -52,7 +52,7 @@ class WebhookAdapter : MahAdapter("webhook") {
 
             setting.destinations.forEach {
                 if (this is MessageEvent) {
-                    MahContextHolder.sessionManager.getCache(bot.id).offer(source)
+                    MahContextHolder.sessionManager.getCache(bot).onMessage(source)
                 }
 
                 bot.launch { hook(it, data, bot) }

+ 1 - 1
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/MahContext.kt

@@ -111,7 +111,7 @@ open class MahContext internal constructor() {
     fun handleBotEvent(session: Session, event: BotEvent) = adapters.forEach { adapter ->
         session.launch {
             if (event is MessageEvent) {
-                session.sourceCache.offer(event.source)
+                session.sourceCache.onMessage(event.source)
             }
             adapter.onReceiveBotEvent(event, session)
         }

+ 0 - 53
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/cache/MessageSourceCache.kt

@@ -1,53 +0,0 @@
-/*
- * Copyright 2020 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/master/LICENSE
- */
-
-package net.mamoe.mirai.api.http.context.cache
-
-import net.mamoe.mirai.message.data.OnlineMessageSource
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.ConcurrentLinkedQueue
-
-class MessageSourceCache(cacheSize: Int) : LRUCache<Int, OnlineMessageSource>(cacheSize) {
-
-    fun offer(source: OnlineMessageSource) {
-        put(source.ids.firstOrNull() ?: 0, source)
-    }
-
-    override operator fun get(key: Int): OnlineMessageSource = super.get(key)
-        ?: throw NoSuchElementException()
-
-    fun getOrDefault(key: Int, default: OnlineMessageSource?) = super.get(key) ?: default
-}
-
-open class LRUCache<K: Any, V: Any>(private val cacheSize: Int) {
-
-    private val lruQueen = ConcurrentLinkedQueue<K>()
-    private val cacheData = ConcurrentHashMap<K, V>()
-
-    open fun get(key: K): V? {
-        return cacheData[key]
-    }
-
-    fun put(key: K, value: V) {
-        val old = cacheData.put(key, value)
-        if (old == null) {
-            lruQueen.offer(key)
-            lru()
-        }
-    }
-
-    private fun lru() {
-        while (lruQueen.size > cacheSize) {
-            val poll = lruQueen.poll()
-            cacheData.remove(poll)
-        }
-    }
-
-    fun size() = cacheData.size
-}

+ 3 - 3
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/api.kt

@@ -10,8 +10,8 @@
 package net.mamoe.mirai.api.http.context.session.manager
 
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.api.http.context.cache.MessageSourceCache
-import net.mamoe.mirai.api.http.context.session.*
+import net.mamoe.mirai.api.http.context.session.Session
+import net.mamoe.mirai.api.http.spi.persistence.Persistence
 
 /**
  * Session管理
@@ -48,5 +48,5 @@ interface SessionManager {
 
     fun authedSessions(): List<Session>
 
-    fun getCache(id: Long): MessageSourceCache
+    fun getCache(bot: Bot): Persistence
 }

+ 14 - 9
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/manager/default.kt

@@ -11,19 +11,24 @@ package net.mamoe.mirai.api.http.context.session.manager
 
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.api.http.context.MahContext
-import net.mamoe.mirai.api.http.context.cache.MessageSourceCache
 import net.mamoe.mirai.api.http.context.session.ListenableSessionWrapper
 import net.mamoe.mirai.api.http.context.session.Session
 import net.mamoe.mirai.api.http.context.session.StandardSession
-import net.mamoe.mirai.api.http.setting.MainSetting
+import net.mamoe.mirai.api.http.spi.persistence.Persistence
+import net.mamoe.mirai.api.http.spi.persistence.PersistenceFactory
+import net.mamoe.mirai.api.http.spi.persistence.PersistenceManager
 
 class DefaultSessionManager(override val verifyKey: String, val context: MahContext) : SessionManager {
+
+    private val persistenceManager: PersistenceManager = PersistenceManager("built-id")
+    private val persistenceFactory: PersistenceFactory = persistenceManager.loadFactory()
+
     private val sessionMap: MutableMap<String, Session> = mutableMapOf()
-    private val cacheMap: MutableMap<Long, MessageSourceCache> = mutableMapOf()
+    private val cacheMap: MutableMap<Long, Persistence> = mutableMapOf()
 
     override fun createOneTimeSession(bot: Bot) =
         StandardSession("", manager = this).also { oneTimeSession ->
-            oneTimeSession.authWith(bot, getCache(bot.id))
+            oneTimeSession.authWith(bot, getCache(bot))
         }
 
     override fun createTempSession() = createTempSession(generateSessionKey())
@@ -41,7 +46,7 @@ class DefaultSessionManager(override val verifyKey: String, val context: MahCont
         }
         session.ref()
         session.putExtElement(ListenableSessionWrapper.botEventHandler, context::handleBotEvent)
-        session.authWith(bot, getCache(bot.id))
+        session.authWith(bot, getCache(bot))
         return session
     }
 
@@ -63,13 +68,13 @@ class DefaultSessionManager(override val verifyKey: String, val context: MahCont
     override fun authedSessions(): List<Session> =
         sessionMap.filterValues { it.isAuthed }.map { it.value }
 
-    override fun getCache(id: Long): MessageSourceCache {
-        var cache = cacheMap[id]
+    override fun getCache(bot: Bot): Persistence {
+        var cache = cacheMap[bot.id]
         if (cache == null) {
             synchronized(this) {
                 if (cache == null) {
-                    cache = MessageSourceCache(MainSetting.cacheSize)
-                    cacheMap[id] = cache!!
+                    cache = persistenceFactory.getService(bot)
+                    cacheMap[bot.id] = cache!!
                 }
             }
         }

+ 7 - 7
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/context/session/session.kt

@@ -12,8 +12,8 @@ package net.mamoe.mirai.api.http.context.session
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.*
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.api.http.context.cache.MessageSourceCache
 import net.mamoe.mirai.api.http.context.session.manager.SessionManager
+import net.mamoe.mirai.api.http.spi.persistence.Persistence
 import net.mamoe.mirai.event.Listener
 import net.mamoe.mirai.event.events.BotEvent
 import kotlin.coroutines.CoroutineContext
@@ -30,17 +30,17 @@ class StandardSession constructor(
     private val lifeCounter = atomic(0)
 
     private lateinit var _bot: Bot
-    private lateinit var _cache: MessageSourceCache
+    private lateinit var _cache: Persistence
     private var _isAuthed = false
     private var _closed = false
     private var _closing = false
 
     override val bot: Bot get() = if (isAuthed) _bot else throw RuntimeException("Session is not authed")
-    override val sourceCache: MessageSourceCache get() = if (isAuthed) _cache else throw RuntimeException("Session is not authed")
+    override val sourceCache: Persistence get() = if (isAuthed) _cache else throw RuntimeException("Session is not authed")
     override val isAuthed get() = _isAuthed
     override val isClosed get() = _closed
 
-    override fun authWith(bot: Bot, sourceCache: MessageSourceCache) {
+    override fun authWith(bot: Bot, sourceCache: Persistence) {
         if(isAuthed) {
             return
         }
@@ -91,7 +91,7 @@ class ListenableSessionWrapper(val session: Session) : Session by session {
      * 代理方法
      * 正常执行认证流程后, 直接开启
      */
-    override fun authWith(bot: Bot, sourceCache: MessageSourceCache) {
+    override fun authWith(bot: Bot, sourceCache: Persistence) {
         session.authWith(bot, sourceCache)
         val job = getExtElement(listenerJob)
         if (job == null) {
@@ -161,12 +161,12 @@ interface Session : CoroutineScope {
 
     val isAuthed: Boolean
     val isClosed: Boolean
-    val sourceCache: MessageSourceCache
+    val sourceCache: Persistence
 
     /**
      * 通过 Bot 和 cache 完成 Session 的认证过程, 执行 AuthedSession 初始化
      */
-    fun authWith(bot: Bot, sourceCache: MessageSourceCache)
+    fun authWith(bot: Bot, sourceCache: Persistence)
 
     /**
      * 引用 Session, 可以使得 Session 在关闭时先检查引用计数

+ 94 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/spi/persistence/Persistence.kt

@@ -0,0 +1,94 @@
+package net.mamoe.mirai.api.http.spi.persistence
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.message.data.MessageSourceBuilder
+import net.mamoe.mirai.message.data.OnlineMessageSource
+
+/**
+ * 持久化器工厂, 通过 SPI 获取该工厂实例, 并由各实现自行实现 [Persistence] 服务
+ */
+interface PersistenceFactory {
+
+    /**
+     * 名称, 会通过配置文件设置的名称加载对应的 [Persistence] 服务
+     */
+    fun getName(): String
+
+    /**
+     * 在此实现 [Persistence] 服务的初始化逻辑
+     */
+    fun getService(bot: Bot): Persistence
+}
+
+/**
+ * 消息持久化组件接口,通过 SPI 加载。需要实现 [PersistenceFactory] 进行该接口实例的初始化
+ *
+ * 该对象可以是单例的、也可以是以 Bot 为作用域的, 该类通过 [PersistenceFactory.getService] 初始化
+ *
+ * 该实现作用于发送消息时,获取 <b>引用回复<b> 的上下文, 以及重复发送消息的引用。同时, 也是提供给依赖插件的持久化消息数据接口, 实现者应该实现较为完整的接口逻辑
+ *
+ * @see PersistenceFactory
+ */
+interface Persistence {
+
+    /**
+     * 接收消息时逻辑
+     *
+     * 由 [Persistence] 服务自行确认该消息是否需要进行持久化
+     *
+     * 需要持久化
+     * + 消息id
+     * + 消息主体类型
+     * + 发送人id
+     * + 主体id
+     * + 发送时间
+     * + 内部id
+     * + 原消息序列
+     *
+     * 注意:持久化消息应该按照消息主体分组, 否则不同消息主体的 id 可能出现重复.
+     * 消息 id 是一个整形数组,在消息分片发送时会出现多个 id,
+     * 逻辑上使用其中一个 id 即可以定位到这条消息,但发送引用回复时,需要提供完整 id 数组.
+     *
+     * @see MessageSourceBuilder
+     */
+    fun onMessage(messageSource: OnlineMessageSource)
+
+    /**
+     * 获取持久化消息
+     *
+     * 主要通过 id 获取持久化的消息.
+     *
+     * 注意: 消息 id 是一个整形数组, 但实际操作上, 对于分片消息, 可能只会传入一个 id (第一个 id)
+     *
+     * @param context 消息上下文, 包含当前会话的消息主体以及消息主体类型
+     */
+    fun getMessage(context: Context): MessageSource
+
+    fun getMessageOrNull(context: Context): MessageSource?
+}
+
+data class Context(
+    val ids: IntArray,
+    val subject: Contact,
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as Context
+
+        if (!ids.contentEquals(other.ids)) return false
+        if (subject != other.subject) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = ids.contentHashCode()
+        result = 31 * result + subject.hashCode()
+        return result
+    }
+
+}

+ 91 - 0
mirai-api-http/src/main/kotlin/net/mamoe/mirai/api/http/spi/persistence/builtinPersistence.kt

@@ -0,0 +1,91 @@
+package net.mamoe.mirai.api.http.spi.persistence
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.message.data.OnlineMessageSource
+import java.util.ServiceLoader
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedQueue
+
+/**
+ * 加载指定消息持久化器工厂,若无法加载,则加载默认的 [BuiltinPersistenceFactory]
+ */
+class PersistenceManager(private val serviceName: String) {
+
+    fun loadFactory(): PersistenceFactory {
+        return ServiceLoader.load(PersistenceFactory::class.java)
+            .firstOrNull { it.getName() == serviceName }
+            ?: BuiltinPersistenceFactory()
+    }
+}
+
+/**
+ * 内置的持久化工厂,实例化 [BuiltinPersistence] 持久化器
+ */
+class BuiltinPersistenceFactory : PersistenceFactory {
+
+    /**
+     *
+     */
+    override fun getName(): String {
+        return "built-in"
+    }
+
+    override fun getService(bot: Bot): Persistence {
+        return BuiltinPersistence(1024)
+    }
+}
+
+/**
+ * 内置消息持久化器,使用内存存储,重启丢失
+ *
+ * 以 64bit 整形作为 key,高 32bit 为消息主体(群、好友、临时会话等),低 32bit 为消息 id (只取首个,分片消息也是首个)
+ *
+ * 可能会出现的问题:同一个消息主体(主要是群聊)出现重复 id 后会被覆盖
+ */
+class BuiltinPersistence(cacheSize: Int) : Persistence, QueueCache<Long, OnlineMessageSource>(cacheSize) {
+    override fun onMessage(messageSource: OnlineMessageSource) {
+        messageSource.ids.firstOrNull()?.let { id ->
+            val key = (messageSource.subject.id shl 32) or (id.toLong() and 0xFFFFFFFF)
+            put(key, messageSource)
+        }
+    }
+
+    override fun getMessage(context: Context): MessageSource {
+        return getMessageOrNull(context) ?: throw NoSuchElementException()
+    }
+
+    override fun getMessageOrNull(context: Context): MessageSource? {
+        return context.ids.firstOrNull()?.let { id ->
+            val key = (context.subject.id shl 32) or (id.toLong() and 0xFFFFFFFF)
+            return super.get(key)
+        }
+    }
+}
+
+open class QueueCache<K : Any, V : Any>(private val cacheSize: Int) {
+
+    private val keyQueue = ConcurrentLinkedQueue<K>()
+    private val dataCache = ConcurrentHashMap<K, V>()
+
+    open fun get(key: K): V? {
+        return dataCache[key]
+    }
+
+    fun put(key: K, value: V) {
+        val old = dataCache.put(key, value)
+        if (old == null) {
+            keyQueue.offer(key)
+            resize()
+        }
+    }
+
+    private fun resize() {
+        while (keyQueue.size > cacheSize) {
+            val poll = keyQueue.poll()
+            dataCache.remove(poll)
+        }
+    }
+
+    fun size() = dataCache.size
+}

+ 2 - 2
mirai-api-http/src/test/kotlin/launch/adapter/LaunchTester.kt

@@ -42,7 +42,7 @@ abstract class LaunchTester {
             MahPluginImpl.start {
                 sessionManager = DefaultSessionManager(verifyKey, this)
                 enableVerify = false
-                singleMode = true
+                singleMode = false
                 debug = true
 
                 for (adapter in adapters) {
@@ -54,7 +54,7 @@ abstract class LaunchTester {
         val bot = BotFactory.newBot(qq, password) {
             fileBasedDeviceInfo("device.json")
 
-            protocol = BotConfiguration.MiraiProtocol.ANDROID_WATCH
+            protocol = BotConfiguration.MiraiProtocol.ANDROID_PHONE
         }
 
         bot.login()

+ 2 - 1
mirai-api-http/src/test/kotlin/net/mamoe/mirai/api/http/context/cache/TestSourceCache.kt

@@ -9,6 +9,7 @@
 
 package net.mamoe.mirai.api.http.context.cache
 
+import net.mamoe.mirai.api.http.spi.persistence.QueueCache
 import kotlin.concurrent.thread
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -48,7 +49,7 @@ class TestSourceCache {
     }
 }
 
-class SourceCache(maxSize: Int) : LRUCache<Int, Int>(maxSize) {
+class SourceCache(maxSize: Int) : QueueCache<Int, Int>(maxSize) {
 
     fun offer(value: Int) = put(value, value)
 }