mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' into patch-2
commit
a25f046c6d
|
|
@ -321,6 +321,7 @@ run_pet.bat
|
||||||
/plugins/*
|
/plugins/*
|
||||||
!/plugins
|
!/plugins
|
||||||
!/plugins/hello_world_plugin
|
!/plugins/hello_world_plugin
|
||||||
|
!/plugins/emoji_manage_plugin
|
||||||
!/plugins/take_picture_plugin
|
!/plugins/take_picture_plugin
|
||||||
|
|
||||||
config.toml
|
config.toml
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
0.10.4饼 表达方式优化
|
## [0.10.4] - 2025-9-22
|
||||||
无了
|
表达方式优化
|
||||||
|
|
||||||
|
|
||||||
## [0.10.3] - 2025-9-22
|
## [0.10.3] - 2025-9-22
|
||||||
### 🌟 主要功能更改
|
### 🌟 主要功能更改
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ version = "1.1.1"
|
||||||
```toml
|
```toml
|
||||||
[[api_providers]]
|
[[api_providers]]
|
||||||
name = "DeepSeek" # 服务商名称(自定义)
|
name = "DeepSeek" # 服务商名称(自定义)
|
||||||
base_url = "https://api.deepseek.cn/v1" # API服务的基础URL
|
base_url = "https://api.deepseek.com/v1" # API服务的基础URL
|
||||||
api_key = "your-api-key-here" # API密钥
|
api_key = "your-api-key-here" # API密钥
|
||||||
client_type = "openai" # 客户端类型
|
client_type = "openai" # 客户端类型
|
||||||
max_retry = 2 # 最大重试次数
|
max_retry = 2 # 最大重试次数
|
||||||
|
|
@ -43,19 +43,19 @@ retry_interval = 10 # 重试间隔(秒)
|
||||||
| `name` | ✅ | 服务商名称,需要在模型配置中引用 | - |
|
| `name` | ✅ | 服务商名称,需要在模型配置中引用 | - |
|
||||||
| `base_url` | ✅ | API服务的基础URL | - |
|
| `base_url` | ✅ | API服务的基础URL | - |
|
||||||
| `api_key` | ✅ | API密钥,请替换为实际密钥 | - |
|
| `api_key` | ✅ | API密钥,请替换为实际密钥 | - |
|
||||||
| `client_type` | ❌ | 客户端类型:`openai`(OpenAI格式)或 `gemini`(Gemini格式,现在支持不良好) | `openai` |
|
| `client_type` | ❌ | 客户端类型:`openai`(OpenAI格式)或 `gemini`(Gemini格式) | `openai` |
|
||||||
| `max_retry` | ❌ | API调用失败时的最大重试次数 | 2 |
|
| `max_retry` | ❌ | API调用失败时的最大重试次数 | 2 |
|
||||||
| `timeout` | ❌ | API请求超时时间(秒) | 30 |
|
| `timeout` | ❌ | API请求超时时间(秒) | 30 |
|
||||||
| `retry_interval` | ❌ | 重试间隔时间(秒) | 10 |
|
| `retry_interval` | ❌ | 重试间隔时间(秒) | 10 |
|
||||||
|
|
||||||
**请注意,对于`client_type`为`gemini`的模型,`base_url`字段无效。**
|
**请注意,对于`client_type`为`gemini`的模型,`retry`字段由`gemini`自己决定。**
|
||||||
### 2.3 支持的服务商示例
|
### 2.3 支持的服务商示例
|
||||||
|
|
||||||
#### DeepSeek
|
#### DeepSeek
|
||||||
```toml
|
```toml
|
||||||
[[api_providers]]
|
[[api_providers]]
|
||||||
name = "DeepSeek"
|
name = "DeepSeek"
|
||||||
base_url = "https://api.deepseek.cn/v1"
|
base_url = "https://api.deepseek.com/v1"
|
||||||
api_key = "your-deepseek-api-key"
|
api_key = "your-deepseek-api-key"
|
||||||
client_type = "openai"
|
client_type = "openai"
|
||||||
```
|
```
|
||||||
|
|
@ -73,7 +73,7 @@ client_type = "openai"
|
||||||
```toml
|
```toml
|
||||||
[[api_providers]]
|
[[api_providers]]
|
||||||
name = "Google"
|
name = "Google"
|
||||||
base_url = "https://api.google.com/v1"
|
base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||||
api_key = "your-google-api-key"
|
api_key = "your-google-api-key"
|
||||||
client_type = "gemini" # 注意:Gemini需要使用特殊客户端
|
client_type = "gemini" # 注意:Gemini需要使用特殊客户端
|
||||||
```
|
```
|
||||||
|
|
@ -131,9 +131,20 @@ enable_thinking = false # 禁用思考
|
||||||
[models.extra_params]
|
[models.extra_params]
|
||||||
thinking = {type = "disabled"} # 禁用思考
|
thinking = {type = "disabled"} # 禁用思考
|
||||||
```
|
```
|
||||||
|
|
||||||
|
而对于`gemini`需要单独进行配置
|
||||||
|
```toml
|
||||||
|
[[models]]
|
||||||
|
model_identifier = "gemini-2.5-flash"
|
||||||
|
name = "gemini-2.5-flash"
|
||||||
|
api_provider = "Google"
|
||||||
|
[models.extra_params]
|
||||||
|
thinking_budget = 0 # 禁用思考
|
||||||
|
# thinking_budget = -1 由模型自己决定
|
||||||
|
```
|
||||||
|
|
||||||
请注意,`extra_params` 的配置应该构成一个合法的TOML字典结构,具体内容取决于API服务商的要求。
|
请注意,`extra_params` 的配置应该构成一个合法的TOML字典结构,具体内容取决于API服务商的要求。
|
||||||
|
|
||||||
**请注意,对于`client_type`为`gemini`的模型,此字段无效。**
|
|
||||||
### 3.3 配置参数说明
|
### 3.3 配置参数说明
|
||||||
|
|
||||||
| 参数 | 必填 | 说明 |
|
| 参数 | 必填 | 说明 |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 1,
|
||||||
|
"name": "BetterEmoji",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "更好的表情包管理插件",
|
||||||
|
"author": {
|
||||||
|
"name": "SengokuCola",
|
||||||
|
"url": "https://github.com/SengokuCola"
|
||||||
|
},
|
||||||
|
"license": "GPL-v3.0-or-later",
|
||||||
|
|
||||||
|
"host_application": {
|
||||||
|
"min_version": "0.10.4"
|
||||||
|
},
|
||||||
|
"homepage_url": "https://github.com/SengokuCola/BetterEmoji",
|
||||||
|
"repository_url": "https://github.com/SengokuCola/BetterEmoji",
|
||||||
|
"keywords": ["emoji", "manage", "plugin"],
|
||||||
|
"categories": ["Examples", "Tutorial"],
|
||||||
|
|
||||||
|
"default_locale": "zh-CN",
|
||||||
|
"locales_path": "_locales",
|
||||||
|
|
||||||
|
"plugin_info": {
|
||||||
|
"is_built_in": false,
|
||||||
|
"plugin_type": "emoji_manage",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "action",
|
||||||
|
"name": "hello_greeting",
|
||||||
|
"description": "向用户发送问候消息"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "action",
|
||||||
|
"name": "bye_greeting",
|
||||||
|
"description": "向用户发送告别消息",
|
||||||
|
"activation_modes": ["keyword"],
|
||||||
|
"keywords": ["再见", "bye", "88", "拜拜"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"name": "time",
|
||||||
|
"description": "查询当前时间",
|
||||||
|
"pattern": "/time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"问候和告别功能",
|
||||||
|
"时间查询命令",
|
||||||
|
"配置文件示例",
|
||||||
|
"新手教程代码"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
import random
|
||||||
|
from typing import List, Tuple, Type, Any
|
||||||
|
from src.plugin_system import (
|
||||||
|
BasePlugin,
|
||||||
|
register_plugin,
|
||||||
|
BaseAction,
|
||||||
|
BaseCommand,
|
||||||
|
BaseTool,
|
||||||
|
ComponentInfo,
|
||||||
|
ActionActivationType,
|
||||||
|
ConfigField,
|
||||||
|
BaseEventHandler,
|
||||||
|
EventType,
|
||||||
|
MaiMessages,
|
||||||
|
ToolParamType,
|
||||||
|
ReplyContentType,
|
||||||
|
emoji_api,
|
||||||
|
)
|
||||||
|
from maim_message import Seg
|
||||||
|
from src.config.config import global_config
|
||||||
|
from src.common.logger import get_logger
|
||||||
|
logger = get_logger("emoji_manage_plugin")
|
||||||
|
|
||||||
|
class AddEmojiCommand(BaseCommand):
|
||||||
|
command_name = "add_emoji"
|
||||||
|
command_description = "添加表情包"
|
||||||
|
command_pattern = r".*/emoji add.*"
|
||||||
|
|
||||||
|
async def execute(self) -> Tuple[bool, str, bool]:
|
||||||
|
# 查找消息中的表情包
|
||||||
|
# logger.info(f"查找消息中的表情包: {self.message.message_segment}")
|
||||||
|
|
||||||
|
emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment)
|
||||||
|
|
||||||
|
if not emoji_base64_list:
|
||||||
|
return False, "未在消息中找到表情包或图片", False
|
||||||
|
|
||||||
|
# 注册找到的表情包
|
||||||
|
success_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, emoji_base64 in enumerate(emoji_base64_list):
|
||||||
|
try:
|
||||||
|
# 使用emoji_api注册表情包(让API自动生成唯一文件名)
|
||||||
|
result = await emoji_api.register_emoji(emoji_base64)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
success_count += 1
|
||||||
|
description = result.get("description", "未知描述")
|
||||||
|
emotions = result.get("emotions", [])
|
||||||
|
replaced = result.get("replaced", False)
|
||||||
|
|
||||||
|
result_msg = f"表情包 {i+1} 注册成功{'(替换旧表情包)' if replaced else '(新增表情包)'}"
|
||||||
|
if description:
|
||||||
|
result_msg += f"\n描述: {description}"
|
||||||
|
if emotions:
|
||||||
|
result_msg += f"\n情感标签: {', '.join(emotions)}"
|
||||||
|
|
||||||
|
results.append(result_msg)
|
||||||
|
else:
|
||||||
|
fail_count += 1
|
||||||
|
error_msg = result.get("message", "注册失败")
|
||||||
|
results.append(f"表情包 {i+1} 注册失败: {error_msg}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
fail_count += 1
|
||||||
|
results.append(f"表情包 {i+1} 注册时发生错误: {str(e)}")
|
||||||
|
|
||||||
|
# 构建返回消息
|
||||||
|
total_count = success_count + fail_count
|
||||||
|
summary_msg = f"表情包注册完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count} 个"
|
||||||
|
|
||||||
|
# 如果有结果详情,添加到返回消息中
|
||||||
|
details_msg = ""
|
||||||
|
if results:
|
||||||
|
details_msg = "\n" + "\n".join(results)
|
||||||
|
final_msg = summary_msg + details_msg
|
||||||
|
else:
|
||||||
|
final_msg = summary_msg
|
||||||
|
|
||||||
|
# 使用表达器重写回复
|
||||||
|
try:
|
||||||
|
from src.plugin_system.apis import generator_api
|
||||||
|
|
||||||
|
# 构建重写数据
|
||||||
|
rewrite_data = {
|
||||||
|
"raw_reply": summary_msg,
|
||||||
|
"reason": f"注册了表情包:{details_msg}\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 调用表达器重写
|
||||||
|
result_status, data = await generator_api.rewrite_reply(
|
||||||
|
chat_stream=self.message.chat_stream,
|
||||||
|
reply_data=rewrite_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result_status:
|
||||||
|
# 发送重写后的回复
|
||||||
|
for reply_seg in data.reply_set.reply_data:
|
||||||
|
send_data = reply_seg.content
|
||||||
|
await self.send_text(send_data)
|
||||||
|
|
||||||
|
return success_count > 0, final_msg, success_count > 0
|
||||||
|
else:
|
||||||
|
# 如果重写失败,发送原始消息
|
||||||
|
await self.send_text(final_msg)
|
||||||
|
return success_count > 0, final_msg, success_count > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 如果表达器调用失败,发送原始消息
|
||||||
|
logger.error(f"[add_emoji] 表达器重写失败: {e}")
|
||||||
|
await self.send_text(final_msg)
|
||||||
|
return success_count > 0, final_msg, success_count > 0
|
||||||
|
|
||||||
|
def find_and_return_emoji_in_message(self, message_segments) -> List[str]:
|
||||||
|
emoji_base64_list = []
|
||||||
|
|
||||||
|
# 处理单个Seg对象的情况
|
||||||
|
if isinstance(message_segments, Seg):
|
||||||
|
if message_segments.type == "emoji":
|
||||||
|
emoji_base64_list.append(message_segments.data)
|
||||||
|
elif message_segments.type == "image":
|
||||||
|
# 假设图片数据是base64编码的
|
||||||
|
emoji_base64_list.append(message_segments.data)
|
||||||
|
elif message_segments.type == "seglist":
|
||||||
|
# 递归处理嵌套的Seg列表
|
||||||
|
emoji_base64_list.extend(self.find_and_return_emoji_in_message(message_segments.data))
|
||||||
|
return emoji_base64_list
|
||||||
|
|
||||||
|
# 处理Seg列表的情况
|
||||||
|
for seg in message_segments:
|
||||||
|
if seg.type == "emoji":
|
||||||
|
emoji_base64_list.append(seg.data)
|
||||||
|
elif seg.type == "image":
|
||||||
|
# 假设图片数据是base64编码的
|
||||||
|
emoji_base64_list.append(seg.data)
|
||||||
|
elif seg.type == "seglist":
|
||||||
|
# 递归处理嵌套的Seg列表
|
||||||
|
emoji_base64_list.extend(self.find_and_return_emoji_in_message(seg.data))
|
||||||
|
return emoji_base64_list
|
||||||
|
|
||||||
|
class ListEmojiCommand(BaseCommand):
|
||||||
|
"""列表表情包Command - 响应/emoji list命令"""
|
||||||
|
|
||||||
|
command_name = "emoji_list"
|
||||||
|
command_description = "列表表情包"
|
||||||
|
|
||||||
|
# === 命令设置(必须填写)===
|
||||||
|
command_pattern = r"^/emoji list(\s+\d+)?$" # 匹配 "/emoji list" 或 "/emoji list 数量"
|
||||||
|
|
||||||
|
async def execute(self) -> Tuple[bool, str, bool]:
|
||||||
|
"""执行列表表情包"""
|
||||||
|
from src.plugin_system.apis import emoji_api
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# 解析命令参数
|
||||||
|
import re
|
||||||
|
match = re.match(r"^/emoji list(?:\s+(\d+))?$", self.message.raw_message)
|
||||||
|
max_count = 10 # 默认显示10个
|
||||||
|
if match and match.group(1):
|
||||||
|
max_count = min(int(match.group(1)), 50) # 最多显示50个
|
||||||
|
|
||||||
|
# 获取当前时间
|
||||||
|
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
time_str = now.strftime(time_format)
|
||||||
|
|
||||||
|
# 获取表情包信息
|
||||||
|
emoji_count = emoji_api.get_count()
|
||||||
|
emoji_info = emoji_api.get_info()
|
||||||
|
|
||||||
|
# 构建返回消息
|
||||||
|
message_lines = [
|
||||||
|
f"📊 表情包统计信息 ({time_str})",
|
||||||
|
f"• 总数: {emoji_count} / {emoji_info['max_count']}",
|
||||||
|
f"• 可用: {emoji_info['available_emojis']}",
|
||||||
|
]
|
||||||
|
|
||||||
|
if emoji_count == 0:
|
||||||
|
message_lines.append("\n❌ 暂无表情包")
|
||||||
|
final_message = "\n".join(message_lines)
|
||||||
|
await self.send_text(final_message)
|
||||||
|
return True, final_message, True
|
||||||
|
|
||||||
|
# 获取所有表情包
|
||||||
|
all_emojis = await emoji_api.get_all()
|
||||||
|
if not all_emojis:
|
||||||
|
message_lines.append("\n❌ 无法获取表情包列表")
|
||||||
|
final_message = "\n".join(message_lines)
|
||||||
|
await self.send_text(final_message)
|
||||||
|
return False, final_message, True
|
||||||
|
|
||||||
|
# 显示前N个表情包
|
||||||
|
display_emojis = all_emojis[:max_count]
|
||||||
|
message_lines.append(f"\n📋 显示前 {len(display_emojis)} 个表情包:")
|
||||||
|
|
||||||
|
for i, (emoji_base64, description, emotion) in enumerate(display_emojis, 1):
|
||||||
|
# 截断过长的描述
|
||||||
|
short_desc = description[:50] + "..." if len(description) > 50 else description
|
||||||
|
message_lines.append(f"{i}. {short_desc} [{emotion}]")
|
||||||
|
|
||||||
|
# 如果还有更多表情包,显示总数
|
||||||
|
if len(all_emojis) > max_count:
|
||||||
|
message_lines.append(f"\n💡 还有 {len(all_emojis) - max_count} 个表情包未显示")
|
||||||
|
|
||||||
|
final_message = "\n".join(message_lines)
|
||||||
|
|
||||||
|
# 直接发送文本消息
|
||||||
|
await self.send_text(final_message)
|
||||||
|
|
||||||
|
return True, final_message, True
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteEmojiCommand(BaseCommand):
|
||||||
|
command_name = "delete_emoji"
|
||||||
|
command_description = "删除表情包"
|
||||||
|
command_pattern = r".*/emoji delete.*"
|
||||||
|
|
||||||
|
async def execute(self) -> Tuple[bool, str, bool]:
|
||||||
|
# 查找消息中的表情包图片
|
||||||
|
logger.info(f"查找消息中的表情包用于删除: {self.message.message_segment}")
|
||||||
|
|
||||||
|
emoji_base64_list = self.find_and_return_emoji_in_message(self.message.message_segment)
|
||||||
|
|
||||||
|
if not emoji_base64_list:
|
||||||
|
return False, "未在消息中找到表情包或图片", False
|
||||||
|
|
||||||
|
# 删除找到的表情包
|
||||||
|
success_count = 0
|
||||||
|
fail_count = 0
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, emoji_base64 in enumerate(emoji_base64_list):
|
||||||
|
try:
|
||||||
|
# 计算图片的哈希值来查找对应的表情包
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
# 确保base64字符串只包含ASCII字符
|
||||||
|
if isinstance(emoji_base64, str):
|
||||||
|
emoji_base64_clean = emoji_base64.encode("ascii", errors="ignore").decode("ascii")
|
||||||
|
else:
|
||||||
|
emoji_base64_clean = str(emoji_base64)
|
||||||
|
|
||||||
|
# 计算哈希值
|
||||||
|
image_bytes = base64.b64decode(emoji_base64_clean)
|
||||||
|
emoji_hash = hashlib.md5(image_bytes).hexdigest()
|
||||||
|
|
||||||
|
# 使用emoji_api删除表情包
|
||||||
|
result = await emoji_api.delete_emoji(emoji_hash)
|
||||||
|
|
||||||
|
if result["success"]:
|
||||||
|
success_count += 1
|
||||||
|
description = result.get("description", "未知描述")
|
||||||
|
count_before = result.get("count_before", 0)
|
||||||
|
count_after = result.get("count_after", 0)
|
||||||
|
emotions = result.get("emotions", [])
|
||||||
|
|
||||||
|
result_msg = f"表情包 {i+1} 删除成功"
|
||||||
|
if description:
|
||||||
|
result_msg += f"\n描述: {description}"
|
||||||
|
if emotions:
|
||||||
|
result_msg += f"\n情感标签: {', '.join(emotions)}"
|
||||||
|
result_msg += f"\n表情包数量: {count_before} → {count_after}"
|
||||||
|
|
||||||
|
results.append(result_msg)
|
||||||
|
else:
|
||||||
|
fail_count += 1
|
||||||
|
error_msg = result.get("message", "删除失败")
|
||||||
|
results.append(f"表情包 {i+1} 删除失败: {error_msg}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
fail_count += 1
|
||||||
|
results.append(f"表情包 {i+1} 删除时发生错误: {str(e)}")
|
||||||
|
|
||||||
|
# 构建返回消息
|
||||||
|
total_count = success_count + fail_count
|
||||||
|
summary_msg = f"表情包删除完成: 成功 {success_count} 个,失败 {fail_count} 个,共处理 {total_count} 个"
|
||||||
|
|
||||||
|
# 如果有结果详情,添加到返回消息中
|
||||||
|
details_msg = ""
|
||||||
|
if results:
|
||||||
|
details_msg = "\n" + "\n".join(results)
|
||||||
|
final_msg = summary_msg + details_msg
|
||||||
|
else:
|
||||||
|
final_msg = summary_msg
|
||||||
|
|
||||||
|
# 使用表达器重写回复
|
||||||
|
try:
|
||||||
|
from src.plugin_system.apis import generator_api
|
||||||
|
|
||||||
|
# 构建重写数据
|
||||||
|
rewrite_data = {
|
||||||
|
"raw_reply": summary_msg,
|
||||||
|
"reason": f"删除了表情包:{details_msg}\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 调用表达器重写
|
||||||
|
result_status, data = await generator_api.rewrite_reply(
|
||||||
|
chat_stream=self.message.chat_stream,
|
||||||
|
reply_data=rewrite_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result_status:
|
||||||
|
# 发送重写后的回复
|
||||||
|
for reply_seg in data.reply_set.reply_data:
|
||||||
|
send_data = reply_seg.content
|
||||||
|
await self.send_text(send_data)
|
||||||
|
|
||||||
|
return success_count > 0, final_msg, success_count > 0
|
||||||
|
else:
|
||||||
|
# 如果重写失败,发送原始消息
|
||||||
|
await self.send_text(final_msg)
|
||||||
|
return success_count > 0, final_msg, success_count > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 如果表达器调用失败,发送原始消息
|
||||||
|
logger.error(f"[delete_emoji] 表达器重写失败: {e}")
|
||||||
|
await self.send_text(final_msg)
|
||||||
|
return success_count > 0, final_msg, success_count > 0
|
||||||
|
|
||||||
|
def find_and_return_emoji_in_message(self, message_segments) -> List[str]:
|
||||||
|
emoji_base64_list = []
|
||||||
|
|
||||||
|
# 处理单个Seg对象的情况
|
||||||
|
if isinstance(message_segments, Seg):
|
||||||
|
if message_segments.type == "emoji":
|
||||||
|
emoji_base64_list.append(message_segments.data)
|
||||||
|
elif message_segments.type == "image":
|
||||||
|
# 假设图片数据是base64编码的
|
||||||
|
emoji_base64_list.append(message_segments.data)
|
||||||
|
elif message_segments.type == "seglist":
|
||||||
|
# 递归处理嵌套的Seg列表
|
||||||
|
emoji_base64_list.extend(self.find_and_return_emoji_in_message(message_segments.data))
|
||||||
|
return emoji_base64_list
|
||||||
|
|
||||||
|
# 处理Seg列表的情况
|
||||||
|
for seg in message_segments:
|
||||||
|
if seg.type == "emoji":
|
||||||
|
emoji_base64_list.append(seg.data)
|
||||||
|
elif seg.type == "image":
|
||||||
|
# 假设图片数据是base64编码的
|
||||||
|
emoji_base64_list.append(seg.data)
|
||||||
|
elif seg.type == "seglist":
|
||||||
|
# 递归处理嵌套的Seg列表
|
||||||
|
emoji_base64_list.extend(self.find_and_return_emoji_in_message(seg.data))
|
||||||
|
return emoji_base64_list
|
||||||
|
|
||||||
|
|
||||||
|
class RandomEmojis(BaseCommand):
|
||||||
|
command_name = "random_emojis"
|
||||||
|
command_description = "发送多张随机表情包"
|
||||||
|
command_pattern = r"^/random_emojis$"
|
||||||
|
|
||||||
|
async def execute(self):
|
||||||
|
emojis = await emoji_api.get_random(5)
|
||||||
|
if not emojis:
|
||||||
|
return False, "未找到表情包", False
|
||||||
|
emoji_base64_list = []
|
||||||
|
for emoji in emojis:
|
||||||
|
emoji_base64_list.append(emoji[0])
|
||||||
|
return await self.forward_images(emoji_base64_list)
|
||||||
|
|
||||||
|
async def forward_images(self, images: List[str]):
|
||||||
|
"""
|
||||||
|
把多张图片用合并转发的方式发给用户
|
||||||
|
"""
|
||||||
|
success = await self.send_forward([("0", "神秘用户", [(ReplyContentType.IMAGE, img)]) for img in images])
|
||||||
|
return (True, "已发送随机表情包", True) if success else (False, "发送随机表情包失败", False)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== 插件注册 =====
|
||||||
|
|
||||||
|
|
||||||
|
@register_plugin
|
||||||
|
class EmojiManagePlugin(BasePlugin):
|
||||||
|
"""表情包管理插件 - 管理表情包"""
|
||||||
|
|
||||||
|
# 插件基本信息
|
||||||
|
plugin_name: str = "emoji_manage_plugin" # 内部标识符
|
||||||
|
enable_plugin: bool = False
|
||||||
|
dependencies: List[str] = [] # 插件依赖列表
|
||||||
|
python_dependencies: List[str] = [] # Python包依赖列表
|
||||||
|
config_file_name: str = "config.toml" # 配置文件名
|
||||||
|
|
||||||
|
# 配置节描述
|
||||||
|
config_section_descriptions = {"plugin": "插件基本信息", "emoji": "表情包功能配置"}
|
||||||
|
|
||||||
|
# 配置Schema定义
|
||||||
|
config_schema: dict = {
|
||||||
|
"plugin": {
|
||||||
|
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
|
||||||
|
"config_version": ConfigField(type=str, default="1.0.1", description="配置文件版本"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
||||||
|
return [
|
||||||
|
(RandomEmojis.get_command_info(), RandomEmojis),
|
||||||
|
(AddEmojiCommand.get_command_info(), AddEmojiCommand),
|
||||||
|
(ListEmojiCommand.get_command_info(), ListEmojiCommand),
|
||||||
|
(DeleteEmojiCommand.get_command_info(), DeleteEmojiCommand),
|
||||||
|
]
|
||||||
|
|
@ -237,8 +237,7 @@ class HelloWorldPlugin(BasePlugin):
|
||||||
# 配置Schema定义
|
# 配置Schema定义
|
||||||
config_schema: dict = {
|
config_schema: dict = {
|
||||||
"plugin": {
|
"plugin": {
|
||||||
"name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"),
|
"config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"),
|
||||||
"version": ConfigField(type=str, default="1.0.0", description="插件版本"),
|
|
||||||
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
|
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
|
||||||
},
|
},
|
||||||
"greeting": {
|
"greeting": {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ dependencies = [
|
||||||
"jieba>=0.42.1",
|
"jieba>=0.42.1",
|
||||||
"json-repair>=0.47.6",
|
"json-repair>=0.47.6",
|
||||||
"jsonlines>=4.0.0",
|
"jsonlines>=4.0.0",
|
||||||
"maim-message>=0.3.8",
|
"maim-message>=0.5.1",
|
||||||
"matplotlib>=3.10.3",
|
"matplotlib>=3.10.3",
|
||||||
"networkx>=3.4.2",
|
"networkx>=3.4.2",
|
||||||
"numpy>=2.2.6",
|
"numpy>=2.2.6",
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,36 @@
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile requirements.txt -o requirements.lock
|
# uv pip compile requirements.txt -o requirements.lock
|
||||||
aenum==3.1.16
|
|
||||||
# via reportportal-client
|
|
||||||
aiohappyeyeballs==2.6.1
|
aiohappyeyeballs==2.6.1
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
aiohttp==3.12.14
|
aiohttp==3.12.14
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
|
# aiohttp-cors
|
||||||
# maim-message
|
# maim-message
|
||||||
# reportportal-client
|
aiohttp-cors==0.8.1
|
||||||
|
# via -r requirements.txt
|
||||||
aiosignal==1.4.0
|
aiosignal==1.4.0
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
anyio==4.9.0
|
anyio==4.9.0
|
||||||
# via
|
# via
|
||||||
|
# google-genai
|
||||||
# httpx
|
# httpx
|
||||||
# openai
|
# openai
|
||||||
# starlette
|
# starlette
|
||||||
apscheduler==3.11.0
|
async-timeout==5.0.1
|
||||||
# via -r requirements.txt
|
# via aiohttp
|
||||||
attrs==25.3.0
|
attrs==25.3.0
|
||||||
# via
|
# via
|
||||||
# aiohttp
|
# aiohttp
|
||||||
# jsonlines
|
# jsonlines
|
||||||
|
cachetools==5.5.2
|
||||||
|
# via google-auth
|
||||||
certifi==2025.7.9
|
certifi==2025.7.9
|
||||||
# via
|
# via
|
||||||
# httpcore
|
# httpcore
|
||||||
# httpx
|
# httpx
|
||||||
# reportportal-client
|
|
||||||
# requests
|
# requests
|
||||||
cffi==1.17.1
|
cffi==1.17.1
|
||||||
# via cryptography
|
# via cryptography
|
||||||
|
|
@ -41,24 +43,16 @@ colorama==0.4.6
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
# click
|
# click
|
||||||
# tqdm
|
# tqdm
|
||||||
contourpy==1.3.2
|
|
||||||
# via matplotlib
|
|
||||||
cryptography==45.0.5
|
cryptography==45.0.5
|
||||||
# via
|
# via maim-message
|
||||||
# -r requirements.txt
|
|
||||||
# maim-message
|
|
||||||
customtkinter==5.2.2
|
|
||||||
# via -r requirements.txt
|
|
||||||
cycler==0.12.1
|
|
||||||
# via matplotlib
|
|
||||||
darkdetect==0.8.0
|
|
||||||
# via customtkinter
|
|
||||||
distro==1.9.0
|
distro==1.9.0
|
||||||
# via openai
|
# via openai
|
||||||
dnspython==2.7.0
|
dnspython==2.7.0
|
||||||
# via pymongo
|
# via pymongo
|
||||||
dotenv==0.9.9
|
dotenv==0.9.9
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
|
exceptiongroup==1.3.0
|
||||||
|
# via anyio
|
||||||
faiss-cpu==1.11.0
|
faiss-cpu==1.11.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
fastapi==0.116.0
|
fastapi==0.116.0
|
||||||
|
|
@ -66,12 +60,14 @@ fastapi==0.116.0
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
# maim-message
|
# maim-message
|
||||||
# strawberry-graphql
|
# strawberry-graphql
|
||||||
fonttools==4.58.5
|
|
||||||
# via matplotlib
|
|
||||||
frozenlist==1.7.0
|
frozenlist==1.7.0
|
||||||
# via
|
# via
|
||||||
# aiohttp
|
# aiohttp
|
||||||
# aiosignal
|
# aiosignal
|
||||||
|
google-auth==2.40.3
|
||||||
|
# via google-genai
|
||||||
|
google-genai==1.38.0
|
||||||
|
# via -r requirements.txt
|
||||||
graphql-core==3.2.6
|
graphql-core==3.2.6
|
||||||
# via strawberry-graphql
|
# via strawberry-graphql
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
|
|
@ -81,86 +77,70 @@ h11==0.16.0
|
||||||
httpcore==1.0.9
|
httpcore==1.0.9
|
||||||
# via httpx
|
# via httpx
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
# via openai
|
# via
|
||||||
|
# google-genai
|
||||||
|
# openai
|
||||||
idna==3.10
|
idna==3.10
|
||||||
# via
|
# via
|
||||||
# anyio
|
# anyio
|
||||||
# httpx
|
# httpx
|
||||||
# requests
|
# requests
|
||||||
# yarl
|
# yarl
|
||||||
igraph==0.11.9
|
|
||||||
# via python-igraph
|
|
||||||
jieba==0.42.1
|
jieba==0.42.1
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
jiter==0.10.0
|
jiter==0.10.0
|
||||||
# via openai
|
# via openai
|
||||||
joblib==1.5.1
|
|
||||||
# via scikit-learn
|
|
||||||
json-repair==0.47.6
|
json-repair==0.47.6
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
jsonlines==4.0.0
|
jsonlines==4.0.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
kiwisolver==1.4.8
|
maim-message==0.5.1
|
||||||
# via matplotlib
|
|
||||||
maim-message==0.3.8
|
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
# via rich
|
# via rich
|
||||||
matplotlib==3.10.3
|
|
||||||
# via
|
|
||||||
# -r requirements.txt
|
|
||||||
# seaborn
|
|
||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
# via markdown-it-py
|
# via markdown-it-py
|
||||||
multidict==6.6.3
|
multidict==6.6.3
|
||||||
# via
|
# via
|
||||||
# aiohttp
|
# aiohttp
|
||||||
# yarl
|
# yarl
|
||||||
networkx==3.5
|
networkx==3.4.2
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
numpy==2.3.1
|
numpy==2.2.6
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
# contourpy
|
|
||||||
# faiss-cpu
|
# faiss-cpu
|
||||||
# matplotlib
|
|
||||||
# pandas
|
|
||||||
# scikit-learn
|
|
||||||
# scipy
|
# scipy
|
||||||
# seaborn
|
|
||||||
openai==1.95.0
|
openai==1.95.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
# customtkinter
|
|
||||||
# faiss-cpu
|
# faiss-cpu
|
||||||
# matplotlib
|
|
||||||
# strawberry-graphql
|
# strawberry-graphql
|
||||||
pandas==2.3.1
|
|
||||||
# via
|
|
||||||
# -r requirements.txt
|
|
||||||
# seaborn
|
|
||||||
peewee==3.18.2
|
peewee==3.18.2
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
pillow==11.3.0
|
pillow==11.3.0
|
||||||
# via
|
# via -r requirements.txt
|
||||||
# -r requirements.txt
|
|
||||||
# matplotlib
|
|
||||||
propcache==0.3.2
|
propcache==0.3.2
|
||||||
# via
|
# via
|
||||||
# aiohttp
|
# aiohttp
|
||||||
# yarl
|
# yarl
|
||||||
psutil==7.0.0
|
psutil==7.0.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
pyarrow==20.0.0
|
pyasn1==0.6.1
|
||||||
# via -r requirements.txt
|
# via
|
||||||
|
# pyasn1-modules
|
||||||
|
# rsa
|
||||||
|
pyasn1-modules==0.4.2
|
||||||
|
# via google-auth
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.11.7
|
pydantic==2.11.7
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
# fastapi
|
# fastapi
|
||||||
|
# google-genai
|
||||||
# maim-message
|
# maim-message
|
||||||
# openai
|
# openai
|
||||||
pydantic-core==2.33.2
|
pydantic-core==2.33.2
|
||||||
|
|
@ -169,45 +149,27 @@ pygments==2.19.2
|
||||||
# via rich
|
# via rich
|
||||||
pymongo==4.13.2
|
pymongo==4.13.2
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
pyparsing==3.2.3
|
|
||||||
# via matplotlib
|
|
||||||
pypinyin==0.54.0
|
pypinyin==0.54.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via
|
# via strawberry-graphql
|
||||||
# -r requirements.txt
|
|
||||||
# matplotlib
|
|
||||||
# pandas
|
|
||||||
# strawberry-graphql
|
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
# dotenv
|
# dotenv
|
||||||
python-igraph==0.11.9
|
|
||||||
# via -r requirements.txt
|
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
# via strawberry-graphql
|
# via strawberry-graphql
|
||||||
pytz==2025.2
|
|
||||||
# via pandas
|
|
||||||
quick-algo==0.1.3
|
quick-algo==0.1.3
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
reportportal-client==5.6.5
|
|
||||||
# via -r requirements.txt
|
|
||||||
requests==2.32.4
|
requests==2.32.4
|
||||||
# via
|
# via google-genai
|
||||||
# -r requirements.txt
|
|
||||||
# reportportal-client
|
|
||||||
rich==14.0.0
|
rich==14.0.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
|
rsa==4.9.1
|
||||||
|
# via google-auth
|
||||||
ruff==0.12.2
|
ruff==0.12.2
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
scikit-learn==1.7.0
|
scipy==1.15.3
|
||||||
# via -r requirements.txt
|
|
||||||
scipy==1.16.0
|
|
||||||
# via
|
|
||||||
# -r requirements.txt
|
|
||||||
# scikit-learn
|
|
||||||
seaborn==0.13.2
|
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
setuptools==80.9.0
|
setuptools==80.9.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
|
|
@ -223,10 +185,8 @@ strawberry-graphql==0.275.5
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
structlog==25.4.0
|
structlog==25.4.0
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
texttable==1.7.0
|
tenacity==9.1.2
|
||||||
# via igraph
|
# via google-genai
|
||||||
threadpoolctl==3.6.0
|
|
||||||
# via scikit-learn
|
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
tomli==2.2.1
|
tomli==2.2.1
|
||||||
|
|
@ -236,25 +196,25 @@ tomli-w==1.2.0
|
||||||
tomlkit==0.13.3
|
tomlkit==0.13.3
|
||||||
# via -r requirements.txt
|
# via -r requirements.txt
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
# via
|
# via openai
|
||||||
# -r requirements.txt
|
|
||||||
# openai
|
|
||||||
typing-extensions==4.14.1
|
typing-extensions==4.14.1
|
||||||
# via
|
# via
|
||||||
|
# aiosignal
|
||||||
|
# anyio
|
||||||
|
# exceptiongroup
|
||||||
# fastapi
|
# fastapi
|
||||||
|
# google-genai
|
||||||
|
# multidict
|
||||||
# openai
|
# openai
|
||||||
# pydantic
|
# pydantic
|
||||||
# pydantic-core
|
# pydantic-core
|
||||||
|
# rich
|
||||||
# strawberry-graphql
|
# strawberry-graphql
|
||||||
|
# structlog
|
||||||
# typing-inspection
|
# typing-inspection
|
||||||
|
# uvicorn
|
||||||
typing-inspection==0.4.1
|
typing-inspection==0.4.1
|
||||||
# via pydantic
|
# via pydantic
|
||||||
tzdata==2025.2
|
|
||||||
# via
|
|
||||||
# pandas
|
|
||||||
# tzlocal
|
|
||||||
tzlocal==5.3.1
|
|
||||||
# via apscheduler
|
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
|
|
@ -266,6 +226,7 @@ uvicorn==0.35.0
|
||||||
websockets==15.0.1
|
websockets==15.0.1
|
||||||
# via
|
# via
|
||||||
# -r requirements.txt
|
# -r requirements.txt
|
||||||
|
# google-genai
|
||||||
# maim-message
|
# maim-message
|
||||||
yarl==1.20.1
|
yarl==1.20.1
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,22 @@
|
||||||
APScheduler
|
|
||||||
Pillow
|
Pillow
|
||||||
aiohttp
|
aiohttp
|
||||||
aiohttp-cors
|
aiohttp-cors
|
||||||
colorama
|
colorama
|
||||||
customtkinter
|
|
||||||
dotenv
|
dotenv
|
||||||
faiss-cpu
|
faiss-cpu
|
||||||
fastapi
|
fastapi
|
||||||
jieba
|
jieba
|
||||||
jsonlines
|
jsonlines
|
||||||
maim_message
|
maim-message>=0
|
||||||
quick_algo
|
quick_algo
|
||||||
matplotlib
|
|
||||||
networkx
|
networkx
|
||||||
numpy
|
numpy
|
||||||
openai
|
openai
|
||||||
pandas
|
|
||||||
peewee
|
peewee
|
||||||
pyarrow
|
|
||||||
pydantic
|
pydantic
|
||||||
pypinyin
|
pypinyin
|
||||||
python-dateutil
|
|
||||||
python-dotenv
|
python-dotenv
|
||||||
python-igraph
|
|
||||||
pymongo
|
pymongo
|
||||||
requests
|
|
||||||
ruff
|
ruff
|
||||||
scipy
|
scipy
|
||||||
setuptools
|
setuptools
|
||||||
|
|
@ -32,7 +24,6 @@ toml
|
||||||
tomli
|
tomli
|
||||||
tomli_w
|
tomli_w
|
||||||
tomlkit
|
tomlkit
|
||||||
tqdm
|
|
||||||
urllib3
|
urllib3
|
||||||
uvicorn
|
uvicorn
|
||||||
websockets
|
websockets
|
||||||
|
|
@ -40,10 +31,6 @@ strawberry-graphql[fastapi]
|
||||||
packaging
|
packaging
|
||||||
rich
|
rich
|
||||||
psutil
|
psutil
|
||||||
cryptography
|
|
||||||
json-repair
|
json-repair
|
||||||
reportportal-client
|
|
||||||
scikit-learn
|
|
||||||
seaborn
|
|
||||||
structlog
|
structlog
|
||||||
google.genai
|
google.genai
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ class ExpressionLearner:
|
||||||
model_set=model_config.model_task_config.replyer, request_type="expression.learner"
|
model_set=model_config.model_task_config.replyer, request_type="expression.learner"
|
||||||
)
|
)
|
||||||
self.chat_id = chat_id
|
self.chat_id = chat_id
|
||||||
|
self.chat_stream = get_chat_manager().get_stream(chat_id)
|
||||||
self.chat_name = get_chat_manager().get_stream_name(chat_id) or chat_id
|
self.chat_name = get_chat_manager().get_stream_name(chat_id) or chat_id
|
||||||
|
|
||||||
# 维护每个chat的上次学习时间
|
# 维护每个chat的上次学习时间
|
||||||
|
|
@ -69,24 +70,8 @@ class ExpressionLearner:
|
||||||
|
|
||||||
# 学习参数
|
# 学习参数
|
||||||
self.min_messages_for_learning = 25 # 触发学习所需的最少消息数
|
self.min_messages_for_learning = 25 # 触发学习所需的最少消息数
|
||||||
self.min_learning_interval = 300 # 最短学习时间间隔(秒)
|
_, self.enable_learning, self.learning_intensity = global_config.expression.get_expression_config_for_chat(self.chat_id)
|
||||||
|
self.min_learning_interval = 300 / self.learning_intensity
|
||||||
def can_learn_for_chat(self) -> bool:
|
|
||||||
"""
|
|
||||||
检查指定聊天流是否允许学习表达
|
|
||||||
|
|
||||||
Args:
|
|
||||||
chat_id: 聊天流ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否允许学习
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
use_expression, enable_learning, _ = global_config.expression.get_expression_config_for_chat(self.chat_id)
|
|
||||||
return enable_learning
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"检查学习权限失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def should_trigger_learning(self) -> bool:
|
def should_trigger_learning(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
@ -98,27 +83,13 @@ class ExpressionLearner:
|
||||||
Returns:
|
Returns:
|
||||||
bool: 是否应该触发学习
|
bool: 是否应该触发学习
|
||||||
"""
|
"""
|
||||||
current_time = time.time()
|
|
||||||
|
|
||||||
# 获取该聊天流的学习强度
|
|
||||||
try:
|
|
||||||
_, enable_learning, learning_intensity = global_config.expression.get_expression_config_for_chat(
|
|
||||||
self.chat_id
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取聊天流 {self.chat_id} 的学习配置失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查是否允许学习
|
# 检查是否允许学习
|
||||||
if not enable_learning:
|
if not self.enable_learning:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 根据学习强度计算最短学习时间间隔
|
|
||||||
min_interval = self.min_learning_interval / learning_intensity
|
|
||||||
|
|
||||||
# 检查时间间隔
|
# 检查时间间隔
|
||||||
time_diff = current_time - self.last_learning_time
|
time_diff = time.time() - self.last_learning_time
|
||||||
if time_diff < min_interval:
|
if time_diff < self.min_learning_interval:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查消息数量(只检查指定聊天流的消息)
|
# 检查消息数量(只检查指定聊天流的消息)
|
||||||
|
|
@ -228,32 +199,17 @@ class ExpressionLearner:
|
||||||
"""
|
"""
|
||||||
学习并存储表达方式
|
学习并存储表达方式
|
||||||
"""
|
"""
|
||||||
# 检查是否允许在此聊天流中学习(在函数最前面检查)
|
|
||||||
if not self.can_learn_for_chat():
|
|
||||||
logger.debug(f"聊天流 {self.chat_name} 不允许学习表达,跳过学习")
|
|
||||||
return []
|
|
||||||
|
|
||||||
res = await self.learn_expression(num)
|
res = await self.learn_expression(num)
|
||||||
|
|
||||||
if res is None:
|
if res is None:
|
||||||
|
logger.info("没有学习到表达风格")
|
||||||
return []
|
return []
|
||||||
learnt_expressions, chat_id = res
|
learnt_expressions, chat_id = res
|
||||||
|
|
||||||
chat_stream = get_chat_manager().get_stream(chat_id)
|
|
||||||
if chat_stream is None:
|
|
||||||
group_name = f"聊天流 {chat_id}"
|
|
||||||
elif chat_stream.group_info:
|
|
||||||
group_name = chat_stream.group_info.group_name
|
|
||||||
else:
|
|
||||||
group_name = f"{chat_stream.user_info.user_nickname}的私聊"
|
|
||||||
learnt_expressions_str = ""
|
learnt_expressions_str = ""
|
||||||
for _chat_id, situation, style in learnt_expressions:
|
for _chat_id, situation, style in learnt_expressions:
|
||||||
learnt_expressions_str += f"{situation}->{style}\n"
|
learnt_expressions_str += f"{situation}->{style}\n"
|
||||||
logger.info(f"在 {group_name} 学习到表达风格:\n{learnt_expressions_str}")
|
|
||||||
|
logger.info(f"在 {self.chat_name} 学习到表达风格:\n{learnt_expressions_str}")
|
||||||
if not learnt_expressions:
|
|
||||||
logger.info("没有学习到表达风格")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# 按chat_id分组
|
# 按chat_id分组
|
||||||
chat_dict: Dict[str, List[Dict[str, Any]]] = {}
|
chat_dict: Dict[str, List[Dict[str, Any]]] = {}
|
||||||
|
|
@ -316,7 +272,7 @@ class ExpressionLearner:
|
||||||
|
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
# 获取上次学习时间
|
# 获取上次学习之后的消息
|
||||||
random_msg = get_raw_msg_by_timestamp_with_chat_inclusive(
|
random_msg = get_raw_msg_by_timestamp_with_chat_inclusive(
|
||||||
chat_id=self.chat_id,
|
chat_id=self.chat_id,
|
||||||
timestamp_start=self.last_learning_time,
|
timestamp_start=self.last_learning_time,
|
||||||
|
|
@ -330,14 +286,15 @@ class ExpressionLearner:
|
||||||
chat_id: str = random_msg[0].chat_id
|
chat_id: str = random_msg[0].chat_id
|
||||||
# random_msg_str: str = build_readable_messages(random_msg, timestamp_mode="normal")
|
# random_msg_str: str = build_readable_messages(random_msg, timestamp_mode="normal")
|
||||||
random_msg_str: str = await build_anonymous_messages(random_msg)
|
random_msg_str: str = await build_anonymous_messages(random_msg)
|
||||||
# print(f"random_msg_str:{random_msg_str}")
|
|
||||||
|
|
||||||
prompt: str = await global_prompt_manager.format_prompt(
|
prompt: str = await global_prompt_manager.format_prompt(
|
||||||
prompt,
|
prompt,
|
||||||
chat_str=random_msg_str,
|
chat_str=random_msg_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"学习{type_str}的prompt: {prompt}")
|
print(f"random_msg_str:{random_msg_str}")
|
||||||
|
logger.info(f"学习{type_str}的prompt: {prompt}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3)
|
response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3)
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ class HeartFChatting:
|
||||||
await self._observe(recent_messages_list=recent_messages_list)
|
await self._observe(recent_messages_list=recent_messages_list)
|
||||||
else:
|
else:
|
||||||
# 没有提到,继续保持沉默,等待5秒防止频繁触发
|
# 没有提到,继续保持沉默,等待5秒防止频繁触发
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(10)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
@ -344,6 +344,10 @@ class HeartFChatting:
|
||||||
available_actions=available_actions,
|
available_actions=available_actions,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"{self.log_prefix}决定执行{len(action_to_use_info)}个动作: {' '.join([a.action_type for a in action_to_use_info])}"
|
||||||
|
)
|
||||||
|
|
||||||
# 3. 并行执行所有动作
|
# 3. 并行执行所有动作
|
||||||
action_tasks = [
|
action_tasks = [
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from maim_message import UserInfo, Seg
|
from maim_message import UserInfo, Seg, GroupInfo
|
||||||
|
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.config.config import global_config
|
from src.config.config import global_config
|
||||||
|
|
@ -27,7 +27,7 @@ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..
|
||||||
logger = get_logger("chat")
|
logger = get_logger("chat")
|
||||||
|
|
||||||
|
|
||||||
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
def _check_ban_words(text: str, userinfo: UserInfo, group_info: Optional[GroupInfo] = None) -> bool:
|
||||||
"""检查消息是否包含过滤词
|
"""检查消息是否包含过滤词
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -40,14 +40,14 @@ def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
||||||
"""
|
"""
|
||||||
for word in global_config.message_receive.ban_words:
|
for word in global_config.message_receive.ban_words:
|
||||||
if word in text:
|
if word in text:
|
||||||
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
|
chat_name = group_info.group_name if group_info else "私聊"
|
||||||
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
||||||
logger.info(f"[过滤词识别]消息中含有{word},filtered")
|
logger.info(f"[过滤词识别]消息中含有{word},filtered")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
def _check_ban_regex(text: str, userinfo: UserInfo, group_info: Optional[GroupInfo] = None) -> bool:
|
||||||
"""检查消息是否匹配过滤正则表达式
|
"""检查消息是否匹配过滤正则表达式
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -61,10 +61,10 @@ def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
||||||
# 检查text是否为None或空字符串
|
# 检查text是否为None或空字符串
|
||||||
if text is None or not text:
|
if text is None or not text:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for pattern in global_config.message_receive.ban_msgs_regex:
|
for pattern in global_config.message_receive.ban_msgs_regex:
|
||||||
if re.search(pattern, text):
|
if re.search(pattern, text):
|
||||||
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
|
chat_name = group_info.group_name if group_info else "私聊"
|
||||||
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
||||||
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||||
return True
|
return True
|
||||||
|
|
@ -251,6 +251,21 @@ class ChatBot:
|
||||||
# return
|
# return
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 处理消息内容,生成纯文本
|
||||||
|
await message.process()
|
||||||
|
|
||||||
|
# 过滤检查
|
||||||
|
if _check_ban_words(
|
||||||
|
message.processed_plain_text,
|
||||||
|
user_info, # type: ignore
|
||||||
|
group_info,
|
||||||
|
) or _check_ban_regex(
|
||||||
|
message.raw_message, # type: ignore
|
||||||
|
user_info, # type: ignore
|
||||||
|
group_info,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
get_chat_manager().register_message(message)
|
get_chat_manager().register_message(message)
|
||||||
|
|
||||||
chat = await get_chat_manager().get_or_create_stream(
|
chat = await get_chat_manager().get_or_create_stream(
|
||||||
|
|
@ -261,21 +276,10 @@ class ChatBot:
|
||||||
|
|
||||||
message.update_chat_stream(chat)
|
message.update_chat_stream(chat)
|
||||||
|
|
||||||
# 处理消息内容,生成纯文本
|
|
||||||
await message.process()
|
|
||||||
|
|
||||||
# if await self.check_ban_content(message):
|
# if await self.check_ban_content(message):
|
||||||
# logger.warning(f"检测到消息中含有违法,色情,暴力,反动,敏感内容,消息内容:{message.processed_plain_text},发送者:{message.message_info.user_info.user_nickname}")
|
# logger.warning(f"检测到消息中含有违法,色情,暴力,反动,敏感内容,消息内容:{message.processed_plain_text},发送者:{message.message_info.user_info.user_nickname}")
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# 过滤检查
|
|
||||||
if _check_ban_words(message.processed_plain_text, chat, user_info) or _check_ban_regex( # type: ignore
|
|
||||||
message.raw_message, # type: ignore
|
|
||||||
chat,
|
|
||||||
user_info, # type: ignore
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
# 命令处理 - 使用新插件系统检查并处理命令
|
# 命令处理 - 使用新插件系统检查并处理命令
|
||||||
is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message)
|
is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -499,8 +499,8 @@ class ActionPlanner:
|
||||||
action.action_data = action.action_data or {}
|
action.action_data = action.action_data or {}
|
||||||
action.action_data["loop_start_time"] = loop_start_time
|
action.action_data["loop_start_time"] = loop_start_time
|
||||||
|
|
||||||
logger.info(
|
logger.debug(
|
||||||
f"{self.log_prefix}规划器决定执行{len(actions)}个动作: {' '.join([a.action_type for a in actions])}"
|
f"{self.log_prefix}规划器选择了{len(actions)}个动作: {' '.join([a.action_type for a in actions])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
|
||||||
|
|
@ -623,3 +623,41 @@ def image_path_to_base64(image_path: str) -> str:
|
||||||
return base64.b64encode(image_data).decode("utf-8")
|
return base64.b64encode(image_data).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
raise IOError(f"读取图片文件失败: {image_path}")
|
raise IOError(f"读取图片文件失败: {image_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def base64_to_image(image_base64: str, output_path: str) -> bool:
|
||||||
|
"""将base64编码的图片保存为文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
output_path: 输出文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功保存
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 当base64编码无效时
|
||||||
|
IOError: 当保存文件失败时
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 确保base64字符串只包含ASCII字符
|
||||||
|
if isinstance(image_base64, str):
|
||||||
|
image_base64 = image_base64.encode("ascii", errors="ignore").decode("ascii")
|
||||||
|
|
||||||
|
# 解码base64
|
||||||
|
image_bytes = base64.b64decode(image_base64)
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
output_dir = os.path.dirname(output_path)
|
||||||
|
if output_dir:
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存base64图片失败: {e}")
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
|
||||||
|
|
||||||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||||
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
|
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
|
||||||
MMC_VERSION = "0.10.3"
|
MMC_VERSION = "0.10.4-snapshot.1"
|
||||||
|
|
||||||
|
|
||||||
def get_key_comment(toml_table, key):
|
def get_key_comment(toml_table, key):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
from typing import Callable, AsyncIterator, Optional, Coroutine, Any, List
|
from typing import Callable, AsyncIterator, Optional, Coroutine, Any, List, Dict
|
||||||
|
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai.types import (
|
from google.genai.types import (
|
||||||
|
|
@ -17,6 +17,7 @@ from google.genai.types import (
|
||||||
EmbedContentResponse,
|
EmbedContentResponse,
|
||||||
EmbedContentConfig,
|
EmbedContentConfig,
|
||||||
SafetySetting,
|
SafetySetting,
|
||||||
|
HttpOptions,
|
||||||
HarmCategory,
|
HarmCategory,
|
||||||
HarmBlockThreshold,
|
HarmBlockThreshold,
|
||||||
)
|
)
|
||||||
|
|
@ -345,7 +346,24 @@ class GeminiClient(BaseClient):
|
||||||
|
|
||||||
def __init__(self, api_provider: APIProvider):
|
def __init__(self, api_provider: APIProvider):
|
||||||
super().__init__(api_provider)
|
super().__init__(api_provider)
|
||||||
|
|
||||||
|
# 增加传入参数处理
|
||||||
|
http_options_kwargs: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# 秒转换为毫秒传入
|
||||||
|
if api_provider.timeout is not None:
|
||||||
|
http_options_kwargs["timeout"] = int(api_provider.timeout * 1000)
|
||||||
|
|
||||||
|
# 传入并处理地址和版本(必须为Gemini格式)
|
||||||
|
if api_provider.base_url:
|
||||||
|
parts = api_provider.base_url.rstrip("/").rsplit("/", 1)
|
||||||
|
if len(parts) == 2 and parts[1].startswith("v"):
|
||||||
|
http_options_kwargs["base_url"] = f"{parts[0]}/"
|
||||||
|
http_options_kwargs["api_version"] = parts[1]
|
||||||
|
else:
|
||||||
|
http_options_kwargs["base_url"] = api_provider.base_url
|
||||||
self.client = genai.Client(
|
self.client = genai.Client(
|
||||||
|
http_options=HttpOptions(**http_options_kwargs),
|
||||||
api_key=api_provider.api_key,
|
api_key=api_provider.api_key,
|
||||||
) # 这里和openai不一样,gemini会自己决定自己是否需要retry
|
) # 这里和openai不一样,gemini会自己决定自己是否需要retry
|
||||||
|
|
||||||
|
|
@ -368,20 +386,29 @@ class GeminiClient(BaseClient):
|
||||||
limits = THINKING_BUDGET_LIMITS[key]
|
limits = THINKING_BUDGET_LIMITS[key]
|
||||||
break
|
break
|
||||||
|
|
||||||
# 特殊值处理
|
# 预算值处理
|
||||||
if tb == THINKING_BUDGET_AUTO:
|
if tb == THINKING_BUDGET_AUTO:
|
||||||
return THINKING_BUDGET_AUTO
|
return THINKING_BUDGET_AUTO
|
||||||
if tb == THINKING_BUDGET_DISABLED:
|
if tb == THINKING_BUDGET_DISABLED:
|
||||||
if limits and limits.get("can_disable", False):
|
if limits and limits.get("can_disable", False):
|
||||||
return THINKING_BUDGET_DISABLED
|
return THINKING_BUDGET_DISABLED
|
||||||
return limits["min"] if limits else THINKING_BUDGET_AUTO
|
if limits:
|
||||||
|
logger.warning(f"模型 {model_id} 不支持禁用思考预算,已回退到最小值 {limits['min']}")
|
||||||
|
return limits["min"]
|
||||||
|
return THINKING_BUDGET_AUTO
|
||||||
|
|
||||||
# 已知模型裁剪到范围
|
# 已知模型范围裁剪 + 提示
|
||||||
if limits:
|
if limits:
|
||||||
return max(limits["min"], min(tb, limits["max"]))
|
if tb < limits["min"]:
|
||||||
|
logger.warning(f"模型 {model_id} 的 thinking_budget={tb} 过小,已调整为最小值 {limits['min']}")
|
||||||
|
return limits["min"]
|
||||||
|
if tb > limits["max"]:
|
||||||
|
logger.warning(f"模型 {model_id} 的 thinking_budget={tb} 过大,已调整为最大值 {limits['max']}")
|
||||||
|
return limits["max"]
|
||||||
|
return tb
|
||||||
|
|
||||||
# 未知模型,返回动态模式
|
# 未知模型 → 默认自动模式
|
||||||
logger.warning(f"模型 {model_id} 未在 THINKING_BUDGET_LIMITS 中定义,将使用动态模式 tb=-1 兼容。")
|
logger.warning(f"模型 {model_id} 未在 THINKING_BUDGET_LIMITS 中定义,已启用模型自动预算兼容")
|
||||||
return THINKING_BUDGET_AUTO
|
return THINKING_BUDGET_AUTO
|
||||||
|
|
||||||
async def get_response(
|
async def get_response(
|
||||||
|
|
@ -436,7 +463,9 @@ class GeminiClient(BaseClient):
|
||||||
try:
|
try:
|
||||||
tb = int(extra_params["thinking_budget"])
|
tb = int(extra_params["thinking_budget"])
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logger.warning(f"无效的 thinking_budget 值 {extra_params['thinking_budget']},将使用默认动态模式 {tb}")
|
logger.warning(
|
||||||
|
f"无效的 thinking_budget 值 {extra_params['thinking_budget']},将使用模型自动预算模式 {tb}"
|
||||||
|
)
|
||||||
# 裁剪到模型支持的范围
|
# 裁剪到模型支持的范围
|
||||||
tb = self.clamp_thinking_budget(tb, model_info.model_identifier)
|
tb = self.clamp_thinking_budget(tb, model_info.model_identifier)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,6 @@ install(extra_lines=3)
|
||||||
|
|
||||||
logger = get_logger("model_utils")
|
logger = get_logger("model_utils")
|
||||||
|
|
||||||
# 常见Error Code Mapping
|
|
||||||
error_code_mapping = {
|
|
||||||
400: "参数不正确",
|
|
||||||
401: "API key 错误,认证失败,请检查 config/model_config.toml 中的配置是否正确",
|
|
||||||
402: "账号余额不足",
|
|
||||||
403: "需要实名,或余额不足",
|
|
||||||
404: "Not Found",
|
|
||||||
429: "请求过于频繁,请稍后再试",
|
|
||||||
500: "服务器内部故障",
|
|
||||||
503: "服务器负载过高",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RequestType(Enum):
|
class RequestType(Enum):
|
||||||
"""请求类型枚举"""
|
"""请求类型枚举"""
|
||||||
|
|
@ -267,14 +255,14 @@ class LLMRequest:
|
||||||
extra_params=model_info.extra_params,
|
extra_params=model_info.extra_params,
|
||||||
)
|
)
|
||||||
elif request_type == RequestType.EMBEDDING:
|
elif request_type == RequestType.EMBEDDING:
|
||||||
assert embedding_input is not None
|
assert embedding_input is not None, "嵌入输入不能为空"
|
||||||
return await client.get_embedding(
|
return await client.get_embedding(
|
||||||
model_info=model_info,
|
model_info=model_info,
|
||||||
embedding_input=embedding_input,
|
embedding_input=embedding_input,
|
||||||
extra_params=model_info.extra_params,
|
extra_params=model_info.extra_params,
|
||||||
)
|
)
|
||||||
elif request_type == RequestType.AUDIO:
|
elif request_type == RequestType.AUDIO:
|
||||||
assert audio_base64 is not None
|
assert audio_base64 is not None, "音频Base64不能为空"
|
||||||
return await client.get_audio_transcriptions(
|
return await client.get_audio_transcriptions(
|
||||||
model_info=model_info,
|
model_info=model_info,
|
||||||
audio_base64=audio_base64,
|
audio_base64=audio_base64,
|
||||||
|
|
@ -365,24 +353,23 @@ class LLMRequest:
|
||||||
embedding_input=embedding_input,
|
embedding_input=embedding_input,
|
||||||
audio_base64=audio_base64,
|
audio_base64=audio_base64,
|
||||||
)
|
)
|
||||||
|
total_tokens, penalty, usage_penalty = self.model_usage[model_info.name]
|
||||||
|
if response_usage := response.usage:
|
||||||
|
total_tokens += response_usage.total_tokens
|
||||||
|
self.model_usage[model_info.name] = (total_tokens, penalty, usage_penalty - 1)
|
||||||
return response, model_info
|
return response, model_info
|
||||||
|
|
||||||
except ModelAttemptFailed as e:
|
except ModelAttemptFailed as e:
|
||||||
last_exception = e.original_exception or e
|
last_exception = e.original_exception or e
|
||||||
logger.warning(f"模型 '{model_info.name}' 尝试失败,切换到下一个模型。原因: {e}")
|
logger.warning(f"模型 '{model_info.name}' 尝试失败,切换到下一个模型。原因: {e}")
|
||||||
total_tokens, penalty, usage_penalty = self.model_usage[model_info.name]
|
total_tokens, penalty, usage_penalty = self.model_usage[model_info.name]
|
||||||
self.model_usage[model_info.name] = (total_tokens, penalty + 1, usage_penalty)
|
self.model_usage[model_info.name] = (total_tokens, penalty + 1, usage_penalty - 1)
|
||||||
failed_models_this_request.add(model_info.name)
|
failed_models_this_request.add(model_info.name)
|
||||||
|
|
||||||
if isinstance(last_exception, RespNotOkException) and last_exception.status_code == 400:
|
if isinstance(last_exception, RespNotOkException) and last_exception.status_code == 400:
|
||||||
logger.error("收到不可恢复的客户端错误 (400),中止所有尝试。")
|
logger.error("收到不可恢复的客户端错误 (400),中止所有尝试。")
|
||||||
raise last_exception from e
|
raise last_exception from e
|
||||||
|
|
||||||
finally:
|
|
||||||
total_tokens, penalty, usage_penalty = self.model_usage[model_info.name]
|
|
||||||
if usage_penalty > 0:
|
|
||||||
self.model_usage[model_info.name] = (total_tokens, penalty, usage_penalty - 1)
|
|
||||||
|
|
||||||
logger.error(f"所有 {max_attempts} 个模型均尝试失败。")
|
logger.error(f"所有 {max_attempts} 个模型均尝试失败。")
|
||||||
if last_exception:
|
if last_exception:
|
||||||
raise last_exception
|
raise last_exception
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,15 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
|
||||||
from typing import Optional, Tuple, List
|
from typing import Optional, Tuple, List, Dict, Any
|
||||||
from src.common.logger import get_logger
|
from src.common.logger import get_logger
|
||||||
from src.chat.emoji_system.emoji_manager import get_emoji_manager
|
from src.chat.emoji_system.emoji_manager import get_emoji_manager, EMOJI_DIR
|
||||||
from src.chat.utils.utils_image import image_path_to_base64
|
from src.chat.utils.utils_image import image_path_to_base64, base64_to_image
|
||||||
|
|
||||||
logger = get_logger("emoji_api")
|
logger = get_logger("emoji_api")
|
||||||
|
|
||||||
|
|
@ -245,6 +249,42 @@ def get_emotions() -> List[str]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all() -> List[Tuple[str, str, str]]:
|
||||||
|
"""获取所有表情包
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[str, str, str]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
emoji_manager = get_emoji_manager()
|
||||||
|
all_emojis = emoji_manager.emoji_objects
|
||||||
|
|
||||||
|
if not all_emojis:
|
||||||
|
logger.warning("[EmojiAPI] 没有可用的表情包")
|
||||||
|
return []
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for emoji_obj in all_emojis:
|
||||||
|
if emoji_obj.is_deleted:
|
||||||
|
continue
|
||||||
|
|
||||||
|
emoji_base64 = image_path_to_base64(emoji_obj.full_path)
|
||||||
|
|
||||||
|
if not emoji_base64:
|
||||||
|
logger.error(f"[EmojiAPI] 无法转换表情包为base64: {emoji_obj.full_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
matched_emotion = random.choice(emoji_obj.emotion) if emoji_obj.emotion else "随机表情"
|
||||||
|
results.append((emoji_base64, emoji_obj.description, matched_emotion))
|
||||||
|
|
||||||
|
logger.debug(f"[EmojiAPI] 成功获取 {len(results)} 个表情包")
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[EmojiAPI] 获取所有表情包失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_descriptions() -> List[str]:
|
def get_descriptions() -> List[str]:
|
||||||
"""获取所有表情包描述
|
"""获取所有表情包描述
|
||||||
|
|
||||||
|
|
@ -264,3 +304,400 @@ def get_descriptions() -> List[str]:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}")
|
logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 表情包注册API函数
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def register_emoji(image_base64: str, filename: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""注册新的表情包
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
filename: 可选的文件名,如果未提供则自动生成
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 注册结果,包含以下字段:
|
||||||
|
- success: bool, 是否成功注册
|
||||||
|
- message: str, 结果消息
|
||||||
|
- description: Optional[str], 表情包描述(成功时)
|
||||||
|
- emotions: Optional[List[str]], 情感标签列表(成功时)
|
||||||
|
- replaced: Optional[bool], 是否替换了旧表情包(成功时)
|
||||||
|
- hash: Optional[str], 表情包哈希值(成功时)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 如果base64为空或无效
|
||||||
|
TypeError: 如果参数类型不正确
|
||||||
|
"""
|
||||||
|
if not image_base64:
|
||||||
|
raise ValueError("图片base64编码不能为空")
|
||||||
|
if not isinstance(image_base64, str):
|
||||||
|
raise TypeError("image_base64必须是字符串类型")
|
||||||
|
if filename is not None and not isinstance(filename, str):
|
||||||
|
raise TypeError("filename必须是字符串类型或None")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"[EmojiAPI] 开始注册表情包,文件名: {filename or '自动生成'}")
|
||||||
|
|
||||||
|
# 1. 获取emoji管理器并检查容量
|
||||||
|
emoji_manager = get_emoji_manager()
|
||||||
|
count_before = emoji_manager.emoji_num
|
||||||
|
max_count = emoji_manager.emoji_num_max
|
||||||
|
|
||||||
|
# 2. 检查是否可以注册(未达到上限或启用替换)
|
||||||
|
can_register = count_before < max_count or (
|
||||||
|
count_before >= max_count and emoji_manager.emoji_num_max_reach_deletion
|
||||||
|
)
|
||||||
|
|
||||||
|
if not can_register:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"表情包数量已达上限({count_before}/{max_count})且未启用替换功能",
|
||||||
|
"description": None,
|
||||||
|
"emotions": None,
|
||||||
|
"replaced": None,
|
||||||
|
"hash": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. 确保emoji目录存在
|
||||||
|
os.makedirs(EMOJI_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# 4. 生成文件名
|
||||||
|
if not filename:
|
||||||
|
# 基于时间戳、微秒和短base64生成唯一文件名
|
||||||
|
import time
|
||||||
|
timestamp = int(time.time())
|
||||||
|
microseconds = int(time.time() * 1000000) % 1000000 # 添加微秒级精度
|
||||||
|
|
||||||
|
# 生成12位随机标识符,使用base64编码(增加随机性)
|
||||||
|
import random
|
||||||
|
random_bytes = random.getrandbits(72).to_bytes(9, 'big') # 72位 = 9字节 = 12位base64
|
||||||
|
short_id = base64.b64encode(random_bytes).decode('ascii')[:12].rstrip('=')
|
||||||
|
# 确保base64编码适合文件名(替换/和-)
|
||||||
|
short_id = short_id.replace('/', '_').replace('+', '-')
|
||||||
|
filename = f"emoji_{timestamp}_{microseconds}_{short_id}"
|
||||||
|
|
||||||
|
# 确保文件名有扩展名
|
||||||
|
if not filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||||||
|
filename = f"{filename}.png" # 默认使用png格式
|
||||||
|
|
||||||
|
# 检查文件名是否已存在,如果存在则重新生成短标识符
|
||||||
|
temp_file_path = os.path.join(EMOJI_DIR, filename)
|
||||||
|
attempts = 0
|
||||||
|
max_attempts = 10
|
||||||
|
while os.path.exists(temp_file_path) and attempts < max_attempts:
|
||||||
|
# 重新生成短标识符
|
||||||
|
import random
|
||||||
|
random_bytes = random.getrandbits(48).to_bytes(6, 'big')
|
||||||
|
short_id = base64.b64encode(random_bytes).decode('ascii')[:8].rstrip('=')
|
||||||
|
short_id = short_id.replace('/', '_').replace('+', '-')
|
||||||
|
|
||||||
|
# 分离文件名和扩展名,重新生成文件名
|
||||||
|
name_part, ext = os.path.splitext(filename)
|
||||||
|
# 去掉原来的标识符,添加新的
|
||||||
|
base_name = name_part.rsplit('_', 1)[0] # 移除最后一个_后的部分
|
||||||
|
filename = f"{base_name}_{short_id}{ext}"
|
||||||
|
temp_file_path = os.path.join(EMOJI_DIR, filename)
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
# 如果还是冲突,使用UUID作为备用方案
|
||||||
|
if os.path.exists(temp_file_path):
|
||||||
|
uuid_short = str(uuid.uuid4())[:8]
|
||||||
|
name_part, ext = os.path.splitext(filename)
|
||||||
|
base_name = name_part.rsplit('_', 1)[0]
|
||||||
|
filename = f"{base_name}_{uuid_short}{ext}"
|
||||||
|
temp_file_path = os.path.join(EMOJI_DIR, filename)
|
||||||
|
|
||||||
|
# 如果UUID方案也冲突,添加序号
|
||||||
|
counter = 1
|
||||||
|
original_filename = filename
|
||||||
|
while os.path.exists(temp_file_path):
|
||||||
|
name_part, ext = os.path.splitext(original_filename)
|
||||||
|
filename = f"{name_part}_{counter}{ext}"
|
||||||
|
temp_file_path = os.path.join(EMOJI_DIR, filename)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# 防止无限循环,最多尝试100次
|
||||||
|
if counter > 100:
|
||||||
|
logger.error(f"[EmojiAPI] 无法生成唯一文件名,尝试次数过多: {original_filename}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "无法生成唯一文件名,请稍后重试",
|
||||||
|
"description": None,
|
||||||
|
"emotions": None,
|
||||||
|
"replaced": None,
|
||||||
|
"hash": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. 保存base64图片到emoji目录
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 解码base64并保存图片
|
||||||
|
if not base64_to_image(image_base64, temp_file_path):
|
||||||
|
logger.error(f"[EmojiAPI] 无法保存base64图片到文件: {temp_file_path}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "无法保存图片文件",
|
||||||
|
"description": None,
|
||||||
|
"emotions": None,
|
||||||
|
"replaced": None,
|
||||||
|
"hash": None
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(f"[EmojiAPI] 图片已保存到临时文件: {temp_file_path}")
|
||||||
|
|
||||||
|
except Exception as save_error:
|
||||||
|
logger.error(f"[EmojiAPI] 保存图片文件失败: {save_error}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"保存图片文件失败: {str(save_error)}",
|
||||||
|
"description": None,
|
||||||
|
"emotions": None,
|
||||||
|
"replaced": None,
|
||||||
|
"hash": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 6. 调用注册方法
|
||||||
|
register_success = await emoji_manager.register_emoji_by_filename(filename)
|
||||||
|
|
||||||
|
# 7. 清理临时文件(如果注册失败但文件还存在)
|
||||||
|
if not register_success and os.path.exists(temp_file_path):
|
||||||
|
try:
|
||||||
|
os.remove(temp_file_path)
|
||||||
|
logger.debug(f"[EmojiAPI] 已清理临时文件: {temp_file_path}")
|
||||||
|
except Exception as cleanup_error:
|
||||||
|
logger.warning(f"[EmojiAPI] 清理临时文件失败: {cleanup_error}")
|
||||||
|
|
||||||
|
# 8. 构建返回结果
|
||||||
|
if register_success:
|
||||||
|
count_after = emoji_manager.emoji_num
|
||||||
|
replaced = count_after <= count_before # 如果数量没增加,说明是替换
|
||||||
|
|
||||||
|
# 尝试获取新注册的表情包信息
|
||||||
|
new_emoji_info = None
|
||||||
|
if count_after > count_before or replaced:
|
||||||
|
# 获取最新的表情包信息
|
||||||
|
try:
|
||||||
|
# 通过文件名查找新注册的表情包(注意:文件名在注册后可能已经改变)
|
||||||
|
for emoji_obj in reversed(emoji_manager.emoji_objects):
|
||||||
|
if not emoji_obj.is_deleted and (
|
||||||
|
emoji_obj.filename == filename or # 直接匹配
|
||||||
|
(hasattr(emoji_obj, 'full_path') and filename in emoji_obj.full_path) # 路径包含匹配
|
||||||
|
):
|
||||||
|
new_emoji_info = emoji_obj
|
||||||
|
break
|
||||||
|
except Exception as find_error:
|
||||||
|
logger.warning(f"[EmojiAPI] 查找新注册表情包信息失败: {find_error}")
|
||||||
|
|
||||||
|
description = new_emoji_info.description if new_emoji_info else None
|
||||||
|
emotions = new_emoji_info.emotion if new_emoji_info else None
|
||||||
|
emoji_hash = new_emoji_info.hash if new_emoji_info else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"表情包注册成功 {'(替换旧表情包)' if replaced else '(新增表情包)'}",
|
||||||
|
"description": description,
|
||||||
|
"emotions": emotions,
|
||||||
|
"replaced": replaced,
|
||||||
|
"hash": emoji_hash
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "表情包注册失败,可能因为重复、格式不支持或审核未通过",
|
||||||
|
"description": None,
|
||||||
|
"emotions": None,
|
||||||
|
"replaced": None,
|
||||||
|
"hash": None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[EmojiAPI] 注册表情包时发生异常: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"注册过程中发生错误: {str(e)}",
|
||||||
|
"description": None,
|
||||||
|
"emotions": None,
|
||||||
|
"replaced": None,
|
||||||
|
"hash": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 表情包删除API函数
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_emoji(emoji_hash: str) -> Dict[str, Any]:
|
||||||
|
"""删除表情包
|
||||||
|
|
||||||
|
Args:
|
||||||
|
emoji_hash: 要删除的表情包的哈希值
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 删除结果,包含以下字段:
|
||||||
|
- success: bool, 是否成功删除
|
||||||
|
- message: str, 结果消息
|
||||||
|
- count_before: Optional[int], 删除前的表情包数量
|
||||||
|
- count_after: Optional[int], 删除后的表情包数量
|
||||||
|
- description: Optional[str], 被删除的表情包描述(成功时)
|
||||||
|
- emotions: Optional[List[str]], 被删除的表情包情感标签(成功时)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 如果哈希值为空
|
||||||
|
TypeError: 如果哈希值不是字符串类型
|
||||||
|
"""
|
||||||
|
if not emoji_hash:
|
||||||
|
raise ValueError("表情包哈希值不能为空")
|
||||||
|
if not isinstance(emoji_hash, str):
|
||||||
|
raise TypeError("emoji_hash必须是字符串类型")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"[EmojiAPI] 开始删除表情包,哈希值: {emoji_hash}")
|
||||||
|
|
||||||
|
# 1. 获取emoji管理器和删除前的数量
|
||||||
|
emoji_manager = get_emoji_manager()
|
||||||
|
count_before = emoji_manager.emoji_num
|
||||||
|
|
||||||
|
# 2. 获取被删除表情包的信息(用于返回结果)
|
||||||
|
try:
|
||||||
|
deleted_emoji = await emoji_manager.get_emoji_from_manager(emoji_hash)
|
||||||
|
description = deleted_emoji.description if deleted_emoji else None
|
||||||
|
emotions = deleted_emoji.emotion if deleted_emoji else None
|
||||||
|
except Exception as info_error:
|
||||||
|
logger.warning(f"[EmojiAPI] 获取被删除表情包信息失败: {info_error}")
|
||||||
|
description = None
|
||||||
|
emotions = None
|
||||||
|
|
||||||
|
# 3. 执行删除操作
|
||||||
|
delete_success = await emoji_manager.delete_emoji(emoji_hash)
|
||||||
|
|
||||||
|
# 4. 获取删除后的数量
|
||||||
|
count_after = emoji_manager.emoji_num
|
||||||
|
|
||||||
|
# 5. 构建返回结果
|
||||||
|
if delete_success:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"表情包删除成功 (哈希: {emoji_hash[:8]}...)",
|
||||||
|
"count_before": count_before,
|
||||||
|
"count_after": count_after,
|
||||||
|
"description": description,
|
||||||
|
"emotions": emotions
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"表情包删除失败,可能因为哈希值不存在或删除过程出错",
|
||||||
|
"count_before": count_before,
|
||||||
|
"count_after": count_after,
|
||||||
|
"description": None,
|
||||||
|
"emotions": None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[EmojiAPI] 删除表情包时发生异常: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"删除过程中发生错误: {str(e)}",
|
||||||
|
"count_before": None,
|
||||||
|
"count_after": None,
|
||||||
|
"description": None,
|
||||||
|
"emotions": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_emoji_by_description(description: str, exact_match: bool = False) -> Dict[str, Any]:
|
||||||
|
"""根据描述删除表情包
|
||||||
|
|
||||||
|
Args:
|
||||||
|
description: 表情包描述文本
|
||||||
|
exact_match: 是否精确匹配描述,False则为模糊匹配
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 删除结果,包含以下字段:
|
||||||
|
- success: bool, 是否成功删除
|
||||||
|
- message: str, 结果消息
|
||||||
|
- deleted_count: int, 删除的表情包数量
|
||||||
|
- deleted_hashes: List[str], 被删除的表情包哈希列表
|
||||||
|
- matched_count: int, 匹配到的表情包数量
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 如果描述为空
|
||||||
|
TypeError: 如果描述不是字符串类型
|
||||||
|
"""
|
||||||
|
if not description:
|
||||||
|
raise ValueError("描述不能为空")
|
||||||
|
if not isinstance(description, str):
|
||||||
|
raise TypeError("description必须是字符串类型")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"[EmojiAPI] 根据描述删除表情包: {description} (精确匹配: {exact_match})")
|
||||||
|
|
||||||
|
emoji_manager = get_emoji_manager()
|
||||||
|
all_emojis = emoji_manager.emoji_objects
|
||||||
|
|
||||||
|
# 筛选匹配的表情包
|
||||||
|
matching_emojis = []
|
||||||
|
for emoji_obj in all_emojis:
|
||||||
|
if emoji_obj.is_deleted:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if exact_match:
|
||||||
|
if emoji_obj.description == description:
|
||||||
|
matching_emojis.append(emoji_obj)
|
||||||
|
else:
|
||||||
|
if description.lower() in emoji_obj.description.lower():
|
||||||
|
matching_emojis.append(emoji_obj)
|
||||||
|
|
||||||
|
matched_count = len(matching_emojis)
|
||||||
|
if matched_count == 0:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"未找到匹配描述 '{description}' 的表情包",
|
||||||
|
"deleted_count": 0,
|
||||||
|
"deleted_hashes": [],
|
||||||
|
"matched_count": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 删除匹配的表情包
|
||||||
|
deleted_count = 0
|
||||||
|
deleted_hashes = []
|
||||||
|
for emoji_obj in matching_emojis:
|
||||||
|
try:
|
||||||
|
delete_success = await emoji_manager.delete_emoji(emoji_obj.hash)
|
||||||
|
if delete_success:
|
||||||
|
deleted_count += 1
|
||||||
|
deleted_hashes.append(emoji_obj.hash)
|
||||||
|
except Exception as delete_error:
|
||||||
|
logger.error(f"[EmojiAPI] 删除表情包失败 (哈希: {emoji_obj.hash}): {delete_error}")
|
||||||
|
|
||||||
|
# 构建返回结果
|
||||||
|
if deleted_count > 0:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"成功删除 {deleted_count} 个表情包 (匹配到 {matched_count} 个)",
|
||||||
|
"deleted_count": deleted_count,
|
||||||
|
"deleted_hashes": deleted_hashes,
|
||||||
|
"matched_count": matched_count
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"匹配到 {matched_count} 个表情包,但删除全部失败",
|
||||||
|
"deleted_count": 0,
|
||||||
|
"deleted_hashes": [],
|
||||||
|
"matched_count": matched_count
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[EmojiAPI] 根据描述删除表情包时发生异常: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"删除过程中发生错误: {str(e)}",
|
||||||
|
"deleted_count": 0,
|
||||||
|
"deleted_hashes": [],
|
||||||
|
"matched_count": 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,6 @@ class EmojiAction(BaseAction):
|
||||||
action_name = "emoji"
|
action_name = "emoji"
|
||||||
action_description = "发送表情包辅助表达情绪"
|
action_description = "发送表情包辅助表达情绪"
|
||||||
|
|
||||||
# LLM判断提示词
|
|
||||||
llm_judge_prompt = """
|
|
||||||
判定是否需要使用表情动作的条件:
|
|
||||||
1. 用户明确要求使用表情包
|
|
||||||
2. 这是一个适合表达强烈情绪的场合
|
|
||||||
3. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否"
|
|
||||||
|
|
||||||
请回答"是"或"否"。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 动作参数定义
|
# 动作参数定义
|
||||||
action_parameters = {}
|
action_parameters = {}
|
||||||
|
|
||||||
|
|
@ -56,8 +46,9 @@ class EmojiAction(BaseAction):
|
||||||
"""执行表情动作"""
|
"""执行表情动作"""
|
||||||
try:
|
try:
|
||||||
# 1. 获取发送表情的原因
|
# 1. 获取发送表情的原因
|
||||||
reason = self.action_data.get("reason", "表达当前情绪")
|
# reason = self.action_data.get("reason", "表达当前情绪")
|
||||||
|
reason = self.reasoning
|
||||||
|
|
||||||
# 2. 随机获取20个表情包
|
# 2. 随机获取20个表情包
|
||||||
sampled_emojis = await emoji_api.get_random(30)
|
sampled_emojis = await emoji_api.get_random(30)
|
||||||
if not sampled_emojis:
|
if not sampled_emojis:
|
||||||
|
|
@ -72,6 +63,9 @@ class EmojiAction(BaseAction):
|
||||||
emotion_map[emo].append((b64, desc))
|
emotion_map[emo].append((b64, desc))
|
||||||
|
|
||||||
available_emotions = list(emotion_map.keys())
|
available_emotions = list(emotion_map.keys())
|
||||||
|
available_emotions_str = ""
|
||||||
|
for emotion in available_emotions:
|
||||||
|
available_emotions_str += f"{emotion}\n"
|
||||||
|
|
||||||
if not available_emotions:
|
if not available_emotions:
|
||||||
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
|
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
|
||||||
|
|
@ -90,14 +84,15 @@ class EmojiAction(BaseAction):
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. 构建prompt让LLM选择情感
|
# 4. 构建prompt让LLM选择情感
|
||||||
prompt = f"""
|
prompt = f"""你正在进行QQ聊天,你需要根据聊天记录,选出一个合适的情感标签。
|
||||||
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。
|
请你根据以下原因和聊天记录进行选择
|
||||||
这是最近的聊天记录:
|
原因:{reason}
|
||||||
{messages_text}
|
聊天记录:
|
||||||
|
{messages_text}
|
||||||
这是理由:“{reason}”
|
|
||||||
这里是可用的情感标签:{available_emotions}
|
这里是可用的情感标签:
|
||||||
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
|
{available_emotions_str}
|
||||||
|
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if global_config.debug.show_prompt:
|
if global_config.debug.show_prompt:
|
||||||
|
|
@ -107,10 +102,10 @@ class EmojiAction(BaseAction):
|
||||||
|
|
||||||
# 5. 调用LLM
|
# 5. 调用LLM
|
||||||
models = llm_api.get_available_models()
|
models = llm_api.get_available_models()
|
||||||
chat_model_config = models.get("utils_small") # 使用字典访问方式
|
chat_model_config = models.get("replyer") # 使用字典访问方式
|
||||||
if not chat_model_config:
|
if not chat_model_config:
|
||||||
logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM")
|
logger.error(f"{self.log_prefix} 未找到'replyer'模型配置,无法调用LLM")
|
||||||
return False, "未找到'utils_small'模型配置"
|
return False, "未找到'replyer'模型配置"
|
||||||
|
|
||||||
success, chosen_emotion, _, _ = await llm_api.generate_with_model(
|
success, chosen_emotion, _, _ = await llm_api.generate_with_model(
|
||||||
prompt, model_config=chat_model_config, request_type="emoji"
|
prompt, model_config=chat_model_config, request_type="emoji"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[inner]
|
[inner]
|
||||||
version = "1.7.0"
|
version = "1.7.2"
|
||||||
|
|
||||||
# 配置文件版本号迭代规则同bot_config.toml
|
# 配置文件版本号迭代规则同bot_config.toml
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ retry_interval = 5
|
||||||
|
|
||||||
[[api_providers]] # 特殊:Google的Gimini使用特殊API,与OpenAI格式不兼容,需要配置client为"gemini"
|
[[api_providers]] # 特殊:Google的Gimini使用特殊API,与OpenAI格式不兼容,需要配置client为"gemini"
|
||||||
name = "Google"
|
name = "Google"
|
||||||
base_url = "https://api.google.com/v1"
|
base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||||
api_key = "your-google-api-key-1"
|
api_key = "your-google-api-key-1"
|
||||||
client_type = "gemini"
|
client_type = "gemini"
|
||||||
max_retry = 2
|
max_retry = 2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue