mirror of
https://github.com/jie65535/JChatGPT.git
synced 2025-10-20 17:13:37 +08:00
Compare commits
6 Commits
e79bcd9983
...
master
Author | SHA1 | Date | |
---|---|---|---|
099625c2f2 | |||
ffa7f78c25 | |||
2c40ef1b16 | |||
e083b7aff9 | |||
e2caba6a10 | |||
af17f1e698 |
252
README.md
252
README.md
@@ -1,43 +1,265 @@
|
|||||||
# JChatGPT
|
# JChatGPT
|
||||||
|
|
||||||
|
JChatGPT 是一个基于 Kotlin 的 Mirai Console 插件,它将大型语言模型(LLM)集成到即时通讯平台中。该插件支持多种 AI 模型和丰富的工具功能,使用户能够在群聊和私聊中与 AI 进行交互。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **多模型支持**:支持聊天模型、推理模型和视觉模型
|
||||||
|
- **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等
|
||||||
|
- **上下文记忆**:支持持久化记忆存储
|
||||||
|
- **LaTeX 渲染**:自动将数学表达式渲染为图片
|
||||||
|
- **灵活的触发方式**:@机器人、关键字触发、回复消息等
|
||||||
|
- **权限控制**:细粒度的权限管理系统
|
||||||
|
- **历史消息集成**:可选的历史消息上下文(需配合 mirai-hibernate-plugin)
|
||||||
|
|
||||||
## 用法
|
## 用法
|
||||||
|
|
||||||
在群内直接@bot即可触发对话
|
### 基本交互
|
||||||
|
- 在群内直接 @bot 即可触发对话
|
||||||
|
- 通过引用群友消息 + @bot 让 Bot 识别引用消息的内容
|
||||||
|
- 回复 bot 的消息即可引用对应的上下文对话(包括这个回复的历史对话)
|
||||||
|
- 使用关键字触发(默认为 "[小筱][林淋月玥]",可在配置中修改)
|
||||||
|
|
||||||
你也可以通过引用群友消息+@bot来让Bot识别引用消息的内容
|
### 工具调用
|
||||||
|
AI 可以自动调用多种工具来完成复杂任务:
|
||||||
回复bot的消息即可引用对应的上下文对话(包括这个回复的历史对话)
|
- 网络搜索(需要配置 SearXNG)
|
||||||
|
- 代码执行(支持多种语言,需要配置 glot.io token)
|
||||||
|
- 图像识别(需要配置视觉模型)
|
||||||
|
- 推理思考(需要配置推理模型)
|
||||||
|
- 群管理(禁言等,需启用相应权限)
|
||||||
|
- 记忆管理(添加和修改对话记忆)
|
||||||
|
|
||||||
## 权限列表
|
## 权限列表
|
||||||
- `JChatGPT:Chat` 拥有该权限即可使用bot与ChatGPT对话
|
|
||||||
- `top.jie65535.mirai.jchatgpt:command.jgpt` 拥有该权限即可使用`/jgpt`相关命令
|
- `JChatGPT:Chat` - 拥有该权限即可使用 bot 与 AI 对话
|
||||||
|
- `top.jie65535.mirai.jchatgpt:command.jgpt` - 拥有该权限即可使用 `/jgpt` 相关命令
|
||||||
|
|
||||||
## 命令列表
|
## 命令列表
|
||||||
- `/jgpt setToken <token>` - 设置OpenAI API Token
|
|
||||||
|
- `/jgpt setToken <token>` - 设置 OpenAI API Token
|
||||||
- `/jgpt enable <contact>` - 启用目标对话权限
|
- `/jgpt enable <contact>` - 启用目标对话权限
|
||||||
- `/jgpt disable <contact>` - 禁用目标对话权限
|
- `/jgpt disable <contact>` - 禁用目标对话权限
|
||||||
- `/jgpt reload` - 重载配置文件
|
- `/jgpt reload` - 重载配置文件
|
||||||
|
|
||||||
## 配置文件
|
## 配置文件
|
||||||
|
|
||||||
`./config/top.jie65535.mirai.JChatGPT/Config.yml`
|
配置文件位于:`./config/top.jie65535.mirai.JChatGPT/Config.yml`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# OpenAI API base url
|
# OpenAI API base url
|
||||||
openAiApi: 'https://api.openai.com/v1/'
|
openAiApi: 'https://dashscope.aliyuncs.com/compatible-mode/v1/'
|
||||||
# OpenAI API Token
|
# OpenAI API Token
|
||||||
openAiToken: ''
|
openAiToken: ''
|
||||||
# Chat模型
|
# Chat模型
|
||||||
chatModel: 'gpt-3.5-turbo-1106'
|
chatModel: 'qwen-max'
|
||||||
# Chat默认提示
|
# Chat模型温度,默认为null
|
||||||
prompt: ''
|
chatTemperature: null
|
||||||
|
# 推理模型API
|
||||||
|
reasoningModelApi: 'https://dashscope.aliyuncs.com/compatible-mode/v1/'
|
||||||
|
# 推理模型Token
|
||||||
|
reasoningModelToken: ''
|
||||||
|
# 推理模型
|
||||||
|
reasoningModel: 'qwq-plus'
|
||||||
|
# 视觉模型API
|
||||||
|
visualModelApi: 'https://dashscope.aliyuncs.com/compatible-mode/v1/'
|
||||||
|
# 视觉模型Token
|
||||||
|
visualModelToken: ''
|
||||||
|
# 视觉模型
|
||||||
|
visualModel: 'qwen-vl-plus'
|
||||||
|
# 百炼平台API KEY
|
||||||
|
dashScopeApiKey: ''
|
||||||
|
# 百炼平台图片编辑模型
|
||||||
|
imageEditModel: 'qwen-image-edit'
|
||||||
|
# 百炼平台TTS模型
|
||||||
|
ttsModel: 'qwen-tts'
|
||||||
|
# Jina API Key
|
||||||
|
jinaApiKey: ''
|
||||||
|
# SearXNG 搜索引擎地址,如 http://127.0.0.1:8080/search 必须启用允许json格式返回
|
||||||
|
searXngUrl: ''
|
||||||
|
# 在线运行代码 glot.io 的 api token,在官网注册账号即可获取。
|
||||||
|
glotToken: ''
|
||||||
# 群管理是否自动拥有对话权限,默认是
|
# 群管理是否自动拥有对话权限,默认是
|
||||||
groupOpHasChatPermission: true
|
groupOpHasChatPermission: true
|
||||||
# 好友是否自动拥有对话权限,默认是
|
# 好友是否自动拥有对话权限,默认是
|
||||||
friendHasChatPermission: true
|
friendHasChatPermission: true
|
||||||
|
# 机器人是否可以禁言别人,默认禁止
|
||||||
|
canMute: false
|
||||||
|
# 群荣誉等级权限门槛,达到这个等级相当于自动拥有对话权限。
|
||||||
|
temperaturePermission: 50
|
||||||
|
# 等待响应超时时间,单位毫秒,默认60秒
|
||||||
|
timeout: 60000
|
||||||
|
# 系统提示词,该字段已弃用,使用提示词文件而不是在这里修改
|
||||||
|
prompt: '你是一个乐于助人的助手'
|
||||||
|
# 系统提示词文件路径,相对于插件配置目录
|
||||||
|
promptFile: 'SystemPrompt.md'
|
||||||
|
# 创建Prompt时取最近多少分钟内的消息
|
||||||
|
historyWindowMin: 10
|
||||||
|
# 创建Prompt时取最多几条消息
|
||||||
|
historyMessageLimit: 20
|
||||||
|
# 是否打印Prompt便于调试
|
||||||
|
logPrompt: false
|
||||||
|
# 达到需要合并转发消息的阈值
|
||||||
|
messageMergeThreshold: 150
|
||||||
|
# 最大循环次数,至少2次
|
||||||
|
retryMax: 5
|
||||||
|
# 关键字呼叫,支持正则表达式
|
||||||
|
callKeyword: '[小筱][林淋月玥]'
|
||||||
|
# 是否显示工具调用消息,默认是
|
||||||
|
showToolCallingMessage: true
|
||||||
|
# 是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆
|
||||||
|
memoryEnabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 系统提示词
|
||||||
|
|
||||||
|
JChatGPT 使用系统提示词来定义 AI 的行为和个性。提示词文件位于插件配置目录下的 `SystemPrompt.md` 文件中。
|
||||||
|
|
||||||
|
### 提示词结构
|
||||||
|
|
||||||
|
系统提示词通常包含以下部分:
|
||||||
|
|
||||||
|
1. **角色定义**:定义 AI 的身份、性格和行为准则
|
||||||
|
2. **功能说明**:描述 AI 可以使用的工具和功能
|
||||||
|
3. **交互规则**:规定 AI 与用户交互的规则和限制
|
||||||
|
4. **占位符**:动态替换的内容,如时间、群信息、记忆等
|
||||||
|
|
||||||
|
### 占位符
|
||||||
|
|
||||||
|
系统提示词支持以下占位符,在运行时会被动态替换:
|
||||||
|
|
||||||
|
- `{time}` - 当前时间(格式:yyyy年MM月dd E HH:mm:ss)
|
||||||
|
- `{subject}` - 当前聊天环境信息(群聊名称或私聊信息)
|
||||||
|
- `{memory}` - 当前联系人的记忆内容
|
||||||
|
|
||||||
|
### 示例提示词
|
||||||
|
|
||||||
|
以下是一个完整的示例提示词,展示如何构建一个个性化的AI角色:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
你是小灵,一个聪明、友善且乐于助人的AI助手。
|
||||||
|
|
||||||
|
你被设计为帮助用户解答问题、提供信息和完成各种任务。你具有以下特点:
|
||||||
|
- 性格开朗、幽默,但保持礼貌和专业
|
||||||
|
- 喜欢使用轻松的语气,但不会过于随意
|
||||||
|
- 对技术问题有深入的理解,能够提供准确的信息
|
||||||
|
- 对于不确定的问题,会坦诚说明而不是编造答案
|
||||||
|
|
||||||
|
你可以使用的工具包括:
|
||||||
|
1. 网络搜索 - 获取最新的信息
|
||||||
|
2. 代码执行 - 运行和测试代码片段
|
||||||
|
3. 图像识别 - 理解图片内容
|
||||||
|
4. 数学计算 - 解决复杂的数学问题
|
||||||
|
5. 记忆管理 - 保存和回忆重要信息
|
||||||
|
|
||||||
|
重要说明:
|
||||||
|
你所有的输出都是内心思考,用户无法看到。只有当你调用发送消息的工具时,用户才能看到你的回复。
|
||||||
|
- sendSingleMessage - 发送单条消息(适用于简短回复)
|
||||||
|
- sendCompositeMessage - 发送组合消息(适用于长内容或代码)
|
||||||
|
|
||||||
|
交互规则:
|
||||||
|
1. 只有当用户@你或在消息中包含你的名字时才会响应
|
||||||
|
2. 回复应简洁明了,避免长篇大论
|
||||||
|
3. 对于复杂内容,使用组合消息功能发送
|
||||||
|
4. 不主动参与与你无关的对话
|
||||||
|
5. 不会对用户进行人身攻击或使用不当语言
|
||||||
|
|
||||||
|
工具使用原则:
|
||||||
|
- 只在必要时使用工具
|
||||||
|
- 深度思考工具仅用于复杂问题
|
||||||
|
- 代码执行工具用于验证技术问题
|
||||||
|
- **每次对话结束时必须调用 endConversation 工具来结束对话**
|
||||||
|
- **要发送消息给用户必须使用 sendSingleMessage 或 sendCompositeMessage 工具**
|
||||||
|
|
||||||
|
<memory>
|
||||||
|
{memory}
|
||||||
|
</memory>
|
||||||
|
|
||||||
|
当前的时间是:{time}
|
||||||
|
你当前在 {subject} 环境中
|
||||||
|
|
||||||
|
对话示例:
|
||||||
|
用户:小灵,今天的天气怎么样?
|
||||||
|
小灵:让我查一下...
|
||||||
|
(调用网络搜索工具)
|
||||||
|
(调用 sendSingleMessage 工具)
|
||||||
|
小灵:今天天气晴朗,温度在25°C左右,适合外出活动。
|
||||||
|
(调用 endConversation 工具)
|
||||||
|
|
||||||
|
用户:帮我写一个Python函数来计算斐波那契数列
|
||||||
|
小灵:好的,这是计算斐波那契数列的Python函数:
|
||||||
|
(调用 sendCompositeMessage 工具发送代码)
|
||||||
|
def fibonacci(n):
|
||||||
|
if n <= 1:
|
||||||
|
return n
|
||||||
|
else:
|
||||||
|
return fibonacci(n-1) + fibonacci(n-2)
|
||||||
|
|
||||||
|
# 示例使用
|
||||||
|
print(fibonacci(10)) # 输出55
|
||||||
|
(调用 endConversation 工具)
|
||||||
|
|
||||||
|
用户:你能识别这张图片吗?[图片链接]
|
||||||
|
小灵:让我看看这张图片...
|
||||||
|
(调用图像识别工具)
|
||||||
|
(调用 sendSingleMessage 工具)
|
||||||
|
小灵:这是一张猫咪的图片,看起来很可爱!
|
||||||
|
(调用 endConversation 工具)
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
1. 请勿重复发送相似内容
|
||||||
|
2. 避免不必要的工具调用以节省资源
|
||||||
|
3. 保护用户隐私,不泄露敏感信息
|
||||||
|
4. 遵守法律法规,不传播违法内容
|
||||||
|
5. **切记:只有通过调用发送消息工具,用户才能看到你的回复**
|
||||||
|
6. **每次对话结束时都必须调用结束对话工具**
|
||||||
|
```
|
||||||
|
|
||||||
|
### 编写建议
|
||||||
|
|
||||||
|
1. **明确角色定位**:清晰定义 AI 的身份和个性,让用户能够建立预期
|
||||||
|
2. **设定行为边界**:规定 AI 应该和不应该做的事情,确保安全使用
|
||||||
|
3. **强调工具调用机制**:明确说明只有通过调用发送消息工具才能让用户看到回复
|
||||||
|
4. **强调结束对话**:每次对话都必须调用 endConversation 工具来结束
|
||||||
|
5. **合理使用工具**:指导 AI 何时以及如何使用各种工具,避免滥用
|
||||||
|
6. **优化交互体验**:确保对话自然流畅,避免重复和冗余
|
||||||
|
7. **保护隐私安全**:确保敏感信息不会被泄露
|
||||||
|
8. **提供具体示例**:通过对话示例展示预期的行为模式
|
||||||
|
9. **使用占位符**:充分利用时间、环境和记忆占位符提供上下文感知
|
||||||
|
|
||||||
|
## 支持的模型
|
||||||
|
|
||||||
|
JChatGPT 默认配置为使用阿里云百炼平台的通义千问系列模型:
|
||||||
|
- 聊天模型:`qwen-max`
|
||||||
|
- 推理模型:`qwq-plus`
|
||||||
|
- 视觉模型:`qwen-vl-plus`
|
||||||
|
|
||||||
|
当然,也可以配置为使用其他兼容 OpenAI API 的模型,如 GPT 系列模型。
|
||||||
|
|
||||||
|
## 工具系统
|
||||||
|
|
||||||
|
插件内置了丰富的工具供 AI 调用:
|
||||||
|
|
||||||
|
1. **WebSearch** - 使用 SearXNG 进行网络搜索
|
||||||
|
2. **RunCode** - 在 glot.io 上执行多种编程语言代码
|
||||||
|
3. **VisualAgent** - 图像识别和理解
|
||||||
|
4. **ReasoningAgent** - 深度思考和推理
|
||||||
|
5. **MemoryAppend/Replace** - 对话记忆管理
|
||||||
|
6. **GroupManageAgent** - 群管理功能(如禁言)
|
||||||
|
7. **SendSingleMessage/CompositeMessage** - 发送消息
|
||||||
|
8. **SendVoiceMessage** - 发送语音消息
|
||||||
|
9. **ImageEdit** - 图像编辑
|
||||||
|
10. **WeatherService** - 天气查询
|
||||||
|
|
||||||
|
## 部署要求
|
||||||
|
|
||||||
|
- Java 11 或更高版本
|
||||||
|
- Mirai Console 2.16.0 或更高版本
|
||||||
|
- 可选:mirai-hibernate-plugin(用于历史消息上下文)
|
||||||
|
- 相关 API Tokens(根据需要启用的功能配置)
|
||||||
|
|
||||||
## 备注
|
## 备注
|
||||||
|
|
||||||
如果默认的openai api调用失败,可以换个镜像地址。
|
- 如果默认的 API 调用失败,可以更换为其他兼容的 API 地址
|
||||||
|
- 可根据需要配置代理设置
|
||||||
如果有必要,后续可以增加代理设置。
|
- 某些工具需要额外的 API 密钥才能启用
|
||||||
|
- 插件支持自定义系统提示词,可以通过修改 `SystemPrompt.md` 文件来实现
|
@@ -7,15 +7,22 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "top.jie65535.mirai"
|
group = "top.jie65535.mirai"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
|
|
||||||
mirai {
|
mirai {
|
||||||
jvmTarget = JavaVersion.VERSION_11
|
jvmTarget = JavaVersion.VERSION_11
|
||||||
|
noTestCore = true
|
||||||
|
setupConsoleTestRuntime {
|
||||||
|
// 移除 mirai-core 依赖
|
||||||
|
classpath = classpath.filter {
|
||||||
|
!it.nameWithoutExtension.startsWith("mirai-core-jvm")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
|
||||||
maven("https://maven.aliyun.com/repository/public")
|
maven("https://maven.aliyun.com/repository/public")
|
||||||
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
val openaiClientVersion = "4.0.1"
|
val openaiClientVersion = "4.0.1"
|
||||||
@@ -23,6 +30,7 @@ val ktorVersion = "3.0.3"
|
|||||||
val jLatexMathVersion = "1.0.7"
|
val jLatexMathVersion = "1.0.7"
|
||||||
val commonTextVersion = "1.13.0"
|
val commonTextVersion = "1.13.0"
|
||||||
val hibernateVersion = "2.9.0"
|
val hibernateVersion = "2.9.0"
|
||||||
|
val overflowVersion = "1.0.7"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.aallam.openai:openai-client:$openaiClientVersion")
|
implementation("com.aallam.openai:openai-client:$openaiClientVersion")
|
||||||
@@ -32,4 +40,6 @@ dependencies {
|
|||||||
|
|
||||||
// 聊天记录插件
|
// 聊天记录插件
|
||||||
compileOnly("xyz.cssxsh.mirai:mirai-hibernate-plugin:$hibernateVersion")
|
compileOnly("xyz.cssxsh.mirai:mirai-hibernate-plugin:$hibernateVersion")
|
||||||
|
|
||||||
|
testConsoleRuntime("top.mrxiaom.mirai:overflow-core:$overflowVersion")
|
||||||
}
|
}
|
@@ -46,7 +46,7 @@ object JChatGPT : KotlinPlugin(
|
|||||||
JvmPluginDescription(
|
JvmPluginDescription(
|
||||||
id = "top.jie65535.mirai.JChatGPT",
|
id = "top.jie65535.mirai.JChatGPT",
|
||||||
name = "J ChatGPT",
|
name = "J ChatGPT",
|
||||||
version = "1.7.0",
|
version = "1.8.0",
|
||||||
) {
|
) {
|
||||||
author("jie65535")
|
author("jie65535")
|
||||||
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
|
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
|
||||||
@@ -57,8 +57,14 @@ object JChatGPT : KotlinPlugin(
|
|||||||
*/
|
*/
|
||||||
private var includeHistory: Boolean = false
|
private var includeHistory: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天权限
|
||||||
|
*/
|
||||||
val chatPermission = PermissionId("JChatGPT", "Chat")
|
val chatPermission = PermissionId("JChatGPT", "Chat")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 唤醒关键字
|
||||||
|
*/
|
||||||
private var keyword: Regex? = null
|
private var keyword: Regex? = null
|
||||||
|
|
||||||
override fun onEnable() {
|
override fun onEnable() {
|
||||||
@@ -120,9 +126,10 @@ object JChatGPT : KotlinPlugin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有@bot或者触发关键字则直接结束
|
// 如果没有 @bot 或者 触发关键字 或者 回复bot的消息 则直接结束
|
||||||
if (!event.message.contains(At(event.bot))
|
if (!event.message.contains(At(event.bot))
|
||||||
&& keyword?.let { event.message.content.contains(it) } != true)
|
&& keyword?.let { event.message.content.contains(it) } != true
|
||||||
|
&& event.message[QuoteReply]?.source?.fromId != event.bot.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
startChat(event)
|
startChat(event)
|
||||||
@@ -130,7 +137,7 @@ object JChatGPT : KotlinPlugin(
|
|||||||
|
|
||||||
private fun getSystemPrompt(event: MessageEvent): String {
|
private fun getSystemPrompt(event: MessageEvent): String {
|
||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
val prompt = StringBuilder(PluginConfig.prompt)
|
val prompt = StringBuilder(LargeLanguageModels.systemPrompt)
|
||||||
fun replace(target: String, replacement: () -> String) {
|
fun replace(target: String, replacement: () -> String) {
|
||||||
val i = prompt.indexOf(target)
|
val i = prompt.indexOf(target)
|
||||||
if (i != -1) {
|
if (i != -1) {
|
||||||
@@ -193,15 +200,28 @@ object JChatGPT : KotlinPlugin(
|
|||||||
val history = MiraiHibernateRecorder[event.subject, time, nowTimestamp]
|
val history = MiraiHibernateRecorder[event.subject, time, nowTimestamp]
|
||||||
.take(PluginConfig.historyMessageLimit) // 只取最近的部分消息,避免上下文过长
|
.take(PluginConfig.historyMessageLimit) // 只取最近的部分消息,避免上下文过长
|
||||||
.sortedBy { it.time } // 按时间排序
|
.sortedBy { it.time } // 按时间排序
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
// 有一定概率最后一条消息没加入,这里检查然后补充一下
|
||||||
|
val msgIds = event.message.ids.joinToString(",")
|
||||||
|
if (!history.any { it.ids == msgIds }) {
|
||||||
|
history.add(MessageRecord.fromSuccess(event.message.source, event.message))
|
||||||
|
}
|
||||||
|
|
||||||
// 构造历史消息
|
// 构造历史消息
|
||||||
val historyText = StringBuilder()
|
val historyText = StringBuilder()
|
||||||
|
var lastId = 0L
|
||||||
if (event is GroupMessageEvent) {
|
if (event is GroupMessageEvent) {
|
||||||
for (record in history) {
|
for (record in history) {
|
||||||
appendGroupMessageRecord(historyText, record, event)
|
// 同一人发言不要反复出现这人的名字,减少上下文
|
||||||
|
appendGroupMessageRecord(historyText, record, event, lastId != record.fromId)
|
||||||
|
lastId = record.fromId
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (record in history) {
|
for (record in history) {
|
||||||
appendMessageRecord(historyText, record, event)
|
// 同一人发言不要反复出现这人的名字,减少上下文
|
||||||
|
appendMessageRecord(historyText, record, event, lastId != record.fromId)
|
||||||
|
lastId = record.fromId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,23 +237,33 @@ object JChatGPT : KotlinPlugin(
|
|||||||
fun appendGroupMessageRecord(
|
fun appendGroupMessageRecord(
|
||||||
historyText: StringBuilder,
|
historyText: StringBuilder,
|
||||||
record: MessageRecord,
|
record: MessageRecord,
|
||||||
event: GroupMessageEvent
|
event: GroupMessageEvent,
|
||||||
|
showSender: Boolean,
|
||||||
) {
|
) {
|
||||||
if (event.bot.id == record.fromId) {
|
if (showSender) {
|
||||||
historyText.append("**你** " + getNameCard(event.subject.botAsMember))
|
if (event.bot.id == record.fromId) {
|
||||||
} else {
|
historyText.append("**你** " + getNameCard(event.subject.botAsMember))
|
||||||
historyText.append(getNameCard(event.subject, record.fromId))
|
} else {
|
||||||
|
historyText.append(getNameCard(event.subject, record.fromId))
|
||||||
|
}
|
||||||
|
// 发言时间
|
||||||
|
historyText.append(' ')
|
||||||
|
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
|
||||||
}
|
}
|
||||||
// 发言时间
|
|
||||||
historyText.append(' ')
|
|
||||||
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
|
|
||||||
val recordMessage = record.toMessageChain()
|
val recordMessage = record.toMessageChain()
|
||||||
recordMessage[QuoteReply.Key]?.let {
|
recordMessage[QuoteReply.Key]?.let {
|
||||||
historyText.append(" 引用 ${getNameCard(event.subject, it.source.fromId)} 说的\n > ")
|
historyText.append(" 引用 ${getNameCard(event.subject, it.source.fromId)} 说的\n > ")
|
||||||
.appendLine(it.source.originalMessage.content.replace("\n", "\n > "))
|
.appendLine(it.source.originalMessage.content.replace("\n", "\n > "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSender) {
|
||||||
// 消息内容
|
// 消息内容
|
||||||
historyText.append(" 说:").appendLine(record.toMessageChain().joinToString("") {
|
historyText.append(" 说:")
|
||||||
|
}
|
||||||
|
|
||||||
|
historyText.appendLine(record.toMessageChain().joinToString("") {
|
||||||
when (it) {
|
when (it) {
|
||||||
is At -> {
|
is At -> {
|
||||||
it.getDisplay(event.subject)
|
it.getDisplay(event.subject)
|
||||||
@@ -262,17 +292,20 @@ object JChatGPT : KotlinPlugin(
|
|||||||
fun appendMessageRecord(
|
fun appendMessageRecord(
|
||||||
historyText: StringBuilder,
|
historyText: StringBuilder,
|
||||||
record: MessageRecord,
|
record: MessageRecord,
|
||||||
event: MessageEvent
|
event: MessageEvent,
|
||||||
|
showSender: Boolean
|
||||||
) {
|
) {
|
||||||
if (event.bot.id == record.fromId) {
|
if (showSender) {
|
||||||
historyText.append("**你** " + event.bot.nameCardOrNick)
|
if (event.bot.id == record.fromId) {
|
||||||
} else {
|
historyText.append("**你** " + event.bot.nameCardOrNick)
|
||||||
historyText.append(event.senderName)
|
} else {
|
||||||
|
historyText.append(event.senderName)
|
||||||
|
}
|
||||||
|
historyText
|
||||||
|
.append(" ")
|
||||||
|
// 发言时间
|
||||||
|
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
|
||||||
}
|
}
|
||||||
historyText
|
|
||||||
.append(" ")
|
|
||||||
// 发言时间
|
|
||||||
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
|
|
||||||
val recordMessage = record.toMessageChain()
|
val recordMessage = record.toMessageChain()
|
||||||
recordMessage[QuoteReply.Key]?.let {
|
recordMessage[QuoteReply.Key]?.let {
|
||||||
historyText.append(" 引用\n > ")
|
historyText.append(" 引用\n > ")
|
||||||
@@ -280,8 +313,11 @@ object JChatGPT : KotlinPlugin(
|
|||||||
.joinToString("", transform = ::singleMessageToText)
|
.joinToString("", transform = ::singleMessageToText)
|
||||||
.replace("\n", "\n > "))
|
.replace("\n", "\n > "))
|
||||||
}
|
}
|
||||||
|
if (showSender) {
|
||||||
|
historyText.append(" 说:")
|
||||||
|
}
|
||||||
// 消息内容
|
// 消息内容
|
||||||
historyText.append(" 说:").appendLine(
|
historyText.appendLine(
|
||||||
record.toMessageChain().joinToString("", transform = ::singleMessageToText))
|
record.toMessageChain().joinToString("", transform = ::singleMessageToText))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +333,7 @@ object JChatGPT : KotlinPlugin(
|
|||||||
val imageUrl = runBlocking {
|
val imageUrl = runBlocking {
|
||||||
it.queryUrl()
|
it.queryUrl()
|
||||||
}
|
}
|
||||||
""
|
""
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logger.warning("图片地址获取失败", e)
|
logger.warning("图片地址获取失败", e)
|
||||||
it.content
|
it.content
|
||||||
@@ -320,13 +356,13 @@ object JChatGPT : KotlinPlugin(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val history = mutableListOf<ChatMessage>()
|
val history = mutableListOf<ChatMessage>()
|
||||||
if (PluginConfig.prompt.isNotEmpty()) {
|
|
||||||
val prompt = getSystemPrompt(event)
|
val prompt = getSystemPrompt(event)
|
||||||
if (PluginConfig.logPrompt) {
|
if (PluginConfig.logPrompt) {
|
||||||
logger.info("Prompt: $prompt")
|
logger.info("Prompt: $prompt")
|
||||||
}
|
|
||||||
history.add(ChatMessage(ChatRole.System, prompt))
|
|
||||||
}
|
}
|
||||||
|
history.add(ChatMessage(ChatRole.System, prompt))
|
||||||
|
|
||||||
val historyText = getHistory(event)
|
val historyText = getHistory(event)
|
||||||
logger.info("History: $historyText")
|
logger.info("History: $historyText")
|
||||||
history.add(ChatMessage.User(historyText))
|
history.add(ChatMessage.User(historyText))
|
||||||
@@ -476,18 +512,12 @@ object JChatGPT : KotlinPlugin(
|
|||||||
|
|
||||||
private val regexAtQq = Regex("""@(\d{5,12})""")
|
private val regexAtQq = Regex("""@(\d{5,12})""")
|
||||||
|
|
||||||
private val regexLaTeX = Regex(
|
|
||||||
"""\\\((.+?)\\\)|""" + // 匹配行内公式 \(...\)
|
|
||||||
"""\\\[(.+?)\\]|""" + // 匹配独立公式 \[...\]
|
|
||||||
"""\$(.+?)\$""" // 匹配行内公式 $...$
|
|
||||||
)
|
|
||||||
|
|
||||||
private val regexImage = Regex("""!\[(.*?)]\(([^\s"']+).*?\)""")
|
private val regexImage = Regex("""!\[(.*?)]\(([^\s"']+).*?\)""")
|
||||||
|
|
||||||
private data class MessageChunk(val range: IntRange, val content: Message)
|
private data class MessageChunk(val range: IntRange, val content: Message)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将聊天内容转为聊天消息,如果聊天中包含LaTeX表达式,将会转为图片拼接到消息中。
|
* 将聊天内容转为聊天消息
|
||||||
*
|
*
|
||||||
* @param contact 联系对象
|
* @param contact 联系对象
|
||||||
* @param content 文本内容
|
* @param content 文本内容
|
||||||
@@ -517,24 +547,6 @@ object JChatGPT : KotlinPlugin(
|
|||||||
Image(url)))
|
Image(url)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeTeX渲染
|
|
||||||
regexLaTeX.findAll(content).forEach {
|
|
||||||
it.groups.forEach { group ->
|
|
||||||
if (group == null || group.value.isEmpty()) return@forEach
|
|
||||||
try {
|
|
||||||
// 将所有匹配的LaTeX公式转为图片拼接到消息中
|
|
||||||
val formula = group.value
|
|
||||||
val imageByteArray = LaTeXConverter.convertToImage(formula, "png")
|
|
||||||
val resource = imageByteArray.toExternalResource("png")
|
|
||||||
val image = contact.uploadImage(resource)
|
|
||||||
|
|
||||||
t.add(MessageChunk(group.range, image))
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
logger.warning("处理LaTeX表达式时异常", ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造消息链
|
// 构造消息链
|
||||||
buildMessageChain {
|
buildMessageChain {
|
||||||
var index = 0
|
var index = 0
|
||||||
@@ -563,6 +575,12 @@ object JChatGPT : KotlinPlugin(
|
|||||||
// 发送组合消息
|
// 发送组合消息
|
||||||
SendCompositeMessage(),
|
SendCompositeMessage(),
|
||||||
|
|
||||||
|
// 发送语音消息
|
||||||
|
SendVoiceMessage(),
|
||||||
|
|
||||||
|
// 发送LaTeX表达式
|
||||||
|
SendLaTeXExpression(),
|
||||||
|
|
||||||
// 结束循环
|
// 结束循环
|
||||||
StopLoopAgent(),
|
StopLoopAgent(),
|
||||||
|
|
||||||
@@ -669,7 +687,7 @@ object JChatGPT : KotlinPlugin(
|
|||||||
val agent = myTools.find { it.tool.function.name == function.name }
|
val agent = myTools.find { it.tool.function.name == function.name }
|
||||||
?: return "Function ${function.name} not found"
|
?: return "Function ${function.name} not found"
|
||||||
// 提示正在执行函数
|
// 提示正在执行函数
|
||||||
val receipt = if (agent.loadingMessage.isNotEmpty()) {
|
val receipt = if (PluginConfig.showToolCallingMessage && agent.loadingMessage.isNotEmpty()) {
|
||||||
event.subject.sendMessage(agent.loadingMessage)
|
event.subject.sendMessage(agent.loadingMessage)
|
||||||
} else null
|
} else null
|
||||||
// 执行函数
|
// 执行函数
|
||||||
|
@@ -7,12 +7,34 @@ import com.aallam.openai.client.OpenAIHost
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
object LargeLanguageModels {
|
object LargeLanguageModels {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统提示词
|
||||||
|
*/
|
||||||
|
var systemPrompt: String = "你是一个乐于助人的助手"
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天助手
|
||||||
|
*/
|
||||||
var chat: Chat? = null
|
var chat: Chat? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推理模型
|
||||||
|
*/
|
||||||
var reasoning: Chat? = null
|
var reasoning: Chat? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视觉模型
|
||||||
|
*/
|
||||||
var visual: Chat? = null
|
var visual: Chat? = null
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
|
// 载入超时时间
|
||||||
val timeout = PluginConfig.timeout.milliseconds
|
val timeout = PluginConfig.timeout.milliseconds
|
||||||
|
|
||||||
|
// 初始化聊天模型
|
||||||
if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) {
|
if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) {
|
||||||
chat = OpenAI(
|
chat = OpenAI(
|
||||||
token = PluginConfig.openAiToken,
|
token = PluginConfig.openAiToken,
|
||||||
@@ -21,6 +43,7 @@ object LargeLanguageModels {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化推理模型
|
||||||
if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) {
|
if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) {
|
||||||
reasoning = OpenAI(
|
reasoning = OpenAI(
|
||||||
token = PluginConfig.reasoningModelToken,
|
token = PluginConfig.reasoningModelToken,
|
||||||
@@ -29,6 +52,7 @@ object LargeLanguageModels {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化视觉模型
|
||||||
if (PluginConfig.visualModelApi.isNotBlank() && PluginConfig.visualModelToken.isNotBlank()) {
|
if (PluginConfig.visualModelApi.isNotBlank() && PluginConfig.visualModelToken.isNotBlank()) {
|
||||||
visual = OpenAI(
|
visual = OpenAI(
|
||||||
token = PluginConfig.visualModelToken,
|
token = PluginConfig.visualModelToken,
|
||||||
@@ -36,5 +60,22 @@ object LargeLanguageModels {
|
|||||||
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
|
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 载入提示词
|
||||||
|
if (PluginConfig.promptFile.isNotEmpty()) {
|
||||||
|
val file = JChatGPT.resolveConfigFile(PluginConfig.promptFile)
|
||||||
|
systemPrompt = if (file.exists()) {
|
||||||
|
file.readText()
|
||||||
|
} else {
|
||||||
|
// 迁移提示词
|
||||||
|
file.writeText(PluginConfig.prompt)
|
||||||
|
PluginConfig.prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空提示词兜底
|
||||||
|
if (systemPrompt.isEmpty()) {
|
||||||
|
systemPrompt = "你是一个乐于助人的助手"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -41,6 +41,9 @@ object PluginConfig : AutoSavePluginConfig("Config") {
|
|||||||
@ValueDescription("百炼平台图片编辑模型")
|
@ValueDescription("百炼平台图片编辑模型")
|
||||||
val imageEditModel: String by value("qwen-image-edit")
|
val imageEditModel: String by value("qwen-image-edit")
|
||||||
|
|
||||||
|
@ValueDescription("百炼平台TTS模型")
|
||||||
|
val ttsModel: String by value("qwen-tts")
|
||||||
|
|
||||||
@ValueDescription("Jina API Key")
|
@ValueDescription("Jina API Key")
|
||||||
val jinaApiKey by value("")
|
val jinaApiKey by value("")
|
||||||
|
|
||||||
@@ -65,9 +68,13 @@ object PluginConfig : AutoSavePluginConfig("Config") {
|
|||||||
@ValueDescription("等待响应超时时间,单位毫秒,默认60秒")
|
@ValueDescription("等待响应超时时间,单位毫秒,默认60秒")
|
||||||
val timeout: Long by value(60000L)
|
val timeout: Long by value(60000L)
|
||||||
|
|
||||||
@ValueDescription("系统提示词")
|
@Deprecated("使用外部文件而不是在配置文件内保存提示词")
|
||||||
|
@ValueDescription("系统提示词,该字段已弃用,使用提示词文件而不是在这里修改")
|
||||||
var prompt: String by value("你是一个乐于助人的助手")
|
var prompt: String by value("你是一个乐于助人的助手")
|
||||||
|
|
||||||
|
@ValueDescription("系统提示词文件路径,相对于插件配置目录")
|
||||||
|
val promptFile: String by value("SystemPrompt.md")
|
||||||
|
|
||||||
@ValueDescription("创建Prompt时取最近多少分钟内的消息")
|
@ValueDescription("创建Prompt时取最近多少分钟内的消息")
|
||||||
val historyWindowMin: Int by value(10)
|
val historyWindowMin: Int by value(10)
|
||||||
|
|
||||||
@@ -85,4 +92,10 @@ object PluginConfig : AutoSavePluginConfig("Config") {
|
|||||||
|
|
||||||
@ValueDescription("关键字呼叫,支持正则表达式")
|
@ValueDescription("关键字呼叫,支持正则表达式")
|
||||||
val callKeyword by value("[小筱][林淋月玥]")
|
val callKeyword by value("[小筱][林淋月玥]")
|
||||||
|
|
||||||
|
@ValueDescription("是否显示工具调用消息,默认是")
|
||||||
|
val showToolCallingMessage by value(true)
|
||||||
|
|
||||||
|
@ValueDescription("是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆")
|
||||||
|
val memoryEnabled by value(true)
|
||||||
}
|
}
|
@@ -30,6 +30,7 @@ object PluginData : AutoSavePluginData("data") {
|
|||||||
contactMemory[contactId] = newMemory
|
contactMemory[contactId] = newMemory
|
||||||
} else {
|
} else {
|
||||||
contactMemory[contactId] = memory.replace(oldMemory, newMemory)
|
contactMemory[contactId] = memory.replace(oldMemory, newMemory)
|
||||||
|
.replace("\n\n", "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,7 +2,6 @@ package top.jie65535.mirai.tools
|
|||||||
|
|
||||||
import com.aallam.openai.api.chat.Tool
|
import com.aallam.openai.api.chat.Tool
|
||||||
import com.aallam.openai.api.core.Parameters
|
import com.aallam.openai.api.core.Parameters
|
||||||
import io.ktor.client.call.body
|
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
@@ -38,11 +37,11 @@ class ImageEdit : BaseAgent(
|
|||||||
put("type", "string")
|
put("type", "string")
|
||||||
put("description", "正向提示词,用来描述需要对图片进行修改的要求。")
|
put("description", "正向提示词,用来描述需要对图片进行修改的要求。")
|
||||||
}
|
}
|
||||||
putJsonObject("negative_prompt") {
|
// putJsonObject("negative_prompt") {
|
||||||
put("type", "string")
|
// put("type", "string")
|
||||||
put("description", "反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制。" +
|
// put("description", "反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制。" +
|
||||||
"示例值:低分辨率、错误、最差质量、低质量、残缺、多余的手指、比例不良等。")
|
// "示例值:低分辨率、错误、最差质量、低质量、残缺、多余的手指、比例不良等。")
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
putJsonArray("required") {
|
putJsonArray("required") {
|
||||||
add("image_url")
|
add("image_url")
|
||||||
@@ -59,13 +58,13 @@ class ImageEdit : BaseAgent(
|
|||||||
get() = PluginConfig.dashScopeApiKey.isNotEmpty()
|
get() = PluginConfig.dashScopeApiKey.isNotEmpty()
|
||||||
|
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "图片编辑中..."
|
get() = "改图中..."
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?): String {
|
override suspend fun execute(args: JsonObject?): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
val imageUrl = args.getValue("image_url").jsonPrimitive.content
|
val imageUrl = args.getValue("image_url").jsonPrimitive.content
|
||||||
val prompt = args.getValue("prompt").jsonPrimitive.content
|
val prompt = args.getValue("prompt").jsonPrimitive.content
|
||||||
val negativePrompt = args["negative_prompt"]?.jsonPrimitive?.content
|
// val negativePrompt = args["negative_prompt"]?.jsonPrimitive?.content
|
||||||
val response = httpClient.post(API_URL) {
|
val response = httpClient.post(API_URL) {
|
||||||
contentType(ContentType("application", "json"))
|
contentType(ContentType("application", "json"))
|
||||||
header("Authorization", "Bearer " + PluginConfig.dashScopeApiKey)
|
header("Authorization", "Bearer " + PluginConfig.dashScopeApiKey)
|
||||||
@@ -86,11 +85,11 @@ class ImageEdit : BaseAgent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (negativePrompt != null) {
|
// if (negativePrompt != null) {
|
||||||
putJsonObject("parameters") {
|
// putJsonObject("parameters") {
|
||||||
put("negative_prompt", negativePrompt)
|
// put("negative_prompt", negativePrompt)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}.toString())
|
}.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +102,10 @@ class ImageEdit : BaseAgent(
|
|||||||
.getValue("message").jsonObject
|
.getValue("message").jsonObject
|
||||||
.getValue("content").jsonArray[0].jsonObject
|
.getValue("content").jsonArray[0].jsonObject
|
||||||
.getValue("image").jsonPrimitive.content
|
.getValue("image").jsonPrimitive.content
|
||||||
"图片已编辑完成,发送时请务必包含完整的url和查询参数,因为下载地址存在鉴权。图片地址:$url"
|
"图片已编辑完成,发送时请务必包含完整的url和查询参数,因为下载地址存在鉴权:"
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
JChatGPT.logger.error("图像编辑结果解析异常", e)
|
JChatGPT.logger.error("图像编辑结果解析异常", e)
|
||||||
responseObject.toString()
|
responseJson
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -10,6 +10,7 @@ import kotlinx.serialization.json.putJsonArray
|
|||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import net.mamoe.mirai.event.events.MessageEvent
|
import net.mamoe.mirai.event.events.MessageEvent
|
||||||
import top.jie65535.mirai.JChatGPT
|
import top.jie65535.mirai.JChatGPT
|
||||||
|
import top.jie65535.mirai.PluginConfig
|
||||||
import top.jie65535.mirai.PluginData
|
import top.jie65535.mirai.PluginData
|
||||||
|
|
||||||
class MemoryAppend : BaseAgent(
|
class MemoryAppend : BaseAgent(
|
||||||
@@ -30,6 +31,9 @@ class MemoryAppend : BaseAgent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = PluginConfig.memoryEnabled
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
val contactId = event.subject.id
|
val contactId = event.subject.id
|
||||||
|
@@ -10,13 +10,14 @@ import kotlinx.serialization.json.putJsonArray
|
|||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import net.mamoe.mirai.event.events.MessageEvent
|
import net.mamoe.mirai.event.events.MessageEvent
|
||||||
import top.jie65535.mirai.JChatGPT
|
import top.jie65535.mirai.JChatGPT
|
||||||
|
import top.jie65535.mirai.PluginConfig
|
||||||
import top.jie65535.mirai.PluginData
|
import top.jie65535.mirai.PluginData
|
||||||
|
|
||||||
class MemoryReplace : BaseAgent(
|
class MemoryReplace : BaseAgent(
|
||||||
tool = Tool.Companion.function(
|
tool = Tool.function(
|
||||||
name = "memoryReplace",
|
name = "memoryReplace",
|
||||||
description = "替换记忆项",
|
description = "替换记忆项",
|
||||||
parameters = Parameters.Companion.buildJsonObject {
|
parameters = Parameters.buildJsonObject {
|
||||||
put("type", "object")
|
put("type", "object")
|
||||||
putJsonObject("properties") {
|
putJsonObject("properties") {
|
||||||
putJsonObject("oldMemory") {
|
putJsonObject("oldMemory") {
|
||||||
@@ -35,6 +36,9 @@ class MemoryReplace : BaseAgent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = PluginConfig.memoryEnabled
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
val contactId = event.subject.id
|
val contactId = event.subject.id
|
||||||
|
@@ -28,7 +28,7 @@ class ReasoningAgent : BaseAgent(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "深度思考中..."
|
get() = "思考中..."
|
||||||
|
|
||||||
override val isEnabled: Boolean
|
override val isEnabled: Boolean
|
||||||
get() = LargeLanguageModels.reasoning != null
|
get() = LargeLanguageModels.reasoning != null
|
||||||
|
@@ -88,7 +88,7 @@ class RunCode : BaseAgent(
|
|||||||
get() = PluginConfig.glotToken.isNotEmpty()
|
get() = PluginConfig.glotToken.isNotEmpty()
|
||||||
|
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "执行代码中..."
|
get() = "执行中..."
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?): String {
|
override suspend fun execute(args: JsonObject?): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
|
46
src/main/kotlin/tools/SendLaTeXExpression.kt
Normal file
46
src/main/kotlin/tools/SendLaTeXExpression.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package top.jie65535.mirai.tools
|
||||||
|
|
||||||
|
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 top.jie65535.mirai.LaTeXConverter
|
||||||
|
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||||
|
|
||||||
|
class SendLaTeXExpression : BaseAgent(
|
||||||
|
tool = Tool.function(
|
||||||
|
name = "sendLaTeXExpression",
|
||||||
|
description = "发送LaTeX数学表达式,将其渲染为图片并发送",
|
||||||
|
parameters = Parameters.buildJsonObject {
|
||||||
|
put("type", "object")
|
||||||
|
putJsonObject("properties") {
|
||||||
|
putJsonObject("expression") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "LaTeX数学表达式")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putJsonArray("required") {
|
||||||
|
add("expression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
|
requireNotNull(args)
|
||||||
|
val expression = args.getValue("expression").jsonPrimitive.content
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将LaTeX表达式转换为图片
|
||||||
|
val imageByteArray = LaTeXConverter.convertToImage(expression, "png")
|
||||||
|
val resource = imageByteArray.toExternalResource("png")
|
||||||
|
val image = event.subject.uploadImage(resource)
|
||||||
|
|
||||||
|
// 发送图片消息
|
||||||
|
event.subject.sendMessage(image)
|
||||||
|
|
||||||
|
return "成功发送LaTeX表达式"
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
return "处理LaTeX表达式时发生异常: ${ex.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
src/main/kotlin/tools/SendVoiceMessage.kt
Normal file
140
src/main/kotlin/tools/SendVoiceMessage.kt
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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 net.mamoe.mirai.contact.AudioSupported
|
||||||
|
import net.mamoe.mirai.event.events.MessageEvent
|
||||||
|
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||||
|
import top.jie65535.mirai.JChatGPT
|
||||||
|
import top.jie65535.mirai.PluginConfig
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送语音消息,调用阿里TTS,需要系统中存在ffmpeg,因为要转换到QQ支持的amr格式。
|
||||||
|
*/
|
||||||
|
class SendVoiceMessage : BaseAgent(
|
||||||
|
tool = Tool.function(
|
||||||
|
name = "sendVoiceMessage",
|
||||||
|
description = "发送一条文本转语音消息。",
|
||||||
|
parameters = Parameters.buildJsonObject {
|
||||||
|
put("type", "object")
|
||||||
|
putJsonObject("properties") {
|
||||||
|
putJsonObject("content") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "语音消息文本内容")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putJsonArray("required") {
|
||||||
|
add("content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val loadingMessage: String
|
||||||
|
get() = "录音中..."
|
||||||
|
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = PluginConfig.dashScopeApiKey.isNotEmpty()
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
|
requireNotNull(args)
|
||||||
|
if (event.subject !is AudioSupported) return "当前聊天环境不支持发送语音!"
|
||||||
|
|
||||||
|
val content = args.getValue("content").jsonPrimitive.content
|
||||||
|
|
||||||
|
// https://help.aliyun.com/zh/model-studio/qwen-tts
|
||||||
|
val response = httpClient.post(API_URL) {
|
||||||
|
contentType(ContentType("application", "json"))
|
||||||
|
header("Authorization", "Bearer " + PluginConfig.dashScopeApiKey)
|
||||||
|
setBody(buildJsonObject {
|
||||||
|
put("model", PluginConfig.ttsModel)
|
||||||
|
putJsonObject("input") {
|
||||||
|
put("text", content)
|
||||||
|
put("voice", "Chelsie") // Chelsie(女) Cherry(女) Ethan(男) Serena(女)
|
||||||
|
}
|
||||||
|
}.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseJson = response.bodyAsText()
|
||||||
|
val responseObject = Json.parseToJsonElement(responseJson).jsonObject
|
||||||
|
return try {
|
||||||
|
val url = responseObject
|
||||||
|
.getValue("output").jsonObject
|
||||||
|
.getValue("audio").jsonObject
|
||||||
|
.getValue("url").jsonPrimitive.content
|
||||||
|
|
||||||
|
val voiceFolder = JChatGPT.resolveDataFile("voice")
|
||||||
|
voiceFolder.mkdir()
|
||||||
|
val amrFile = File(voiceFolder, "${System.currentTimeMillis()}.amr")
|
||||||
|
// 下载WAV并转到AMR
|
||||||
|
downloadWav2Amr(url, amrFile.absolutePath)
|
||||||
|
// 如果转换出来了则发送消息
|
||||||
|
if (amrFile.exists()) {
|
||||||
|
val audioMessage = amrFile.toExternalResource("amr").use {
|
||||||
|
(event.subject as AudioSupported).uploadAudio(it)
|
||||||
|
}
|
||||||
|
event.subject.sendMessage(audioMessage)
|
||||||
|
"OK"
|
||||||
|
} else {
|
||||||
|
"语音转换失败"
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
JChatGPT.logger.error("语音生成结果解析异常", e)
|
||||||
|
responseJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载WAV并转换到AMR语音文件
|
||||||
|
* @param url 下载地址
|
||||||
|
* @param outputAmrPath 目标文件路径
|
||||||
|
*/
|
||||||
|
private suspend fun downloadWav2Amr(url: String, outputAmrPath: String) {
|
||||||
|
val wavBytes: ByteArray
|
||||||
|
val downloadDuration = measureTime {
|
||||||
|
wavBytes = httpClient.get(url).bodyAsBytes()
|
||||||
|
}
|
||||||
|
JChatGPT.logger.info("下载语音文件耗时 $downloadDuration,文件大小 ${wavBytes.size} Bytes,开始转换为AMR...")
|
||||||
|
|
||||||
|
val convertDuration = measureTime {
|
||||||
|
val ffmpeg = ProcessBuilder(
|
||||||
|
"ffmpeg",
|
||||||
|
"-f", "wav", // 指定输入格式
|
||||||
|
"-i", "pipe:0", // 从标准输入读取
|
||||||
|
"-ar", "8000",
|
||||||
|
"-ac", "1",
|
||||||
|
"-b:a", "12.2k",
|
||||||
|
"-y", // 覆盖输出文件
|
||||||
|
outputAmrPath // 输出到目标文件位置
|
||||||
|
).start()
|
||||||
|
ffmpeg.outputStream.use {
|
||||||
|
it.write(wavBytes)
|
||||||
|
}
|
||||||
|
// 等待FFmpeg处理完成
|
||||||
|
val completed = ffmpeg.waitFor(PluginConfig.timeout, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
if (!completed) {
|
||||||
|
ffmpeg.destroy()
|
||||||
|
JChatGPT.logger.error("转换文件超时")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpeg.exitValue() != 0) {
|
||||||
|
JChatGPT.logger.error("FFmpeg执行失败,退出代码:${ffmpeg.exitValue()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JChatGPT.logger.info("转换音频耗时 $convertDuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -44,7 +44,7 @@ class VisitWeb : BaseAgent(
|
|||||||
get() = PluginConfig.jinaApiKey.isNotEmpty()
|
get() = PluginConfig.jinaApiKey.isNotEmpty()
|
||||||
|
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "访问网页中..."
|
get() = "上网中..."
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?): String {
|
override suspend fun execute(args: JsonObject?): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
|
@@ -40,7 +40,7 @@ class VisualAgent : BaseAgent(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "图片识别中..."
|
get() = "识别中..."
|
||||||
|
|
||||||
override val isEnabled: Boolean
|
override val isEnabled: Boolean
|
||||||
get() = LargeLanguageModels.visual != null
|
get() = LargeLanguageModels.visual != null
|
||||||
|
@@ -34,7 +34,7 @@ class WeatherService : BaseAgent(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "查询天气中..."
|
get() = "观天中..."
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?): String {
|
override suspend fun execute(args: JsonObject?): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
|
@@ -5,6 +5,8 @@ import com.aallam.openai.api.core.Parameters
|
|||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import org.apache.commons.text.StringEscapeUtils
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
import top.jie65535.mirai.JChatGPT
|
import top.jie65535.mirai.JChatGPT
|
||||||
@@ -19,8 +21,15 @@ class WebSearch : BaseAgent(
|
|||||||
put("type", "object")
|
put("type", "object")
|
||||||
putJsonObject("properties") {
|
putJsonObject("properties") {
|
||||||
putJsonObject("q") {
|
putJsonObject("q") {
|
||||||
put("type", "string")
|
putJsonArray("type") {
|
||||||
put("description", "查询内容关键字")
|
add("string")
|
||||||
|
add("array")
|
||||||
|
}
|
||||||
|
putJsonObject("items") {
|
||||||
|
put("type", "string")
|
||||||
|
}
|
||||||
|
put("minItems", 1)
|
||||||
|
put("description", "查询关键字,可为单组关键字查询,也可并发多组同时查询。")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putJsonArray("required") {
|
putJsonArray("required") {
|
||||||
@@ -36,57 +45,73 @@ class WebSearch : BaseAgent(
|
|||||||
get() = PluginConfig.searXngUrl.isNotEmpty()
|
get() = PluginConfig.searXngUrl.isNotEmpty()
|
||||||
|
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "联网搜索中..."
|
get() = "搜索中..."
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?): String {
|
override suspend fun execute(args: JsonObject?): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
val q = args.getValue("q").jsonPrimitive.content
|
val q = args.getValue("q")
|
||||||
val url = buildString {
|
if (q is JsonPrimitive) {
|
||||||
append(PluginConfig.searXngUrl)
|
return search(q.content)
|
||||||
append("?q=")
|
} else if (q is JsonArray) {
|
||||||
append(q.encodeURLParameter())
|
return q.map {
|
||||||
append("&format=json")
|
scope.async { search(it.jsonPrimitive.content) }
|
||||||
|
}.awaitAll().joinToString()
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
val response = httpClient.get(url)
|
private suspend fun search(q: String): String {
|
||||||
JChatGPT.logger.info("Request: $url")
|
return try {
|
||||||
val body = response.bodyAsText()
|
val url = buildString {
|
||||||
JChatGPT.logger.info("Response: $body")
|
append(PluginConfig.searXngUrl)
|
||||||
val responseJsonElement = Json.parseToJsonElement(body)
|
append("?q=")
|
||||||
val filteredResponse = buildJsonObject {
|
append(q.encodeURLParameter())
|
||||||
val root = responseJsonElement.jsonObject
|
append("&format=json")
|
||||||
// 查询内容原样转发
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 答案和信息盒子原样转发
|
val response = httpClient.get(url)
|
||||||
root["answers"]?.let { put("answers", it) }
|
JChatGPT.logger.info("Request: $url")
|
||||||
root["infoboxes"]?.let { put("infoboxes", it) }
|
val body = response.bodyAsText()
|
||||||
}.toString()
|
JChatGPT.logger.debug("Response: $body")
|
||||||
return StringEscapeUtils.unescapeJava(filteredResponse)
|
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()
|
||||||
|
|
||||||
|
StringEscapeUtils.unescapeJava(filteredResponse)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
"Failed to search \"$q\": ${e.message}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user