diff --git a/build.gradle.kts b/build.gradle.kts index 87e814f..691ba4e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - val kotlinVersion = "1.9.24" + val kotlinVersion = "1.8.10" kotlin("jvm") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion @@ -7,7 +7,7 @@ plugins { } group = "top.jie65535.mirai" -version = "1.1.0" +version = "1.2.0" repositories { mavenCentral() @@ -16,8 +16,10 @@ repositories { val openaiClientVersion = "3.8.2" val ktorVersion = "2.3.12" +val jLatexMathVersion = "1.0.7" dependencies { implementation("com.aallam.openai:openai-client:$openaiClientVersion") implementation("io.ktor:ktor-client-okhttp:$ktorVersion") + implementation("org.scilab.forge:jlatexmath:$jLatexMathVersion") } \ No newline at end of file diff --git a/src/main/kotlin/JChatGPT.kt b/src/main/kotlin/JChatGPT.kt index cf5c596..5d0ff04 100644 --- a/src/main/kotlin/JChatGPT.kt +++ b/src/main/kotlin/JChatGPT.kt @@ -16,6 +16,7 @@ import net.mamoe.mirai.console.permission.PermissionService import net.mamoe.mirai.console.permission.PermissionService.Companion.hasPermission import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.isOperator import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.events.FriendMessageEvent @@ -24,14 +25,16 @@ import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.MessageSource.Key.quote import net.mamoe.mirai.message.sourceIds +import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.info +import java.util.regex.Pattern import kotlin.time.Duration.Companion.milliseconds object JChatGPT : KotlinPlugin( JvmPluginDescription( id = "top.jie65535.mirai.JChatGPT", name = "J ChatGPT", - version = "1.1.0", + version = "1.2.0", ) { author("jie65535") } @@ -61,13 +64,14 @@ object JChatGPT : KotlinPlugin( fun updateOpenAiToken(token: String) { val timeout = PluginConfig.timeout.milliseconds - openAi = OpenAI(token, + openAi = OpenAI( + token, host = OpenAIHost(baseUrl = PluginConfig.openAiApi), timeout = Timeout(request = timeout, connect = timeout, socket = timeout) ) } -// private val userContext = ConcurrentMap>() + // private val userContext = ConcurrentMap>() private const val REPLAY_QUEUE_MAX = 30 private val replyMap = ConcurrentMap>() private val replyQueue = mutableListOf() @@ -143,23 +147,26 @@ object JChatGPT : KotlinPlugin( val content = reply.content ?: "..." val replyMsg = subject.sendMessage( - if (content.length < 100) { - message.quote() + content + if (content.length < 128) { + message.quote() + toMessage(subject, content) } else { // 消息内容太长则转为转发消息避免刷屏 buildForwardMessage { for (item in history) { + val temp = toMessage(subject, item.content ?: "...") when (item.role) { - Role.User -> sender says (item.content ?: "...") - Role.Assistant -> bot says (item.content ?: "...") + Role.User -> sender says temp + Role.Assistant -> bot says temp } } } } ) - val msgId = replyMsg.sourceIds[0] - replyMap[msgId] = history - replyQueue.add(msgId) + if (replyMsg.sourceIds.isNotEmpty()) { + val msgId = replyMsg.sourceIds[0] + replyMap[msgId] = history + replyQueue.add(msgId) + } if (replyQueue.size > REPLAY_QUEUE_MAX) { replyMap.remove(replyQueue.removeAt(0)) } @@ -171,6 +178,58 @@ object JChatGPT : KotlinPlugin( } } + private val laTeXPattern = Pattern.compile( + "\\\\\\((.+?)\\\\\\)|" + // 匹配行内公式 \(...\) + "\\\\\\[(.+?)\\\\\\]|" + // 匹配独立公式 \[...\] + "\\$\\$([^$]+?)\\$\\$|" + // 匹配独立公式 $$...$$ + "\\$(.+?)\\$|" + // 匹配行内公式 $...$ + "```latex\\s*([^`]+?)\\s*```" // 匹配 ```latex ... ``` + , Pattern.DOTALL + ) + + /** + * 将聊天内容转为聊天消息,如果聊天中包含LaTeX表达式,将会转为图片拼接到消息中。 + * + * @param contact 联系对象 + * @param content 文本内容 + * @return 构造的消息 + */ + private suspend fun toMessage(contact: Contact, content: String): Message { + if (content.length < 3) { + return PlainText(content) + } + return buildMessageChain { + // 匹配LaTeX表达式 + val matcher = laTeXPattern.matcher(content) + var index = 0 + while (matcher.find()) { + for (i in 1..matcher.groupCount()) { + if (matcher.group(i) == null) { + continue + } + try { + // 将所有匹配的LaTeX公式转为图片拼接到消息中 + val formula = matcher.group(i) + val imageByteArray = LaTeXConverter.convertToImage(formula, "png") + val resource = imageByteArray.toExternalResource("png") + val image = contact.uploadImage(resource) + + // 拼接公式前的文本 + append(content, index, matcher.start()) + // 插入图片 + append(image) + // 移动索引 + index = matcher.end() + } catch (ex: Throwable) { + logger.warning("处理LaTeX表达式时异常", ex) + } + } + } + // 拼接后续消息 + append(content, index, content.length) + } + } + private suspend fun chatCompletion(messages: List): ChatMessage { val openAi = this.openAi ?: throw NullPointerException("OpenAI Token 未设置,无法开始") val request = ChatCompletionRequest(ModelId(PluginConfig.chatModel), messages) diff --git a/src/main/kotlin/LaTeXConverter.kt b/src/main/kotlin/LaTeXConverter.kt new file mode 100644 index 0000000..6551d0d --- /dev/null +++ b/src/main/kotlin/LaTeXConverter.kt @@ -0,0 +1,32 @@ +package top.jie65535.mirai + +import org.scilab.forge.jlatexmath.TeXConstants +import org.scilab.forge.jlatexmath.TeXFormula +import java.awt.Color +import java.awt.Insets +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import javax.imageio.ImageIO +import javax.swing.JLabel + + +object LaTeXConverter { + /** + * 转换LaTeX到图片字节数组 + */ + fun convertToImage(latexString: String, format: String = "png"): ByteArray { + val formula = TeXFormula(latexString) + val icon = formula.TeXIconBuilder().setStyle(TeXConstants.STYLE_DISPLAY).setSize(20f).build() + icon.insets = Insets(5, 5, 5, 5) + val image = BufferedImage(icon.iconWidth, icon.iconHeight, BufferedImage.TYPE_INT_ARGB) + val g2 = image.createGraphics() + g2.color = Color.white + g2.fillRect(0, 0, icon.iconWidth, icon.iconHeight) + val jl = JLabel() + jl.setForeground(Color(0, 0, 0)) + icon.paintIcon(jl, g2, 0, 0) + val stream = ByteArrayOutputStream() + ImageIO.write(image, format, stream) + return stream.toByteArray() + } +} \ No newline at end of file