diff --git a/mirai-core-api/src/commonMain/kotlin/IMirai.kt b/mirai-core-api/src/commonMain/kotlin/IMirai.kt
index 151fe8412..f1e453172 100644
--- a/mirai-core-api/src/commonMain/kotlin/IMirai.kt
+++ b/mirai-core-api/src/commonMain/kotlin/IMirai.kt
@@ -59,6 +59,12 @@ public interface IMirai : LowLevelApiAccessor {
     @MiraiInternalApi
     public val Http: HttpClient
 
+    public fun getUin(contactOrBot: ContactOrBot): Long {
+        return if (contactOrBot is Group)
+            calculateGroupUinByGroupCode(contactOrBot.id)
+        else contactOrBot.id
+    }
+
     /**
      * 使用 groupCode 计算 groupUin. 这两个值仅在 mirai 内部协议区分, 一般人使用时无需在意.
      */
@@ -137,15 +143,11 @@ public interface IMirai : LowLevelApiAccessor {
      *
      * @param ids 即 [MessageSource.ids]
      * @param internalIds 即 [MessageSource.internalIds]
-     *
-     * @param fromUin 为用户时为 [Friend.id], 为群时需使用 [IMirai.calculateGroupUinByGroupCode] 计算
-     * @param targetUin 为用户时为 [Friend.id], 为群时需使用 [IMirai.calculateGroupUinByGroupCode] 计算
      */
-    @MiraiExperimentalApi("This is very experimental and is subject to change.")
     public fun constructMessageSource(
         botId: Long,
         kind: MessageSourceKind,
-        fromUin: Long, targetUin: Long,
+        fromId: Long, targetId: Long,
         ids: IntArray, time: Int, internalIds: IntArray,
         originalMessage: MessageChain
     ): OfflineMessageSource
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
index 04de05096..76986de20 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
@@ -66,7 +66,7 @@ import net.mamoe.mirai.utils.safeCast
  * @see OnlineMessageSource 在线消息的 [MessageSource]
  * @see OfflineMessageSource 离线消息的 [MessageSource]
  *
- * @see buildMessageSource 构造一个 [OfflineMessageSource]
+ * @see buildMessageSource 构建一个 [OfflineMessageSource]
  */
 @Serializable(MessageSource.Serializer::class)
 public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle {
@@ -445,6 +445,8 @@ public sealed class OnlineMessageSource : MessageSource() {
  * 此消息源可能来自一条与机器人无关的消息. 因此无法提供对象化的 `sender` 或 `target` 获取.
  *
  * @see buildMessageSource 构建一个 [OfflineMessageSource]
+ * @see IMirai.constructMessageSource
+ * @see OnlineMessageSource.toOffline
  */
 public abstract class OfflineMessageSource : MessageSource() {
     public companion object Key :
diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSourceBuilder.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSourceBuilder.kt
index 27f5f7314..ddcd7a8f0 100644
--- a/mirai-core-api/src/commonMain/kotlin/message/data/MessageSourceBuilder.kt
+++ b/mirai-core-api/src/commonMain/kotlin/message/data/MessageSourceBuilder.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2020 Mamoe Technologies and contributors.
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
  *
  *  此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  *  Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@@ -14,14 +14,12 @@
 package net.mamoe.mirai.message.data
 
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.IMirai
 import net.mamoe.mirai.Mirai
-import net.mamoe.mirai.contact.*
-import net.mamoe.mirai.message.data.MessageSource.Key.isAboutFriend
-import net.mamoe.mirai.message.data.MessageSource.Key.isAboutGroup
-import net.mamoe.mirai.message.data.MessageSource.Key.isAboutTemp
+import net.mamoe.mirai.contact.ContactOrBot
 import net.mamoe.mirai.message.data.MessageSource.Key.quote
 import net.mamoe.mirai.message.data.MessageSource.Key.recall
-import net.mamoe.mirai.utils.MiraiExperimentalApi
+import net.mamoe.mirai.message.data.MessageSourceBuilder.Companion.create
 import net.mamoe.mirai.utils.currentTimeSeconds
 
 /**
@@ -29,7 +27,7 @@ import net.mamoe.mirai.utils.currentTimeSeconds
  */
 @JvmName("toOfflineMessageSource")
 public fun OnlineMessageSource.toOffline(): OfflineMessageSource =
-    OfflineMessageSourceByOnline(this)
+    Mirai.constructMessageSource(botId, kind, fromId, targetId, ids, time, internalIds, originalMessage)
 
 ///////////////
 //// AMEND ////
@@ -41,24 +39,27 @@ public fun OnlineMessageSource.toOffline(): OfflineMessageSource =
  *
  * @see buildMessageSource 查看更多说明
  */
-@MiraiExperimentalApi
 @JvmName("copySource")
 public fun MessageSource.copyAmend(
     block: MessageSourceAmender.() -> Unit
-): OfflineMessageSource = toMutableOffline().apply(block)
+): OfflineMessageSource = MessageSourceAmender(this).apply(block).run {
+    Mirai.constructMessageSource(botId, kind, fromId, targetId, ids, time, internalIds, originalMessage)
+}
 
 /**
  * 仅于 [copyAmend] 中修改 [MessageSource]
  */
-public interface MessageSourceAmender {
-    public var kind: MessageSourceKind
-    public var fromUin: Long
-    public var targetUin: Long
-    public var ids: IntArray
-    public var time: Int
-    public var internalIds: IntArray
+public class MessageSourceAmender internal constructor(
+    origin: MessageSource,
+) : MessageSourceBuilder() {
+    public var kind: MessageSourceKind = origin.kind
+    public var originalMessage: MessageChain = origin.originalMessage
 
-    public var originalMessage: MessageChain
+    public override var fromId: Long = origin.fromId
+    public override var targetId: Long = origin.targetId
+    public override var ids: IntArray = origin.ids
+    public override var time: Int = origin.time
+    public override var internalIds: IntArray = origin.internalIds
 
     /** 从另一个 [MessageSource] 中复制 [ids], [internalIds], [time]*/
     public fun metadataFrom(another: MessageSource) {
@@ -78,8 +79,8 @@ public interface MessageSourceAmender {
  * 构建一个 [OfflineMessageSource]
  *
  * ### 参数
- * 一个 [OfflineMessageSource] 须要以下参数:
- * - 发送人和发送目标: 通过 [MessageSourceBuilder.sendTo] 设置
+ * 一个 [OfflineMessageSource] 需要以下参数:
+ * - 发送人和发送目标: 通过 [MessageSourceBuilder.sender], [MessageSourceBuilder.target] 设置
  * - 消息元数据 (即 [MessageSource.ids], [MessageSource.internalIds], [MessageSource.time])
  *   元数据用于 [撤回][MessageSource.recall], [引用回复][MessageSource.quote], 和官方客户端定位原消息.
  *   可通过 [MessageSourceBuilder.ids], [MessageSourceBuilder.time], [MessageSourceBuilder.internalIds] 设置
@@ -92,8 +93,9 @@ public interface MessageSourceAmender {
  *
  * ### 实例
  * ```
- * bot.buildMessageSource {
- *     bot sendTo target // 指定发送人和发送目标
+ * bot.buildMessageSource(MessageSourceKind.GROUP) {
+ *     from(bot)
+ *     target(target)
  *     metadata(source) // 从另一个消息源复制 ids, internalIds, time
  *
  *     messages { // 指定消息内容
@@ -101,40 +103,49 @@ public interface MessageSourceAmender {
  *     }
  * }
  * ```
+ *
+ * @see copyAmend
  */
-@JvmSynthetic
-@MiraiExperimentalApi
-public fun Bot.buildMessageSource(block: MessageSourceBuilder.() -> Unit): MessageSource {
-    val builder = MessageSourceBuilderImpl().apply(block)
-    return Mirai.constructMessageSource(
-        this.id,
-        builder.kind ?: error("You must call `Contact.sendTo(Contact)` when `buildMessageSource`"),
-        builder.fromUin,
-        builder.targetUin,
-        builder.ids,
-        builder.time,
-        builder.internalIds,
-        builder.originalMessages.build()
-    )
+public fun IMirai.buildMessageSource(
+    botId: Long,
+    kind: MessageSourceKind,
+    block: MessageSourceBuilder.() -> Unit
+): OfflineMessageSource = MessageSourceBuilder.create().apply(block).run {
+    Mirai.constructMessageSource(botId, kind, fromId, targetId, ids, time, internalIds, originalMessages.build())
 }
 
 /**
+ * 构建一个 [OfflineMessageSource]
+ *
  * @see buildMessageSource
  */
-public abstract class MessageSourceBuilder {
-    internal abstract var kind: MessageSourceKind?
-    internal abstract var fromUin: Long
-    internal abstract var targetUin: Long
+public fun Bot.buildMessageSource(
+    kind: MessageSourceKind,
+    block: MessageSourceBuilder.() -> Unit
+): OfflineMessageSource = Mirai.buildMessageSource(this.id, kind, block)
 
-    internal abstract var ids: IntArray
-    internal abstract var time: Int
-    internal abstract var internalIds: IntArray
+
+/**
+ * @see buildMessageSource
+ * @see create
+ */
+public open class MessageSourceBuilder internal constructor() {
+    public open var fromId: Long = 0
+    public open var targetId: Long = 0
+
+    public open var ids: IntArray = intArrayOf()
+
+    /**
+     * seconds
+     * @see MessageSource.time
+     */
+    public open var time: Int = currentTimeSeconds().toInt()
+    public open var internalIds: IntArray = intArrayOf()
 
     @PublishedApi
     internal val originalMessages: MessageChainBuilder = MessageChainBuilder()
 
     public fun time(from: MessageSource): MessageSourceBuilder = apply { this.time = from.time }
-    public val now: Int get() = currentTimeSeconds().toInt()
     public fun time(value: Int): MessageSourceBuilder = apply { this.time = value }
 
     public fun internalId(from: MessageSource): MessageSourceBuilder = apply { this.internalIds = from.internalIds }
@@ -158,11 +169,10 @@ public abstract class MessageSourceBuilder {
      * 从另一个 [MessageSource] 复制所有信息, 包括消息内容. 不会清空已有消息.
      */
     public fun allFrom(source: MessageSource): MessageSourceBuilder {
-        this.kind = determineKind(source)
         this.ids = source.ids
         this.time = source.time
-        this.fromUin = source.fromId
-        this.targetUin = source.targetId
+        this.fromId = source.fromId
+        this.targetId = source.targetId
         this.internalIds = source.internalIds
         this.originalMessages.addAll(source.originalMessage)
         return this
@@ -194,101 +204,38 @@ public abstract class MessageSourceBuilder {
     public fun clearMessages(): MessageSourceBuilder = apply { this.originalMessages.clear() }
 
     /**
-     * 设置 [发送人][this] 和 [发送目标][target], 并自动判断 [kind]
+     * 设置发信人
      */
-    @JvmSynthetic
-    public abstract infix fun ContactOrBot.sendTo(target: ContactOrBot): MessageSourceBuilder
+    public fun sender(sender: ContactOrBot): MessageSourceBuilder = apply {
+        this.fromId = Mirai.getUin(sender)
+    }
+
+    /**
+     * @see IMirai.getUin
+     */
+    public fun sender(uin: Long): MessageSourceBuilder = apply {
+        this.fromId = uin
+    }
+
+    /**
+     * 设置发信目标
+     */
+    public fun target(target: ContactOrBot): MessageSourceBuilder = apply {
+        this.targetId = Mirai.getUin(target)
+    }
+
+    /**
+     * @see IMirai.getUin
+     */
+    public fun target(uin: Long): MessageSourceBuilder = apply {
+        this.targetId = uin
+    }
 
     public fun setSenderAndTarget(sender: ContactOrBot, target: ContactOrBot): MessageSourceBuilder =
-        sender sendTo target
-}
+        sender(sender).target(target)
 
-
-//////////////////
-//// INTERNAL ////
-//////////////////
-
-
-internal class MessageSourceBuilderImpl : MessageSourceBuilder() {
-    override var kind: MessageSourceKind? = null
-    override var fromUin: Long = 0
-    override var targetUin: Long = 0
-
-    override var ids: IntArray = intArrayOf()
-    override var time: Int = currentTimeSeconds().toInt()
-    override var internalIds: IntArray = intArrayOf()
-
-    @JvmSynthetic
-    override fun ContactOrBot.sendTo(target: ContactOrBot): MessageSourceBuilder {
-        fromUin = if (this is Group) {
-            Mirai.calculateGroupUinByGroupCode(this.id)
-        } else this.id
-
-        targetUin = if (target is Group) {
-            Mirai.calculateGroupUinByGroupCode(target.id)
-        } else target.id
-
-        check(this != target) { "sender and target mustn't be the same" }
-
-        kind = when {
-            this is Group || target is Group -> MessageSourceKind.GROUP
-            this is Member || target is Member -> MessageSourceKind.TEMP
-            this is Bot && target is Friend -> MessageSourceKind.FRIEND
-            this is Friend && target is Bot -> MessageSourceKind.FRIEND
-            this is Stranger || target is Stranger -> MessageSourceKind.STRANGER
-            else -> throw IllegalArgumentException("Cannot determine source kind for sender $this and target $target")
-        }
-        return this@MessageSourceBuilderImpl
+    public companion object {
+        @JvmStatic
+        public fun create(): MessageSourceBuilder = MessageSourceBuilder()
     }
-}
-
-
-@JvmSynthetic
-internal fun MessageSource.toMutableOffline(): MutableOfflineMessageSourceByOnline =
-    MutableOfflineMessageSourceByOnline(this)
-
-internal class MutableOfflineMessageSourceByOnline(
-    origin: MessageSource
-) : OfflineMessageSource(), MessageSourceAmender {
-    override var kind: MessageSourceKind = determineKind(origin)
-    override var fromUin: Long
-        get() = fromId
-        set(value) {
-            fromId = value
-        }
-    override var targetUin: Long
-        get() = targetId
-        set(value) {
-            targetId = value
-        }
-    override val botId: Long = origin.botId
-    override var ids: IntArray = origin.ids
-    override var internalIds: IntArray = origin.internalIds
-    override var time: Int = origin.time
-    override var fromId: Long = origin.fromId
-    override var targetId: Long = origin.targetId
-    override var originalMessage: MessageChain = origin.originalMessage
-}
-
-private fun determineKind(source: MessageSource): MessageSourceKind {
-    return when {
-        source.isAboutGroup() -> MessageSourceKind.GROUP
-        source.isAboutFriend() -> MessageSourceKind.FRIEND
-        source.isAboutTemp() -> MessageSourceKind.TEMP
-        else -> error("stub")
-    }
-}
-
-internal class OfflineMessageSourceByOnline(
-    private val onlineMessageSource: OnlineMessageSource
-) : OfflineMessageSource() {
-    override val kind: MessageSourceKind
-        get() = onlineMessageSource.kind
-    override val botId: Long get() = onlineMessageSource.botId
-    override val ids: IntArray get() = onlineMessageSource.ids
-    override val internalIds: IntArray get() = onlineMessageSource.internalIds
-    override val time: Int get() = onlineMessageSource.time
-    override val fromId: Long get() = onlineMessageSource.fromId
-    override val targetId: Long get() = onlineMessageSource.targetId
-    override val originalMessage: MessageChain get() = onlineMessageSource.originalMessage
-}
+}
\ No newline at end of file
diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt
index c21b1836e..063add083 100644
--- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt
+++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt
@@ -892,14 +892,14 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
     override fun constructMessageSource(
         botId: Long,
         kind: MessageSourceKind,
-        fromUin: Long,
-        targetUin: Long,
+        fromId: Long,
+        targetId: Long,
         ids: IntArray,
         time: Int,
         internalIds: IntArray,
         originalMessage: MessageChain
     ): OfflineMessageSource = OfflineMessageSourceImplData(
-        kind, ids, botId, time, fromUin, targetUin, originalMessage, internalIds
+        kind, ids, botId, time, fromId, targetId, originalMessage, internalIds
     )
 
 }
\ No newline at end of file