mirror of
https://github.com/jie65535/JChatGPT.git
synced 2025-06-02 17:39:10 +08:00
Compare commits
12 Commits
49b1b0c345
...
e387cdc1f0
Author | SHA1 | Date | |
---|---|---|---|
e387cdc1f0 | |||
be465a7bd7 | |||
d4f9650bc7 | |||
e1af3d8233 | |||
72d0914a7a | |||
c74457e739 | |||
917c1727ab | |||
ef25b06aab | |||
12df62d7fe | |||
98bcd3785b | |||
f91dbf8a6d | |||
5cef899993 |
@ -7,7 +7,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = "top.jie65535.mirai"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -17,9 +17,11 @@ repositories {
|
||||
val openaiClientVersion = "3.8.2"
|
||||
val ktorVersion = "2.3.12"
|
||||
val jLatexMathVersion = "1.0.7"
|
||||
val commonTextVersion = "1.13.0"
|
||||
|
||||
dependencies {
|
||||
implementation("com.aallam.openai:openai-client:$openaiClientVersion")
|
||||
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
|
||||
implementation("org.scilab.forge:jlatexmath:$jLatexMathVersion")
|
||||
implementation("org.apache.commons:commons-text:$commonTextVersion")
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
package top.jie65535.mirai
|
||||
|
||||
import com.aallam.openai.api.chat.ChatCompletionRequest
|
||||
import com.aallam.openai.api.chat.ChatMessage
|
||||
import com.aallam.openai.api.chat.ChatRole
|
||||
import com.aallam.openai.api.chat.*
|
||||
import com.aallam.openai.api.core.Role
|
||||
import com.aallam.openai.api.http.Timeout
|
||||
import com.aallam.openai.api.model.ModelId
|
||||
import com.aallam.openai.client.OpenAI
|
||||
import com.aallam.openai.client.OpenAIHost
|
||||
import io.ktor.util.collections.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
|
||||
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
|
||||
import net.mamoe.mirai.console.permission.PermissionId
|
||||
@ -17,7 +17,9 @@ import net.mamoe.mirai.console.permission.PermissionService.Companion.hasPermiss
|
||||
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.MemberPermission.*
|
||||
import net.mamoe.mirai.contact.isOperator
|
||||
import net.mamoe.mirai.contact.nameCardOrNick
|
||||
import net.mamoe.mirai.event.GlobalEventChannel
|
||||
import net.mamoe.mirai.event.events.FriendMessageEvent
|
||||
import net.mamoe.mirai.event.events.GroupMessageEvent
|
||||
@ -26,15 +28,21 @@ 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.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.info
|
||||
import top.jie65535.mirai.tools.*
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.TextStyle
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object JChatGPT : KotlinPlugin(
|
||||
JvmPluginDescription(
|
||||
id = "top.jie65535.mirai.JChatGPT",
|
||||
name = "J ChatGPT",
|
||||
version = "1.2.0",
|
||||
version = "1.3.0",
|
||||
) {
|
||||
author("jie65535")
|
||||
}
|
||||
@ -72,22 +80,27 @@ object JChatGPT : KotlinPlugin(
|
||||
}
|
||||
|
||||
// private val userContext = ConcurrentMap<Long, MutableList<ChatMessage>>()
|
||||
private const val REPLAY_QUEUE_MAX = 30
|
||||
private const val REPLAY_QUEUE_MAX = 10
|
||||
private val replyMap = ConcurrentMap<Int, MutableList<ChatMessage>>()
|
||||
private val replyQueue = mutableListOf<Int>()
|
||||
private val requestMap = ConcurrentSet<Long>()
|
||||
|
||||
private suspend fun MessageEvent.onMessage(event: MessageEvent) {
|
||||
private suspend fun onMessage(event: MessageEvent) {
|
||||
// 检查Token是否设置
|
||||
if (openAi == null) return
|
||||
// 发送者是否有权限
|
||||
if (!toCommandSender().hasPermission(chatPermission)) {
|
||||
if (this is GroupMessageEvent) {
|
||||
if (!sender.isOperator() || !PluginConfig.groupOpHasChatPermission) {
|
||||
if (!event.toCommandSender().hasPermission(chatPermission)) {
|
||||
if (event is GroupMessageEvent) {
|
||||
if (PluginConfig.groupOpHasChatPermission && event.sender.isOperator()) {
|
||||
// 允许管理员使用
|
||||
} else if (event.sender.active.temperature >= PluginConfig.temperaturePermission) {
|
||||
// 允许活跃度达标成员使用
|
||||
} else {
|
||||
// 其它情况阻止使用
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this is FriendMessageEvent) {
|
||||
if (event is FriendMessageEvent) {
|
||||
if (!PluginConfig.friendHasChatPermission) {
|
||||
return
|
||||
}
|
||||
@ -96,9 +109,9 @@ object JChatGPT : KotlinPlugin(
|
||||
}
|
||||
|
||||
// 是否@bot
|
||||
val isAtBot = message.contains(At(bot))
|
||||
val isAtBot = event.message.contains(At(event.bot))
|
||||
// 是否包含引用消息
|
||||
val quote = message[QuoteReply]
|
||||
val quote = event.message[QuoteReply]
|
||||
// 如果没有@bot或者引用消息则直接结束
|
||||
if (!isAtBot && quote == null)
|
||||
return
|
||||
@ -122,59 +135,151 @@ object JChatGPT : KotlinPlugin(
|
||||
}
|
||||
}
|
||||
|
||||
startChat(context)
|
||||
startChat(event, context)
|
||||
}
|
||||
|
||||
private suspend fun MessageEvent.startChat(context: List<ChatMessage>? = null) {
|
||||
private fun getSystemPrompt(event: MessageEvent): String {
|
||||
val now = OffsetDateTime.now()
|
||||
val prompt = StringBuilder(PluginConfig.prompt)
|
||||
fun replace(target: String, replacement: () -> String) {
|
||||
val i = prompt.indexOf(target)
|
||||
if (i != -1) {
|
||||
prompt.replace(i, i + target.length, replacement())
|
||||
}
|
||||
}
|
||||
replace("{time}") {
|
||||
"$now ${now.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.CHINA)}"
|
||||
}
|
||||
replace("{subject}") {
|
||||
if (event is GroupMessageEvent) {
|
||||
"\"${event.subject.name}\" 群聊中"
|
||||
} else {
|
||||
"私聊中"
|
||||
}
|
||||
}
|
||||
replace("{sender}") {
|
||||
if (event is GroupMessageEvent) {
|
||||
event.sender.specialTitle
|
||||
val permissionName = when (event.sender.permission) {
|
||||
MEMBER -> "普通群员"
|
||||
ADMINISTRATOR -> "管理员"
|
||||
OWNER -> "群主"
|
||||
}
|
||||
"\"${event.senderName}\" 身份:$permissionName"
|
||||
} else {
|
||||
"\"${event.senderName}\""
|
||||
}
|
||||
}
|
||||
return prompt.toString()
|
||||
}
|
||||
|
||||
@OptIn(MiraiExperimentalApi::class)
|
||||
private suspend fun startChat(event: MessageEvent, context: List<ChatMessage>? = null) {
|
||||
val history = mutableListOf<ChatMessage>()
|
||||
if (!context.isNullOrEmpty()) {
|
||||
history.addAll(context)
|
||||
} else if (PluginConfig.prompt.isNotEmpty()) {
|
||||
history.add(ChatMessage(ChatRole.System, PluginConfig.prompt))
|
||||
history.add(ChatMessage(ChatRole.System, getSystemPrompt(event)))
|
||||
}
|
||||
val msg = message.plainText()
|
||||
val msg = event.message.plainText()
|
||||
if (msg.isNotEmpty()) {
|
||||
history.add(ChatMessage(ChatRole.User, msg))
|
||||
}
|
||||
|
||||
try {
|
||||
if (!requestMap.add(sender.id)) {
|
||||
subject.sendMessage(message.quote() + "再等等...")
|
||||
if (!requestMap.add(event.sender.id)) {
|
||||
event.subject.sendMessage(event.message.quote() + "再等等...")
|
||||
return
|
||||
}
|
||||
val reply = chatCompletion(history)
|
||||
history.add(reply)
|
||||
val content = reply.content ?: "..."
|
||||
|
||||
val replyMsg = subject.sendMessage(
|
||||
var done: Boolean
|
||||
var retry = 2
|
||||
var hasTools = true
|
||||
do {
|
||||
val reply = chatCompletion(history, hasTools)
|
||||
history.add(reply)
|
||||
done = true
|
||||
|
||||
for (toolCall in reply.toolCalls.orEmpty()) {
|
||||
require(toolCall is ToolCall.Function) { "Tool call is not a function" }
|
||||
val functionResponse = toolCall.execute(event)
|
||||
history.add(
|
||||
ChatMessage(
|
||||
role = ChatRole.Tool,
|
||||
toolCallId = toolCall.id,
|
||||
name = toolCall.function.name,
|
||||
content = functionResponse
|
||||
)
|
||||
)
|
||||
done = false
|
||||
hasTools = false
|
||||
}
|
||||
} while (!done && 0 < --retry)
|
||||
|
||||
val content = history.last().content ?: "..."
|
||||
val replyMsg = event.subject.sendMessage(
|
||||
if (content.length < 128) {
|
||||
message.quote() + toMessage(subject, content)
|
||||
event.message.quote() + toMessage(event.subject, content)
|
||||
} else {
|
||||
// 消息内容太长则转为转发消息避免刷屏
|
||||
buildForwardMessage {
|
||||
event.buildForwardMessage {
|
||||
for (item in history) {
|
||||
val temp = toMessage(subject, item.content ?: "...")
|
||||
if (item.content.isNullOrEmpty())
|
||||
continue
|
||||
val temp = toMessage(event.subject, item.content!!)
|
||||
when (item.role) {
|
||||
Role.User -> sender says temp
|
||||
Role.Assistant -> bot says temp
|
||||
Role.User -> event.sender says temp
|
||||
Role.Assistant -> event.bot says temp
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并移除超出转发消息上限的消息
|
||||
var isOverflow = false
|
||||
var count = 0
|
||||
for (i in size - 1 downTo 0) {
|
||||
if (count > 4900) {
|
||||
isOverflow = true
|
||||
// 删除早期上下文消息
|
||||
removeAt(i)
|
||||
} else {
|
||||
for (text in this[i].messageChain.filterIsInstance<PlainText>()) {
|
||||
count += text.content.length
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 5000) {
|
||||
removeAt(0)
|
||||
}
|
||||
if (isOverflow) {
|
||||
// 如果溢出了,插入一条提示到最开始
|
||||
add(
|
||||
0, ForwardMessage.Node(
|
||||
senderId = event.bot.id,
|
||||
time = this[0].time - 1,
|
||||
senderName = event.bot.nameCardOrNick,
|
||||
message = PlainText("更早的消息已隐藏,避免超出转发消息上限。")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 将回复的消息和对话历史保存到队列
|
||||
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))
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.warning(ex)
|
||||
subject.sendMessage(message.quote() + "发生异常,请重试")
|
||||
event.subject.sendMessage(event.message.quote() + "发生异常,请重试")
|
||||
} finally {
|
||||
requestMap.remove(sender.id)
|
||||
requestMap.remove(event.sender.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,10 +300,11 @@ object JChatGPT : KotlinPlugin(
|
||||
* @return 构造的消息
|
||||
*/
|
||||
private suspend fun toMessage(contact: Contact, content: String): Message {
|
||||
if (content.length < 3) {
|
||||
return PlainText(content)
|
||||
}
|
||||
return buildMessageChain {
|
||||
return if (content.isEmpty()) {
|
||||
PlainText("...")
|
||||
} else if (content.length < 3) {
|
||||
PlainText(content)
|
||||
} else buildMessageChain {
|
||||
// 匹配LaTeX表达式
|
||||
val matcher = laTeXPattern.matcher(content)
|
||||
var index = 0
|
||||
@ -230,14 +336,77 @@ object JChatGPT : KotlinPlugin(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun chatCompletion(messages: List<ChatMessage>): ChatMessage {
|
||||
/**
|
||||
* 工具列表
|
||||
*/
|
||||
private val myTools = listOf(
|
||||
// 网页搜索
|
||||
WebSearch(),
|
||||
|
||||
// 运行代码
|
||||
RunCode(),
|
||||
|
||||
// 天气服务
|
||||
WeatherService(),
|
||||
|
||||
// IP所在地查询
|
||||
IpAddressQuery(),
|
||||
|
||||
// 疯狂星期四
|
||||
CrazyKfc(),
|
||||
)
|
||||
|
||||
|
||||
private suspend fun chatCompletion(
|
||||
chatMessages: List<ChatMessage>,
|
||||
hasTools: Boolean = true
|
||||
): ChatMessage {
|
||||
val openAi = this.openAi ?: throw NullPointerException("OpenAI Token 未设置,无法开始")
|
||||
val request = ChatCompletionRequest(ModelId(PluginConfig.chatModel), messages)
|
||||
logger.info("OpenAI API Requesting... Model=${PluginConfig.chatModel}")
|
||||
val availableTools = if (hasTools) {
|
||||
myTools.filter { it.isEnabled }.map { it.tool }
|
||||
} else null
|
||||
val request = ChatCompletionRequest(
|
||||
model = ModelId(PluginConfig.chatModel),
|
||||
messages = chatMessages,
|
||||
tools = availableTools,
|
||||
toolChoice = ToolChoice.Auto
|
||||
)
|
||||
logger.info(
|
||||
"API Requesting..." +
|
||||
" Model=${PluginConfig.chatModel}" +
|
||||
" Tools=${availableTools?.joinToString(prefix = "[", postfix = "]")}"
|
||||
)
|
||||
val response = openAi.chatCompletion(request)
|
||||
logger.info("OpenAI API Usage: ${response.usage}")
|
||||
return response.choices.first().message
|
||||
val message = response.choices.first().message
|
||||
logger.info("Response: $message ${response.usage}")
|
||||
return message
|
||||
}
|
||||
|
||||
private fun MessageChain.plainText() = this.filterIsInstance<PlainText>().joinToString().trim()
|
||||
|
||||
private suspend fun ToolCall.Function.execute(event: MessageEvent): String {
|
||||
val agent = myTools.find { it.tool.function.name == function.name }
|
||||
?: return "Function ${function.name} not found"
|
||||
// 提示正在执行函数
|
||||
val receipt = if (agent.loadingMessage.isNotEmpty()) {
|
||||
event.subject.sendMessage(event.message.quote() + agent.loadingMessage)
|
||||
} else null
|
||||
// 提取参数
|
||||
val args = function.argumentsAsJsonOrNull()
|
||||
logger.info("Calling ${function.name}(${args})")
|
||||
// 执行函数
|
||||
val result = try {
|
||||
agent.execute(args)
|
||||
} catch (e: Throwable) {
|
||||
logger.error("Failed to call ${function.name}", e)
|
||||
"工具调用失败,请尝试自行回答用户,或如实告知。"
|
||||
}
|
||||
logger.info("Result=$result")
|
||||
// 过会撤回加载消息
|
||||
if (receipt != null) {
|
||||
launch { delay(3.seconds); receipt.recall() }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
@ -23,6 +23,16 @@ object PluginConfig : AutoSavePluginConfig("Config") {
|
||||
@ValueDescription("好友是否自动拥有对话权限,默认是")
|
||||
val friendHasChatPermission: Boolean by value(true)
|
||||
|
||||
@ValueDescription("群荣誉等级权限门槛,达到这个等级相当于自动拥有权限。")
|
||||
val temperaturePermission: Int by value(60)
|
||||
|
||||
@ValueDescription("等待响应超时时间,单位毫秒,默认60秒")
|
||||
val timeout: Long by value(60000L)
|
||||
|
||||
@ValueDescription("SearXNG 搜索引擎地址,如 http://127.0.0.1:8080/search 必须启用允许json格式返回")
|
||||
val searXngUrl: String by value("")
|
||||
|
||||
@ValueDescription("在线运行代码 glot.io 的 api token,在官网注册账号即可获取。")
|
||||
val glotToken: String by value("")
|
||||
|
||||
}
|
30
src/main/kotlin/tools/BaseAgent.kt
Normal file
30
src/main/kotlin/tools/BaseAgent.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import com.aallam.openai.api.chat.Tool
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
abstract class BaseAgent(
|
||||
val tool: Tool
|
||||
) {
|
||||
/**
|
||||
* 是否启用该工具
|
||||
*/
|
||||
open val isEnabled: Boolean = true
|
||||
|
||||
/**
|
||||
* 加载时消息 可用于提示用户正在执行
|
||||
*/
|
||||
open val loadingMessage: String = ""
|
||||
|
||||
protected val httpClient by lazy {
|
||||
HttpClient(OkHttp)
|
||||
}
|
||||
|
||||
abstract suspend fun execute(args: JsonObject?): String
|
||||
|
||||
override fun toString(): String {
|
||||
return "${tool.function.name}: ${tool.function.description}"
|
||||
}
|
||||
}
|
28
src/main/kotlin/tools/CrazyKfc.kt
Normal file
28
src/main/kotlin/tools/CrazyKfc.kt
Normal file
@ -0,0 +1,28 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import com.aallam.openai.api.chat.Tool
|
||||
import com.aallam.openai.api.core.Parameters
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.serialization.json.*
|
||||
import java.time.DayOfWeek
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class CrazyKfc : BaseAgent(
|
||||
tool = Tool.function(
|
||||
name = "crazyThursday",
|
||||
description = "获取一条KFC疯狂星期四文案",
|
||||
parameters = Parameters.Empty
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* 仅周四可用
|
||||
*/
|
||||
override val isEnabled: Boolean
|
||||
get() = OffsetDateTime.now().dayOfWeek == DayOfWeek.THURSDAY
|
||||
|
||||
override suspend fun execute(args: JsonObject?): String {
|
||||
val response = httpClient.get("https://api.52vmy.cn/api/wl/yan/kfc")
|
||||
return response.bodyAsText()
|
||||
}
|
||||
}
|
33
src/main/kotlin/tools/IpAddressQuery.kt
Normal file
33
src/main/kotlin/tools/IpAddressQuery.kt
Normal file
@ -0,0 +1,33 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import com.aallam.openai.api.chat.Tool
|
||||
import com.aallam.openai.api.core.Parameters
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
class IpAddressQuery : BaseAgent(
|
||||
tool = Tool.function(
|
||||
name = "ipAddressQuery",
|
||||
description = "可查询IP地址归属地",
|
||||
parameters = Parameters.buildJsonObject {
|
||||
put("type", "object")
|
||||
putJsonObject("properties") {
|
||||
putJsonObject("ip") {
|
||||
put("type", "string")
|
||||
put("description", "IPv4地址")
|
||||
}
|
||||
}
|
||||
putJsonArray("required") {
|
||||
add("ip")
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
override suspend fun execute(args: JsonObject?): String {
|
||||
requireNotNull(args)
|
||||
val ip = args.getValue("ip").jsonPrimitive.content
|
||||
val response = httpClient.get("https://api.52vmy.cn/api/query/itad?ip=$ip")
|
||||
return response.bodyAsText()
|
||||
}
|
||||
}
|
107
src/main/kotlin/tools/RunCode.kt
Normal file
107
src/main/kotlin/tools/RunCode.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import com.aallam.openai.api.chat.Tool
|
||||
import com.aallam.openai.api.core.Parameters
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.json.*
|
||||
import top.jie65535.mirai.PluginConfig
|
||||
|
||||
class RunCode : BaseAgent(
|
||||
tool = Tool.function(
|
||||
name = "runCode",
|
||||
description = "执行代码,请尽量避免需要运行时输入或可能导致死循环的代码!",
|
||||
parameters = Parameters.buildJsonObject {
|
||||
put("type", "object")
|
||||
putJsonObject("properties") {
|
||||
putJsonObject("language") {
|
||||
put("type", "string")
|
||||
putJsonArray("enum") {
|
||||
GLOT_LANGUAGES.forEach(::add)
|
||||
}
|
||||
}
|
||||
putJsonObject("files") {
|
||||
put("type", "array")
|
||||
putJsonObject("items") {
|
||||
put("type", "object")
|
||||
putJsonObject("file") {
|
||||
putJsonObject("name") {
|
||||
put("type", "string")
|
||||
put("description", "文件名,如 main.py")
|
||||
}
|
||||
putJsonObject("content") {
|
||||
put("type", "string")
|
||||
put("description", "文件内容,如 print(42)")
|
||||
}
|
||||
}
|
||||
}
|
||||
put("description", "代码文件")
|
||||
}
|
||||
putJsonObject("stdin") {
|
||||
put("type", "string")
|
||||
put("description", "可选的标准输入内容")
|
||||
}
|
||||
}
|
||||
putJsonArray("required") {
|
||||
add("language")
|
||||
add("files")
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Glot API 地址
|
||||
*/
|
||||
const val GLOT_RUN_API_URL = "https://glot.io/api/run/"
|
||||
|
||||
/**
|
||||
* 使用的语言版本 仅有最新
|
||||
*/
|
||||
const val GLOT_LANGUAGE_VERSION = "/latest"
|
||||
|
||||
/**
|
||||
* Glot支持的编程语言(经过过滤和排序,实际支持40+,没必要)
|
||||
*/
|
||||
val GLOT_LANGUAGES = listOf(
|
||||
"bash",
|
||||
"python",
|
||||
"c",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"kotlin",
|
||||
"java",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"go",
|
||||
"rust",
|
||||
"lua",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置了Token以后才启用
|
||||
*/
|
||||
override val isEnabled: Boolean
|
||||
get() = PluginConfig.glotToken.isNotEmpty()
|
||||
|
||||
override val loadingMessage: String
|
||||
get() = "执行代码中..."
|
||||
|
||||
override suspend fun execute(args: JsonObject?): String {
|
||||
requireNotNull(args)
|
||||
val language = args.getValue("language").jsonPrimitive.content
|
||||
val filteredArgs = args.toMutableMap().let {
|
||||
it.remove("language")
|
||||
JsonObject(it)
|
||||
}
|
||||
val url = GLOT_RUN_API_URL + language + GLOT_LANGUAGE_VERSION
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType("application", "json"))
|
||||
header("Authorization", PluginConfig.glotToken)
|
||||
setBody(filteredArgs.toString())
|
||||
}
|
||||
return response.bodyAsText()
|
||||
}
|
||||
}
|
56
src/main/kotlin/tools/WeatherService.kt
Normal file
56
src/main/kotlin/tools/WeatherService.kt
Normal file
@ -0,0 +1,56 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import com.aallam.openai.api.chat.Tool
|
||||
import com.aallam.openai.api.core.Parameters
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
class WeatherService : BaseAgent(
|
||||
tool = Tool.function(
|
||||
name = "queryWeather",
|
||||
description = "可用于查询某城市地区天气.",
|
||||
parameters = Parameters.buildJsonObject {
|
||||
put("type", "object")
|
||||
putJsonObject("properties") {
|
||||
putJsonObject("city") {
|
||||
put("type", "string")
|
||||
put("description", "城市地区,如\"深圳市\"")
|
||||
}
|
||||
putJsonObject("time_range") {
|
||||
put("type", "string")
|
||||
putJsonArray("enum") {
|
||||
add("day")
|
||||
add("three")
|
||||
add("many")
|
||||
}
|
||||
put("description", "时间范围,仅当天天气可获得最详细信息,三天和更多只能获得简单信息。")
|
||||
}
|
||||
}
|
||||
putJsonArray("required") {
|
||||
add("city")
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
override val loadingMessage: String
|
||||
get() = "查询天气中..."
|
||||
|
||||
override suspend fun execute(args: JsonObject?): String {
|
||||
requireNotNull(args)
|
||||
val city = args.getValue("city").jsonPrimitive.content
|
||||
val timeRange = args["time_range"]?.jsonPrimitive?.contentOrNull
|
||||
val response = httpClient.get(
|
||||
buildString {
|
||||
append(when (timeRange) {
|
||||
"many" -> "https://api.52vmy.cn/api/query/tian/many"
|
||||
"three" -> "https://api.52vmy.cn/api/query/tian/three"
|
||||
else -> "https://api.52vmy.cn/api/query/tian"
|
||||
})
|
||||
append("?city=")
|
||||
append(city)
|
||||
}
|
||||
)
|
||||
return response.bodyAsText()
|
||||
}
|
||||
}
|
126
src/main/kotlin/tools/WebSearch.kt
Normal file
126
src/main/kotlin/tools/WebSearch.kt
Normal file
@ -0,0 +1,126 @@
|
||||
package top.jie65535.mirai.tools
|
||||
|
||||
import com.aallam.openai.api.chat.Tool
|
||||
import com.aallam.openai.api.core.Parameters
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.json.*
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
import top.jie65535.mirai.PluginConfig
|
||||
|
||||
class WebSearch : BaseAgent(
|
||||
tool = Tool.function(
|
||||
name = "search",
|
||||
description = "Provides meta-search functionality through SearXNG," +
|
||||
" aggregating results from multiple search engines.",
|
||||
parameters = Parameters.buildJsonObject {
|
||||
put("type", "object")
|
||||
putJsonObject("properties") {
|
||||
putJsonObject("q") {
|
||||
put("type", "string")
|
||||
put("description", "查询内容关键字")
|
||||
}
|
||||
putJsonObject("categories") {
|
||||
put("type", "array")
|
||||
putJsonObject("items") {
|
||||
put("type", "string")
|
||||
putJsonArray("enum") {
|
||||
add("general")
|
||||
add("images")
|
||||
add("videos")
|
||||
add("news")
|
||||
add("music")
|
||||
add("it")
|
||||
add("science")
|
||||
add("files")
|
||||
add("social_media")
|
||||
}
|
||||
}
|
||||
put("description", "可选择多项查询分类,通常情况下不传或用general即可。")
|
||||
}
|
||||
putJsonObject("time_range") {
|
||||
put("type", "string")
|
||||
putJsonArray("enum") {
|
||||
add("day")
|
||||
add("month")
|
||||
add("year")
|
||||
}
|
||||
put("description", "可选择获取最新消息,例如day表示只查询最近一天相关信息,以此类推。")
|
||||
}
|
||||
}
|
||||
putJsonArray("required") {
|
||||
add("q")
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* 插件配置了 SearXNG URL 时才允许启用
|
||||
*/
|
||||
override val isEnabled: Boolean
|
||||
get() = PluginConfig.searXngUrl.isNotEmpty()
|
||||
|
||||
override val loadingMessage: String
|
||||
get() = "联网搜索中..."
|
||||
|
||||
override suspend fun execute(args: JsonObject?): String {
|
||||
requireNotNull(args)
|
||||
val q = args.getValue("q").jsonPrimitive.content
|
||||
val categories = args["categories"]?.jsonArray
|
||||
val timeRange = args["time_range"]?.jsonPrimitive?.contentOrNull
|
||||
val response = httpClient.get(
|
||||
buildString {
|
||||
append(PluginConfig.searXngUrl)
|
||||
append("?q=")
|
||||
append(q.encodeURLParameter())
|
||||
append("&format=json")
|
||||
if (categories != null) {
|
||||
append("&")
|
||||
append(categories.joinToString { it.jsonPrimitive.content })
|
||||
}
|
||||
if (timeRange != null) {
|
||||
append("&")
|
||||
append(timeRange)
|
||||
}
|
||||
}
|
||||
)
|
||||
val body = response.bodyAsText()
|
||||
val responseJsonElement = Json.parseToJsonElement(body)
|
||||
val filteredResponse = buildJsonObject {
|
||||
val root = responseJsonElement.jsonObject
|
||||
// 查询内容原样转发
|
||||
root["query"]?.let { put("query", it) }
|
||||
|
||||
// 过滤搜索结果
|
||||
val results = root["results"]?.jsonArray
|
||||
if (results != null) {
|
||||
val filteredResults = results
|
||||
.filter {
|
||||
// 去掉所有内容为空的结果
|
||||
!it.jsonObject.getValue("content").jsonPrimitive.contentOrNull.isNullOrEmpty()
|
||||
}.sortedByDescending {
|
||||
it.jsonObject.getValue("score").jsonPrimitive.double
|
||||
}.take(5) // 只取得分最高的前5条结果
|
||||
.map {
|
||||
// 移除掉我不想要的字段
|
||||
val item = it.jsonObject.toMutableMap()
|
||||
item.remove("engine")
|
||||
item.remove("parsed_url")
|
||||
item.remove("template")
|
||||
item.remove("engines")
|
||||
item.remove("positions")
|
||||
item.remove("metadata")
|
||||
item.remove("thumbnail")
|
||||
JsonObject(item)
|
||||
}
|
||||
put("results", JsonArray(filteredResults))
|
||||
}
|
||||
|
||||
// 答案和信息盒子原样转发
|
||||
root["answers"]?.let { put("answers", it) }
|
||||
root["infoboxes"]?.let { put("infoboxes", it) }
|
||||
}.toString()
|
||||
return StringEscapeUtils.unescapeJava(filteredResponse)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user