Add reply-to-message capability with ID-addressable history

Lets the bot quote-reply to a specific message via an optional replyTo
on sendSingleMessage, and reworks history serialization so the LLM can
address messages and stop confusing quoted content with the quoter.

- Serialize each history line with a short [n] id; consecutive messages
  from the same sender continue under "[n]  └". A per-subject ReplyIndex
  maps [n] -> MessageRecord, kept alive with the context cache so ids
  stay continuous across cached turns.
- Replace inlined quote text with a reference: "↩[k]" when the quoted
  message is in-window, otherwise "↩(author:"snippet…")". This removes
  the ambiguity where A quoting B looked like A's own speech.
- Collapse forwarded messages to "[转发消息·N条:title]".
- sendSingleMessage accepts replyTo (the [n]); it resolves the record via
  MessageRecord.toMessageSource() and prepends a QuoteReply, falling back
  to a plain send with a note if the source is gone. ids may be null, so
  numbering still happens but such records can't be reply targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 22:46:59 +08:00
parent eedbd55f62
commit d2bdd273b2
2 changed files with 158 additions and 58 deletions

View File

@@ -4,6 +4,8 @@ import com.aallam.openai.api.chat.Tool
import com.aallam.openai.api.core.Parameters
import kotlinx.serialization.json.*
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.QuoteReply
import top.jie65535.mirai.JChatGPT
class SendSingleMessageAgent : BaseAgent(
@@ -17,6 +19,10 @@ class SendSingleMessageAgent : BaseAgent(
put("type", "string")
put("description", "消息内容")
}
putJsonObject("replyTo") {
put("type", "integer")
put("description", "可选。要引用回复的历史消息编号(即历史记录中每行行首的[n])。不需要回复具体某条消息时省略此参数。")
}
}
putJsonArray("required") {
add("content")
@@ -27,7 +33,28 @@ class SendSingleMessageAgent : BaseAgent(
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
requireNotNull(args)
val content = args.getValue("content").jsonPrimitive.content
event.subject.sendMessage(JChatGPT.toMessage(event.subject, content))
return "OK"
val replyTo = args["replyTo"]?.jsonPrimitive?.intOrNull
val baseMsg = JChatGPT.toMessage(event.subject, content)
var note = ""
val message: Message = if (replyTo != null) {
val record = JChatGPT.lookupReplyTarget(event.subject.id, replyTo)
val source = try {
record?.toMessageSource()
} catch (e: Throwable) {
null
}
if (source != null) {
QuoteReply(source) + baseMsg
} else {
note = "(编号${replyTo}对应的消息已失效,未能引用,已直接发送)"
baseMsg
}
} else {
baseMsg
}
event.subject.sendMessage(message)
return "OK$note"
}
}
}