mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-06-23 00:49:31 +08:00
Add global skill system for self-accumulated knowledge
Introduce a cross-group skill system that lets the bot distill reusable
knowledge into markdown docs and load them on demand, keeping day-to-day
context pollution low.
- SkillStore manages data/skills/*.md files with name/description
frontmatter and an in-memory index cache (rebuilt on init/reload)
- Only the skill index (name + one-line description) is injected via the
new {skills} system-prompt placeholder; bodies load on demand
- New tools: loadSkill / saveSkill (upsert, iterate = load+overwrite) /
deleteSkill, gated by PluginConfig.skillsEnabled
- Skill names validated against ^[A-Za-z0-9_-]+$ to prevent traversal
- Wire SkillStore.init into onEnable and refresh on /jgpt reload; add
/jgpt skills listing command
- Bump version to 1.12.0; update README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
53
README.md
53
README.md
@@ -7,6 +7,7 @@ JChatGPT 是一个基于 Kotlin 的 Mirai Console 插件,它将大型语言模
|
|||||||
- **多模型支持**:支持聊天模型、推理模型和视觉模型
|
- **多模型支持**:支持聊天模型、推理模型和视觉模型
|
||||||
- **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等
|
- **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等
|
||||||
- **上下文记忆**:支持持久化记忆存储
|
- **上下文记忆**:支持持久化记忆存储
|
||||||
|
- **技能系统**:Bot 可在群聊中自我沉淀可复用知识,全局跨群、按需加载、低上下文污染
|
||||||
- **用户画像系统**:好感度、印象、标签、Bot 自定义代号
|
- **用户画像系统**:好感度、印象、标签、Bot 自定义代号
|
||||||
- **Token消耗统计**:按天 × 用户 × 群聚合记录,支持多维度统计查询
|
- **Token消耗统计**:按天 × 用户 × 群聚合记录,支持多维度统计查询
|
||||||
- **LaTeX 渲染**:自动将数学表达式渲染为图片
|
- **LaTeX 渲染**:自动将数学表达式渲染为图片
|
||||||
@@ -30,6 +31,7 @@ AI 可以自动调用多种工具来完成复杂任务:
|
|||||||
- 推理思考(需要配置推理模型)
|
- 推理思考(需要配置推理模型)
|
||||||
- 群管理(禁言等,需启用相应权限)
|
- 群管理(禁言等,需启用相应权限)
|
||||||
- 记忆管理(添加和修改对话记忆)
|
- 记忆管理(添加和修改对话记忆)
|
||||||
|
- 技能管理(沉淀、加载、迭代、删除可复用知识技能)
|
||||||
- 聊天历史搜索(按关键词、发送者、时间范围检索群聊消息,需启用历史消息上下文)
|
- 聊天历史搜索(按关键词、发送者、时间范围检索群聊消息,需启用历史消息上下文)
|
||||||
|
|
||||||
## 权限列表
|
## 权限列表
|
||||||
@@ -45,6 +47,7 @@ AI 可以自动调用多种工具来完成复杂任务:
|
|||||||
- `/jgpt reload` - 重载配置文件
|
- `/jgpt reload` - 重载配置文件
|
||||||
- `/jgpt clearMemory` - 清空所有对话记忆
|
- `/jgpt clearMemory` - 清空所有对话记忆
|
||||||
- `/jgpt clearContextCache` - 清空所有对话上下文缓存
|
- `/jgpt clearContextCache` - 清空所有对话上下文缓存
|
||||||
|
- `/jgpt skills` - 列出当前所有技能(名称 + 简介)
|
||||||
|
|
||||||
### 好感度管理
|
### 好感度管理
|
||||||
- `/jgpt setFavor <user> <value>` - 设置指定用户的好感度值(-100~100)
|
- `/jgpt setFavor <user> <value>` - 设置指定用户的好感度值(-100~100)
|
||||||
@@ -135,6 +138,8 @@ callKeyword: '[小筱][林淋月玥]'
|
|||||||
showToolCallingMessage: true
|
showToolCallingMessage: true
|
||||||
# 是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆
|
# 是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆
|
||||||
memoryEnabled: true
|
memoryEnabled: true
|
||||||
|
# 是否启用技能系统,技能存在data/skills目录(全局跨群),提示词中需要加上{skills}来注入技能索引
|
||||||
|
skillsEnabled: true
|
||||||
# 是否启用好感度系统
|
# 是否启用好感度系统
|
||||||
enableFavorabilitySystem: true
|
enableFavorabilitySystem: true
|
||||||
# 好感度每日基础偏移速度(点/天)
|
# 好感度每日基础偏移速度(点/天)
|
||||||
@@ -165,6 +170,7 @@ JChatGPT 使用系统提示词来定义 AI 的行为和个性。提示词文件
|
|||||||
- `{time}` - 当前时间(格式:yyyy年MM月dd E HH:mm:ss)
|
- `{time}` - 当前时间(格式:yyyy年MM月dd E HH:mm:ss)
|
||||||
- `{subject}` - 当前聊天环境信息(群聊名称或私聊信息)
|
- `{subject}` - 当前聊天环境信息(群聊名称或私聊信息)
|
||||||
- `{memory}` - 当前联系人的记忆内容
|
- `{memory}` - 当前联系人的记忆内容
|
||||||
|
- `{skills}` - 全局技能索引(仅名称 + 一句话简介,正文按需加载)
|
||||||
|
|
||||||
### 示例提示词
|
### 示例提示词
|
||||||
|
|
||||||
@@ -279,12 +285,13 @@ JChatGPT 默认配置为使用阿里云百炼平台的通义千问系列模型
|
|||||||
3. **VisualAgent** - 图像识别和理解
|
3. **VisualAgent** - 图像识别和理解
|
||||||
4. **ReasoningAgent** - 深度思考和推理
|
4. **ReasoningAgent** - 深度思考和推理
|
||||||
5. **MemoryAppend/Replace** - 对话记忆管理
|
5. **MemoryAppend/Replace** - 对话记忆管理
|
||||||
6. **GroupManageAgent** - 群管理功能(如禁言)
|
6. **LoadSkill/SaveSkill/DeleteSkill** - 技能管理(加载、沉淀/迭代、删除全局技能)
|
||||||
7. **SendSingleMessage/CompositeMessage** - 发送消息
|
7. **GroupManageAgent** - 群管理功能(如禁言)
|
||||||
8. **SendVoiceMessage** - 发送语音消息
|
8. **SendSingleMessage/CompositeMessage** - 发送消息
|
||||||
9. **ImageAgent** - 图像生成与编辑(文生图、单图编辑、多图融合)
|
9. **SendVoiceMessage** - 发送语音消息
|
||||||
10. **WeatherService** - 天气查询
|
10. **ImageAgent** - 图像生成与编辑(文生图、单图编辑、多图融合)
|
||||||
11. **SearchChatHistory** - 按关键词、发送者、时间范围搜索群聊消息历史(依赖 mirai-hibernate-plugin)
|
11. **WeatherService** - 天气查询
|
||||||
|
12. **SearchChatHistory** - 按关键词、发送者、时间范围搜索群聊消息历史(依赖 mirai-hibernate-plugin)
|
||||||
|
|
||||||
## 用户画像系统
|
## 用户画像系统
|
||||||
|
|
||||||
@@ -315,6 +322,40 @@ JChatGPT 维护对每位用户的画像,由好感度、Bot 自定义代号、
|
|||||||
- `enableFavorabilitySystem` - 是否启用画像系统(默认:true)
|
- `enableFavorabilitySystem` - 是否启用画像系统(默认:true)
|
||||||
- `favorabilityBaseShiftSpeed` - 好感度每日基础偏移速度(点/天,默认:2.0)
|
- `favorabilityBaseShiftSpeed` - 好感度每日基础偏移速度(点/天,默认:2.0)
|
||||||
|
|
||||||
|
## 技能系统
|
||||||
|
|
||||||
|
JChatGPT 允许 Bot 在群聊中**自我沉淀可复用的知识和经验**,存成"技能"(本质是带简介的提示词文档),并在需要时按需加载。例如群友反复问到某个软件/模组的用法、常见报错排查,Bot 在回答或被纠正的过程中学到的内容可以沉淀成技能,跨群复用。
|
||||||
|
|
||||||
|
### 设计要点
|
||||||
|
- **全局跨群**:技能不按群隔离,任何群学到的都能在其它群复用。
|
||||||
|
- **低上下文污染**:日常对话只在系统提示词里常驻"技能名 + 一句话简介"的索引,正文不进上下文。
|
||||||
|
- **按需加载**:当话题命中某个技能时,Bot 才用 `loadSkill` 把正文读入上下文。
|
||||||
|
- **自我迭代**:Bot 通过 `saveSkill` 沉淀/更新技能,过时的用 `deleteSkill` 删除,无需人工介入(也支持手动编辑文件)。
|
||||||
|
|
||||||
|
### 存储格式
|
||||||
|
每个技能 = `data/skills/` 下的一个 markdown 文件,带 frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: kubejs-basics
|
||||||
|
description: KubeJS 基础语法、常见报错与排查方法
|
||||||
|
---
|
||||||
|
|
||||||
|
(正文:沉淀下来的知识、经验或提示词)
|
||||||
|
```
|
||||||
|
|
||||||
|
技能名为 kebab-case,只能包含字母、数字、下划线、连字符(用于校验防止路径穿越)。
|
||||||
|
|
||||||
|
### 相关工具
|
||||||
|
- **loadSkill(name)** - 加载某技能正文到上下文
|
||||||
|
- **saveSkill(name, description, content)** - 新增或整篇覆盖一个技能(迭代 = 先 loadSkill 读全文,改好后同名写回)
|
||||||
|
- **deleteSkill(name)** - 删除过时或失效的技能
|
||||||
|
|
||||||
|
### 配置与命令
|
||||||
|
- 配置项 `skillsEnabled`(默认 true)控制是否启用技能系统
|
||||||
|
- 系统提示词中需包含 `{skills}` 占位符以注入技能索引
|
||||||
|
- `/jgpt skills` - 列出当前所有技能;`/jgpt reload` 会重新扫描技能目录
|
||||||
|
|
||||||
## Token消耗统计
|
## Token消耗统计
|
||||||
|
|
||||||
JChatGPT 按 (日期, userId, groupId) 三元组聚合每次对话的 Token 消耗,提供多维度统计查询。
|
JChatGPT 按 (日期, userId, groupId) 三元组聚合每次对话的 Token 消耗,提供多维度统计查询。
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "top.jie65535.mirai"
|
group = "top.jie65535.mirai"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
|
|
||||||
mirai {
|
mirai {
|
||||||
jvmTarget = JavaVersion.VERSION_11
|
jvmTarget = JavaVersion.VERSION_11
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ object JChatGPT : KotlinPlugin(
|
|||||||
JvmPluginDescription(
|
JvmPluginDescription(
|
||||||
id = "top.jie65535.mirai.JChatGPT",
|
id = "top.jie65535.mirai.JChatGPT",
|
||||||
name = "J ChatGPT",
|
name = "J ChatGPT",
|
||||||
version = "1.11.0",
|
version = "1.12.0",
|
||||||
) {
|
) {
|
||||||
author("jie65535")
|
author("jie65535")
|
||||||
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
|
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
|
||||||
@@ -82,6 +82,9 @@ object JChatGPT : KotlinPlugin(
|
|||||||
// 初始化 token 使用日聚合存储(独立 JSON 文件,绕开 yamlkt 大数据 bug)
|
// 初始化 token 使用日聚合存储(独立 JSON 文件,绕开 yamlkt 大数据 bug)
|
||||||
TokenUsageStore.init(dataFolder)
|
TokenUsageStore.init(dataFolder)
|
||||||
|
|
||||||
|
// 初始化技能存储(data/skills/ 下的 markdown 文件,全局跨群)
|
||||||
|
SkillStore.init(dataFolder)
|
||||||
|
|
||||||
// 设置Token
|
// 设置Token
|
||||||
LargeLanguageModels.reload()
|
LargeLanguageModels.reload()
|
||||||
|
|
||||||
@@ -269,6 +272,12 @@ object JChatGPT : KotlinPlugin(
|
|||||||
} else memoryText
|
} else memoryText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replace("{skills}") {
|
||||||
|
if (PluginConfig.skillsEnabled) {
|
||||||
|
SkillStore.buildIndexPrompt()
|
||||||
|
} else "暂无技能"
|
||||||
|
}
|
||||||
|
|
||||||
replace("{meme}") {
|
replace("{meme}") {
|
||||||
memePrompt?.let { return@replace it }
|
memePrompt?.let { return@replace it }
|
||||||
|
|
||||||
@@ -928,6 +937,15 @@ object JChatGPT : KotlinPlugin(
|
|||||||
// 记忆修改
|
// 记忆修改
|
||||||
MemoryReplace(),
|
MemoryReplace(),
|
||||||
|
|
||||||
|
// 技能:加载
|
||||||
|
LoadSkill(),
|
||||||
|
|
||||||
|
// 技能:沉淀/迭代
|
||||||
|
SaveSkill(),
|
||||||
|
|
||||||
|
// 技能:删除
|
||||||
|
DeleteSkill(),
|
||||||
|
|
||||||
// 搜索聊天历史
|
// 搜索聊天历史
|
||||||
SearchChatHistory(),
|
SearchChatHistory(),
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,24 @@ object PluginCommands : CompositeCommand(
|
|||||||
PluginConfig.reload()
|
PluginConfig.reload()
|
||||||
PluginData.reload()
|
PluginData.reload()
|
||||||
LargeLanguageModels.reload()
|
LargeLanguageModels.reload()
|
||||||
|
SkillStore.reload()
|
||||||
sendMessage("OK")
|
sendMessage("OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SubCommand
|
||||||
|
suspend fun CommandSender.skills() {
|
||||||
|
val all = SkillStore.all
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
sendMessage("暂无技能")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val response = buildString {
|
||||||
|
appendLine("当前技能(共 ${all.size} 个):")
|
||||||
|
all.forEach { appendLine("- ${it.name}: ${it.description}") }
|
||||||
|
}
|
||||||
|
sendMessage(response.trim())
|
||||||
|
}
|
||||||
|
|
||||||
@SubCommand
|
@SubCommand
|
||||||
suspend fun CommandSender.enable(contact: Contact) {
|
suspend fun CommandSender.enable(contact: Contact) {
|
||||||
when (contact) {
|
when (contact) {
|
||||||
|
|||||||
@@ -123,6 +123,9 @@ object PluginConfig : AutoSavePluginConfig("Config") {
|
|||||||
@ValueDescription("是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆")
|
@ValueDescription("是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆")
|
||||||
val memoryEnabled by value(true)
|
val memoryEnabled by value(true)
|
||||||
|
|
||||||
|
@ValueDescription("是否启用技能系统,技能存在data/skills目录(全局跨群),提示词中需要加上{skills}来注入技能索引")
|
||||||
|
val skillsEnabled by value(true)
|
||||||
|
|
||||||
@ValueDescription("是否启用好感度系统")
|
@ValueDescription("是否启用好感度系统")
|
||||||
val enableFavorabilitySystem by value(true)
|
val enableFavorabilitySystem by value(true)
|
||||||
|
|
||||||
|
|||||||
169
src/main/kotlin/SkillStore.kt
Normal file
169
src/main/kotlin/SkillStore.kt
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package top.jie65535.mirai
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能元信息:用于索引展示,不含正文。
|
||||||
|
* @param name 技能名(kebab-case,同时是文件名),唯一
|
||||||
|
* @param description 一句话简介,决定 bot 何时按需加载
|
||||||
|
*/
|
||||||
|
data class SkillMeta(
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 技能存储。每个技能 = 一个带 frontmatter 的 markdown 文件,放在 data/skills/ 下,全局跨群共享。
|
||||||
|
*
|
||||||
|
* 文件格式:
|
||||||
|
* ```
|
||||||
|
* ---
|
||||||
|
* name: kubejs-basics
|
||||||
|
* description: KubeJS 基础语法、常见报错与排查方法
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* (正文:沉淀下来的知识/经验/提示词)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 索引(name + description)常驻系统提示词,正文按需通过 loadSkill 工具加载,
|
||||||
|
* 以此实现"低上下文污染 + 可自我沉淀迭代"。
|
||||||
|
*/
|
||||||
|
object SkillStore {
|
||||||
|
private lateinit var dir: File
|
||||||
|
|
||||||
|
/** 内存索引缓存,key 为技能名。仅缓存元信息,正文每次按需读盘。 */
|
||||||
|
private val index = linkedMapOf<String, SkillMeta>()
|
||||||
|
|
||||||
|
/** 合法技能名:字母数字、下划线、连字符,防止路径穿越。 */
|
||||||
|
private val nameRegex = Regex("^[A-Za-z0-9_-]+$")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 onEnable 中调用一次,传入插件数据目录。随后可通过 [reload] 刷新。
|
||||||
|
*/
|
||||||
|
fun init(dataFolder: File) {
|
||||||
|
dir = File(dataFolder, "skills")
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重新扫描技能目录,重建内存索引。/jgpt reload 时调用。 */
|
||||||
|
@Synchronized
|
||||||
|
fun reload() {
|
||||||
|
if (!::dir.isInitialized) return
|
||||||
|
index.clear()
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkdirs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir.listFiles { f -> f.isFile && f.extension.equals("md", ignoreCase = true) }
|
||||||
|
?.sortedBy { it.name }
|
||||||
|
?.forEach { file ->
|
||||||
|
try {
|
||||||
|
val (meta, _) = parse(file.readText())
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
val desc = meta["description"].orEmpty()
|
||||||
|
index[name] = SkillMeta(name, desc)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// 单个文件解析失败不影响其它技能加载
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前所有技能元信息。 */
|
||||||
|
val all: List<SkillMeta> @Synchronized get() = index.values.toList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建注入到系统提示词 {skills} 占位符的索引文本,仅含 name + description。
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun buildIndexPrompt(): String {
|
||||||
|
if (index.isEmpty()) return "暂无技能"
|
||||||
|
return index.values.joinToString("\n") { "- ${it.name}: ${it.description}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取技能正文(不含 frontmatter)。技能不存在返回 null。
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun load(name: String): String? {
|
||||||
|
if (!isValidName(name)) return null
|
||||||
|
val file = File(dir, "$name.md")
|
||||||
|
if (!file.exists()) return null
|
||||||
|
return try {
|
||||||
|
parse(file.readText()).second
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增或整篇覆盖一个技能(upsert)。迭代即"读全文→改→整篇写回"。
|
||||||
|
* @return 失败时返回错误信息,成功返回 null
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun save(name: String, description: String, content: String): String? {
|
||||||
|
if (!isValidName(name)) {
|
||||||
|
return "技能名非法,只能包含字母、数字、下划线、连字符:$name"
|
||||||
|
}
|
||||||
|
if (!::dir.isInitialized) return "技能目录未初始化"
|
||||||
|
if (!dir.exists()) dir.mkdirs()
|
||||||
|
val file = File(dir, "$name.md")
|
||||||
|
val safeDesc = description.replace('\n', ' ').trim()
|
||||||
|
val text = buildString {
|
||||||
|
appendLine("---")
|
||||||
|
appendLine("name: $name")
|
||||||
|
appendLine("description: $safeDesc")
|
||||||
|
appendLine("---")
|
||||||
|
appendLine()
|
||||||
|
append(content.trim())
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
file.writeText(text)
|
||||||
|
index[name] = SkillMeta(name, safeDesc)
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"写入技能文件失败:${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一个技能。
|
||||||
|
* @return 是否删除成功(技能不存在返回 false)
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun delete(name: String): Boolean {
|
||||||
|
if (!isValidName(name)) return false
|
||||||
|
val file = File(dir, "$name.md")
|
||||||
|
index.remove(name)
|
||||||
|
return if (file.exists()) file.delete() else false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidName(name: String): Boolean = nameRegex.matches(name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 frontmatter。返回 (元信息键值对, 正文)。
|
||||||
|
* 无 frontmatter 时元信息为空,正文为全文。
|
||||||
|
*/
|
||||||
|
private fun parse(raw: String): Pair<Map<String, String>, String> {
|
||||||
|
val text = raw.replace("\r\n", "\n")
|
||||||
|
if (!text.startsWith("---")) {
|
||||||
|
return emptyMap<String, String>() to text.trim()
|
||||||
|
}
|
||||||
|
val lines = text.split("\n")
|
||||||
|
// 第一行是 ---,找到下一处 --- 作为 frontmatter 结束
|
||||||
|
val endIdx = (1 until lines.size).firstOrNull { lines[it].trim() == "---" }
|
||||||
|
?: return emptyMap<String, String>() to text.trim()
|
||||||
|
val meta = mutableMapOf<String, String>()
|
||||||
|
for (i in 1 until endIdx) {
|
||||||
|
val line = lines[i]
|
||||||
|
val sep = line.indexOf(':')
|
||||||
|
if (sep > 0) {
|
||||||
|
val key = line.substring(0, sep).trim()
|
||||||
|
val value = line.substring(sep + 1).trim()
|
||||||
|
meta[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val body = lines.subList(endIdx + 1, lines.size).joinToString("\n").trim()
|
||||||
|
return meta to body
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/kotlin/tools/DeleteSkill.kt
Normal file
50
src/main/kotlin/tools/DeleteSkill.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package top.jie65535.mirai.tools
|
||||||
|
|
||||||
|
import com.aallam.openai.api.chat.Tool
|
||||||
|
import com.aallam.openai.api.core.Parameters
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import net.mamoe.mirai.event.events.MessageEvent
|
||||||
|
import top.jie65535.mirai.JChatGPT
|
||||||
|
import top.jie65535.mirai.PluginConfig
|
||||||
|
import top.jie65535.mirai.SkillStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一个过时或失效的技能。
|
||||||
|
*/
|
||||||
|
class DeleteSkill : BaseAgent(
|
||||||
|
tool = Tool.function(
|
||||||
|
name = "deleteSkill",
|
||||||
|
description = "删除一个已过时或失效的技能。",
|
||||||
|
parameters = Parameters.buildJsonObject {
|
||||||
|
put("type", "object")
|
||||||
|
putJsonObject("properties") {
|
||||||
|
putJsonObject("name") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "要删除的技能名")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putJsonArray("required") {
|
||||||
|
add("name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = PluginConfig.skillsEnabled
|
||||||
|
|
||||||
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
|
requireNotNull(args)
|
||||||
|
val name = args.getValue("name").jsonPrimitive.content
|
||||||
|
JChatGPT.logger.info("Delete skill: \"$name\"")
|
||||||
|
return if (SkillStore.delete(name)) {
|
||||||
|
"OK,技能 \"$name\" 已删除。"
|
||||||
|
} else {
|
||||||
|
"技能 \"$name\" 不存在。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/kotlin/tools/LoadSkill.kt
Normal file
50
src/main/kotlin/tools/LoadSkill.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package top.jie65535.mirai.tools
|
||||||
|
|
||||||
|
import com.aallam.openai.api.chat.Tool
|
||||||
|
import com.aallam.openai.api.core.Parameters
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import net.mamoe.mirai.event.events.MessageEvent
|
||||||
|
import top.jie65535.mirai.JChatGPT
|
||||||
|
import top.jie65535.mirai.PluginConfig
|
||||||
|
import top.jie65535.mirai.SkillStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按需加载某个技能的正文进上下文。技能索引(name+简介)常驻系统提示词,
|
||||||
|
* 当话题命中某技能时调用本工具读取其完整内容。
|
||||||
|
*/
|
||||||
|
class LoadSkill : BaseAgent(
|
||||||
|
tool = Tool.function(
|
||||||
|
name = "loadSkill",
|
||||||
|
description = "当话题命中某个技能时,加载该技能的完整内容到上下文。可用技能见系统提示词中的技能索引。",
|
||||||
|
parameters = Parameters.buildJsonObject {
|
||||||
|
put("type", "object")
|
||||||
|
putJsonObject("properties") {
|
||||||
|
putJsonObject("name") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "技能名(技能索引中的 name)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putJsonArray("required") {
|
||||||
|
add("name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = PluginConfig.skillsEnabled
|
||||||
|
|
||||||
|
override val loadingMessage: String = "翻阅资料中..."
|
||||||
|
|
||||||
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
|
requireNotNull(args)
|
||||||
|
val name = args.getValue("name").jsonPrimitive.content
|
||||||
|
JChatGPT.logger.info("Load skill: \"$name\"")
|
||||||
|
val content = SkillStore.load(name)
|
||||||
|
return content ?: "技能 \"$name\" 不存在,可用技能见系统提示词中的技能索引。"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/main/kotlin/tools/SaveSkill.kt
Normal file
62
src/main/kotlin/tools/SaveSkill.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package top.jie65535.mirai.tools
|
||||||
|
|
||||||
|
import com.aallam.openai.api.chat.Tool
|
||||||
|
import com.aallam.openai.api.core.Parameters
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import net.mamoe.mirai.event.events.MessageEvent
|
||||||
|
import top.jie65535.mirai.JChatGPT
|
||||||
|
import top.jie65535.mirai.PluginConfig
|
||||||
|
import top.jie65535.mirai.SkillStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增或整篇覆盖一个技能(全局,跨群共享)。
|
||||||
|
* 用于把群里学到/被纠正的知识沉淀下来;迭代时先用 loadSkill 读全文,改好后整篇写回。
|
||||||
|
*/
|
||||||
|
class SaveSkill : BaseAgent(
|
||||||
|
tool = Tool.function(
|
||||||
|
name = "saveSkill",
|
||||||
|
description = "沉淀或更新一个技能(知识文档),全局跨群共享。新增直接写;迭代时先 loadSkill 读全文,修改后整篇写回。技能名相同则覆盖。",
|
||||||
|
parameters = Parameters.buildJsonObject {
|
||||||
|
put("type", "object")
|
||||||
|
putJsonObject("properties") {
|
||||||
|
putJsonObject("name") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "技能名,kebab-case,只能含字母/数字/下划线/连字符,如 kubejs-basics。相同则覆盖")
|
||||||
|
}
|
||||||
|
putJsonObject("description") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "一句话简介,会常驻技能索引,决定你以后何时加载它")
|
||||||
|
}
|
||||||
|
putJsonObject("content") {
|
||||||
|
put("type", "string")
|
||||||
|
put("description", "技能正文(markdown),沉淀的知识、经验或提示词。整篇内容,会覆盖旧版本")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putJsonArray("required") {
|
||||||
|
add("name")
|
||||||
|
add("description")
|
||||||
|
add("content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
override val isEnabled: Boolean
|
||||||
|
get() = PluginConfig.skillsEnabled
|
||||||
|
|
||||||
|
override val loadingMessage: String = "记下来了..."
|
||||||
|
|
||||||
|
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
|
||||||
|
requireNotNull(args)
|
||||||
|
val name = args.getValue("name").jsonPrimitive.content
|
||||||
|
val description = args.getValue("description").jsonPrimitive.content
|
||||||
|
val content = args.getValue("content").jsonPrimitive.content
|
||||||
|
JChatGPT.logger.info("Save skill: \"$name\" - \"$description\"")
|
||||||
|
val error = SkillStore.save(name, description, content)
|
||||||
|
return error ?: "OK,技能 \"$name\" 已保存。"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user