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/hello_world_plugin
|
||||
!/plugins/emoji_manage_plugin
|
||||
!/plugins/take_picture_plugin
|
||||
|
||||
config.toml
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
0.10.4饼 表达方式优化
|
||||
无了
|
||||
## [0.10.4] - 2025-9-22
|
||||
表达方式优化
|
||||
|
||||
|
||||
## [0.10.3] - 2025-9-22
|
||||
### 🌟 主要功能更改
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ version = "1.1.1"
|
|||
```toml
|
||||
[[api_providers]]
|
||||
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密钥
|
||||
client_type = "openai" # 客户端类型
|
||||
max_retry = 2 # 最大重试次数
|
||||
|
|
@ -43,19 +43,19 @@ retry_interval = 10 # 重试间隔(秒)
|
|||
| `name` | ✅ | 服务商名称,需要在模型配置中引用 | - |
|
||||
| `base_url` | ✅ | API服务的基础URL | - |
|
||||
| `api_key` | ✅ | API密钥,请替换为实际密钥 | - |
|
||||
| `client_type` | ❌ | 客户端类型:`openai`(OpenAI格式)或 `gemini`(Gemini格式,现在支持不良好) | `openai` |
|
||||
| `client_type` | ❌ | 客户端类型:`openai`(OpenAI格式)或 `gemini`(Gemini格式) | `openai` |
|
||||
| `max_retry` | ❌ | API调用失败时的最大重试次数 | 2 |
|
||||
| `timeout` | ❌ | API请求超时时间(秒) | 30 |
|
||||
| `retry_interval` | ❌ | 重试间隔时间(秒) | 10 |
|
||||
|
||||
**请注意,对于`client_type`为`gemini`的模型,`base_url`字段无效。**
|
||||
**请注意,对于`client_type`为`gemini`的模型,`retry`字段由`gemini`自己决定。**
|
||||
### 2.3 支持的服务商示例
|
||||
|
||||
#### DeepSeek
|
||||
```toml
|
||||
[[api_providers]]
|
||||
name = "DeepSeek"
|
||||
base_url = "https://api.deepseek.cn/v1"
|
||||
base_url = "https://api.deepseek.com/v1"
|
||||
api_key = "your-deepseek-api-key"
|
||||
client_type = "openai"
|
||||
```
|
||||
|
|
@ -73,7 +73,7 @@ client_type = "openai"
|
|||
```toml
|
||||
[[api_providers]]
|
||||
name = "Google"
|
||||
base_url = "https://api.google.com/v1"
|
||||
base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||
api_key = "your-google-api-key"
|
||||
client_type = "gemini" # 注意:Gemini需要使用特殊客户端
|
||||
```
|
||||
|
|
@ -131,9 +131,20 @@ enable_thinking = false # 禁用思考
|
|||
[models.extra_params]
|
||||
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服务商的要求。
|
||||
|
||||
**请注意,对于`client_type`为`gemini`的模型,此字段无效。**
|
||||
### 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定义
|
||||
config_schema: dict = {
|
||||
"plugin": {
|
||||
"name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"),
|
||||
"version": ConfigField(type=str, default="1.0.0", description="插件版本"),
|
||||
"config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"),
|
||||
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
|
||||
},
|
||||
"greeting": {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ dependencies = [
|
|||
"jieba>=0.42.1",
|
||||
"json-repair>=0.47.6",
|
||||
"jsonlines>=4.0.0",
|
||||
"maim-message>=0.3.8",
|
||||
"maim-message>=0.5.1",
|
||||
"matplotlib>=3.10.3",
|
||||
"networkx>=3.4.2",
|
||||
"numpy>=2.2.6",
|
||||
|
|
|
|||
|
|
@ -1,34 +1,36 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.txt -o requirements.lock
|
||||
aenum==3.1.16
|
||||
# via reportportal-client
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.12.14
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# aiohttp-cors
|
||||
# maim-message
|
||||
# reportportal-client
|
||||
aiohttp-cors==0.8.1
|
||||
# via -r requirements.txt
|
||||
aiosignal==1.4.0
|
||||
# via aiohttp
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.9.0
|
||||
# via
|
||||
# google-genai
|
||||
# httpx
|
||||
# openai
|
||||
# starlette
|
||||
apscheduler==3.11.0
|
||||
# via -r requirements.txt
|
||||
async-timeout==5.0.1
|
||||
# via aiohttp
|
||||
attrs==25.3.0
|
||||
# via
|
||||
# aiohttp
|
||||
# jsonlines
|
||||
cachetools==5.5.2
|
||||
# via google-auth
|
||||
certifi==2025.7.9
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# reportportal-client
|
||||
# requests
|
||||
cffi==1.17.1
|
||||
# via cryptography
|
||||
|
|
@ -41,24 +43,16 @@ colorama==0.4.6
|
|||
# -r requirements.txt
|
||||
# click
|
||||
# tqdm
|
||||
contourpy==1.3.2
|
||||
# via matplotlib
|
||||
cryptography==45.0.5
|
||||
# via
|
||||
# -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
|
||||
# via maim-message
|
||||
distro==1.9.0
|
||||
# via openai
|
||||
dnspython==2.7.0
|
||||
# via pymongo
|
||||
dotenv==0.9.9
|
||||
# via -r requirements.txt
|
||||
exceptiongroup==1.3.0
|
||||
# via anyio
|
||||
faiss-cpu==1.11.0
|
||||
# via -r requirements.txt
|
||||
fastapi==0.116.0
|
||||
|
|
@ -66,12 +60,14 @@ fastapi==0.116.0
|
|||
# -r requirements.txt
|
||||
# maim-message
|
||||
# strawberry-graphql
|
||||
fonttools==4.58.5
|
||||
# via matplotlib
|
||||
frozenlist==1.7.0
|
||||
# via
|
||||
# aiohttp
|
||||
# aiosignal
|
||||
google-auth==2.40.3
|
||||
# via google-genai
|
||||
google-genai==1.38.0
|
||||
# via -r requirements.txt
|
||||
graphql-core==3.2.6
|
||||
# via strawberry-graphql
|
||||
h11==0.16.0
|
||||
|
|
@ -81,86 +77,70 @@ h11==0.16.0
|
|||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via openai
|
||||
# via
|
||||
# google-genai
|
||||
# openai
|
||||
idna==3.10
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
# yarl
|
||||
igraph==0.11.9
|
||||
# via python-igraph
|
||||
jieba==0.42.1
|
||||
# via -r requirements.txt
|
||||
jiter==0.10.0
|
||||
# via openai
|
||||
joblib==1.5.1
|
||||
# via scikit-learn
|
||||
json-repair==0.47.6
|
||||
# via -r requirements.txt
|
||||
jsonlines==4.0.0
|
||||
# via -r requirements.txt
|
||||
kiwisolver==1.4.8
|
||||
# via matplotlib
|
||||
maim-message==0.3.8
|
||||
maim-message==0.5.1
|
||||
# via -r requirements.txt
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
matplotlib==3.10.3
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# seaborn
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.6.3
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
networkx==3.5
|
||||
networkx==3.4.2
|
||||
# via -r requirements.txt
|
||||
numpy==2.3.1
|
||||
numpy==2.2.6
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# contourpy
|
||||
# faiss-cpu
|
||||
# matplotlib
|
||||
# pandas
|
||||
# scikit-learn
|
||||
# scipy
|
||||
# seaborn
|
||||
openai==1.95.0
|
||||
# via -r requirements.txt
|
||||
packaging==25.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# customtkinter
|
||||
# faiss-cpu
|
||||
# matplotlib
|
||||
# strawberry-graphql
|
||||
pandas==2.3.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# seaborn
|
||||
peewee==3.18.2
|
||||
# via -r requirements.txt
|
||||
pillow==11.3.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# matplotlib
|
||||
# via -r requirements.txt
|
||||
propcache==0.3.2
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
psutil==7.0.0
|
||||
# via -r requirements.txt
|
||||
pyarrow==20.0.0
|
||||
# via -r requirements.txt
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# rsa
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.11.7
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# fastapi
|
||||
# google-genai
|
||||
# maim-message
|
||||
# openai
|
||||
pydantic-core==2.33.2
|
||||
|
|
@ -169,45 +149,27 @@ pygments==2.19.2
|
|||
# via rich
|
||||
pymongo==4.13.2
|
||||
# via -r requirements.txt
|
||||
pyparsing==3.2.3
|
||||
# via matplotlib
|
||||
pypinyin==0.54.0
|
||||
# via -r requirements.txt
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# matplotlib
|
||||
# pandas
|
||||
# strawberry-graphql
|
||||
# via strawberry-graphql
|
||||
python-dotenv==1.1.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# dotenv
|
||||
python-igraph==0.11.9
|
||||
# via -r requirements.txt
|
||||
python-multipart==0.0.20
|
||||
# via strawberry-graphql
|
||||
pytz==2025.2
|
||||
# via pandas
|
||||
quick-algo==0.1.3
|
||||
# via -r requirements.txt
|
||||
reportportal-client==5.6.5
|
||||
# via -r requirements.txt
|
||||
requests==2.32.4
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# reportportal-client
|
||||
# via google-genai
|
||||
rich==14.0.0
|
||||
# via -r requirements.txt
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
ruff==0.12.2
|
||||
# via -r requirements.txt
|
||||
scikit-learn==1.7.0
|
||||
# via -r requirements.txt
|
||||
scipy==1.16.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# scikit-learn
|
||||
seaborn==0.13.2
|
||||
scipy==1.15.3
|
||||
# via -r requirements.txt
|
||||
setuptools==80.9.0
|
||||
# via -r requirements.txt
|
||||
|
|
@ -223,10 +185,8 @@ strawberry-graphql==0.275.5
|
|||
# via -r requirements.txt
|
||||
structlog==25.4.0
|
||||
# via -r requirements.txt
|
||||
texttable==1.7.0
|
||||
# via igraph
|
||||
threadpoolctl==3.6.0
|
||||
# via scikit-learn
|
||||
tenacity==9.1.2
|
||||
# via google-genai
|
||||
toml==0.10.2
|
||||
# via -r requirements.txt
|
||||
tomli==2.2.1
|
||||
|
|
@ -236,25 +196,25 @@ tomli-w==1.2.0
|
|||
tomlkit==0.13.3
|
||||
# via -r requirements.txt
|
||||
tqdm==4.67.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# openai
|
||||
# via openai
|
||||
typing-extensions==4.14.1
|
||||
# via
|
||||
# aiosignal
|
||||
# anyio
|
||||
# exceptiongroup
|
||||
# fastapi
|
||||
# google-genai
|
||||
# multidict
|
||||
# openai
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# rich
|
||||
# strawberry-graphql
|
||||
# structlog
|
||||
# typing-inspection
|
||||
# uvicorn
|
||||
typing-inspection==0.4.1
|
||||
# via pydantic
|
||||
tzdata==2025.2
|
||||
# via
|
||||
# pandas
|
||||
# tzlocal
|
||||
tzlocal==5.3.1
|
||||
# via apscheduler
|
||||
urllib3==2.5.0
|
||||
# via
|
||||
# -r requirements.txt
|
||||
|
|
@ -266,6 +226,7 @@ uvicorn==0.35.0
|
|||
websockets==15.0.1
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# google-genai
|
||||
# maim-message
|
||||
yarl==1.20.1
|
||||
# via aiohttp
|
||||
|
|
|
|||
|
|
@ -1,30 +1,22 @@
|
|||
APScheduler
|
||||
Pillow
|
||||
aiohttp
|
||||
aiohttp-cors
|
||||
colorama
|
||||
customtkinter
|
||||
dotenv
|
||||
faiss-cpu
|
||||
fastapi
|
||||
jieba
|
||||
jsonlines
|
||||
maim_message
|
||||
maim-message>=0
|
||||
quick_algo
|
||||
matplotlib
|
||||
networkx
|
||||
numpy
|
||||
openai
|
||||
pandas
|
||||
peewee
|
||||
pyarrow
|
||||
pydantic
|
||||
pypinyin
|
||||
python-dateutil
|
||||
python-dotenv
|
||||
python-igraph
|
||||
pymongo
|
||||
requests
|
||||
ruff
|
||||
scipy
|
||||
setuptools
|
||||
|
|
@ -32,7 +24,6 @@ toml
|
|||
tomli
|
||||
tomli_w
|
||||
tomlkit
|
||||
tqdm
|
||||
urllib3
|
||||
uvicorn
|
||||
websockets
|
||||
|
|
@ -40,10 +31,6 @@ strawberry-graphql[fastapi]
|
|||
packaging
|
||||
rich
|
||||
psutil
|
||||
cryptography
|
||||
json-repair
|
||||
reportportal-client
|
||||
scikit-learn
|
||||
seaborn
|
||||
structlog
|
||||
google.genai
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class ExpressionLearner:
|
|||
model_set=model_config.model_task_config.replyer, request_type="expression.learner"
|
||||
)
|
||||
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
|
||||
|
||||
# 维护每个chat的上次学习时间
|
||||
|
|
@ -69,24 +70,8 @@ class ExpressionLearner:
|
|||
|
||||
# 学习参数
|
||||
self.min_messages_for_learning = 25 # 触发学习所需的最少消息数
|
||||
self.min_learning_interval = 300 # 最短学习时间间隔(秒)
|
||||
|
||||
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
|
||||
_, 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 should_trigger_learning(self) -> bool:
|
||||
"""
|
||||
|
|
@ -98,27 +83,13 @@ class ExpressionLearner:
|
|||
Returns:
|
||||
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
|
||||
|
||||
# 根据学习强度计算最短学习时间间隔
|
||||
min_interval = self.min_learning_interval / learning_intensity
|
||||
|
||||
# 检查时间间隔
|
||||
time_diff = current_time - self.last_learning_time
|
||||
if time_diff < min_interval:
|
||||
time_diff = time.time() - self.last_learning_time
|
||||
if time_diff < self.min_learning_interval:
|
||||
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)
|
||||
|
||||
if res is None:
|
||||
logger.info("没有学习到表达风格")
|
||||
return []
|
||||
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 = ""
|
||||
for _chat_id, situation, style in learnt_expressions:
|
||||
learnt_expressions_str += f"{situation}->{style}\n"
|
||||
logger.info(f"在 {group_name} 学习到表达风格:\n{learnt_expressions_str}")
|
||||
|
||||
if not learnt_expressions:
|
||||
logger.info("没有学习到表达风格")
|
||||
return []
|
||||
|
||||
logger.info(f"在 {self.chat_name} 学习到表达风格:\n{learnt_expressions_str}")
|
||||
|
||||
# 按chat_id分组
|
||||
chat_dict: Dict[str, List[Dict[str, Any]]] = {}
|
||||
|
|
@ -316,7 +272,7 @@ class ExpressionLearner:
|
|||
|
||||
current_time = time.time()
|
||||
|
||||
# 获取上次学习时间
|
||||
# 获取上次学习之后的消息
|
||||
random_msg = get_raw_msg_by_timestamp_with_chat_inclusive(
|
||||
chat_id=self.chat_id,
|
||||
timestamp_start=self.last_learning_time,
|
||||
|
|
@ -330,14 +286,15 @@ class ExpressionLearner:
|
|||
chat_id: str = random_msg[0].chat_id
|
||||
# random_msg_str: str = build_readable_messages(random_msg, timestamp_mode="normal")
|
||||
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,
|
||||
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:
|
||||
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)
|
||||
else:
|
||||
# 没有提到,继续保持沉默,等待5秒防止频繁触发
|
||||
await asyncio.sleep(5)
|
||||
await asyncio.sleep(10)
|
||||
return True
|
||||
else:
|
||||
await asyncio.sleep(0.2)
|
||||
|
|
@ -344,6 +344,10 @@ class HeartFChatting:
|
|||
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. 并行执行所有动作
|
||||
action_tasks = [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import os
|
|||
import re
|
||||
|
||||
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.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")
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -40,14 +40,14 @@ def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
|||
"""
|
||||
for word in global_config.message_receive.ban_words:
|
||||
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"[过滤词识别]消息中含有{word},filtered")
|
||||
return True
|
||||
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:
|
||||
|
|
@ -61,10 +61,10 @@ def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
|
|||
# 检查text是否为None或空字符串
|
||||
if text is None or not text:
|
||||
return False
|
||||
|
||||
|
||||
for pattern in global_config.message_receive.ban_msgs_regex:
|
||||
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"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||
return True
|
||||
|
|
@ -251,6 +251,21 @@ class ChatBot:
|
|||
# return
|
||||
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)
|
||||
|
||||
chat = await get_chat_manager().get_or_create_stream(
|
||||
|
|
@ -261,21 +276,10 @@ class ChatBot:
|
|||
|
||||
message.update_chat_stream(chat)
|
||||
|
||||
# 处理消息内容,生成纯文本
|
||||
await message.process()
|
||||
|
||||
# if await self.check_ban_content(message):
|
||||
# logger.warning(f"检测到消息中含有违法,色情,暴力,反动,敏感内容,消息内容:{message.processed_plain_text},发送者:{message.message_info.user_info.user_nickname}")
|
||||
# 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -499,8 +499,8 @@ class ActionPlanner:
|
|||
action.action_data = action.action_data or {}
|
||||
action.action_data["loop_start_time"] = loop_start_time
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix}规划器决定执行{len(actions)}个动作: {' '.join([a.action_type for a in actions])}"
|
||||
logger.debug(
|
||||
f"{self.log_prefix}规划器选择了{len(actions)}个动作: {' '.join([a.action_type for a in actions])}"
|
||||
)
|
||||
|
||||
return actions
|
||||
|
|
|
|||
|
|
@ -623,3 +623,41 @@ def image_path_to_base64(image_path: str) -> str:
|
|||
return base64.b64encode(image_data).decode("utf-8")
|
||||
else:
|
||||
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是不会自动更新的,所以采用硬编码
|
||||
# 对该字段的更新,请严格参照语义化版本规范: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):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import asyncio
|
||||
import io
|
||||
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.genai.types import (
|
||||
|
|
@ -17,6 +17,7 @@ from google.genai.types import (
|
|||
EmbedContentResponse,
|
||||
EmbedContentConfig,
|
||||
SafetySetting,
|
||||
HttpOptions,
|
||||
HarmCategory,
|
||||
HarmBlockThreshold,
|
||||
)
|
||||
|
|
@ -345,7 +346,24 @@ class GeminiClient(BaseClient):
|
|||
|
||||
def __init__(self, api_provider: APIProvider):
|
||||
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(
|
||||
http_options=HttpOptions(**http_options_kwargs),
|
||||
api_key=api_provider.api_key,
|
||||
) # 这里和openai不一样,gemini会自己决定自己是否需要retry
|
||||
|
||||
|
|
@ -368,20 +386,29 @@ class GeminiClient(BaseClient):
|
|||
limits = THINKING_BUDGET_LIMITS[key]
|
||||
break
|
||||
|
||||
# 特殊值处理
|
||||
# 预算值处理
|
||||
if tb == THINKING_BUDGET_AUTO:
|
||||
return THINKING_BUDGET_AUTO
|
||||
if tb == THINKING_BUDGET_DISABLED:
|
||||
if limits and limits.get("can_disable", False):
|
||||
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:
|
||||
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
|
||||
|
||||
async def get_response(
|
||||
|
|
@ -436,7 +463,9 @@ class GeminiClient(BaseClient):
|
|||
try:
|
||||
tb = int(extra_params["thinking_budget"])
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,18 +26,6 @@ install(extra_lines=3)
|
|||
|
||||
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):
|
||||
"""请求类型枚举"""
|
||||
|
|
@ -267,14 +255,14 @@ class LLMRequest:
|
|||
extra_params=model_info.extra_params,
|
||||
)
|
||||
elif request_type == RequestType.EMBEDDING:
|
||||
assert embedding_input is not None
|
||||
assert embedding_input is not None, "嵌入输入不能为空"
|
||||
return await client.get_embedding(
|
||||
model_info=model_info,
|
||||
embedding_input=embedding_input,
|
||||
extra_params=model_info.extra_params,
|
||||
)
|
||||
elif request_type == RequestType.AUDIO:
|
||||
assert audio_base64 is not None
|
||||
assert audio_base64 is not None, "音频Base64不能为空"
|
||||
return await client.get_audio_transcriptions(
|
||||
model_info=model_info,
|
||||
audio_base64=audio_base64,
|
||||
|
|
@ -365,24 +353,23 @@ class LLMRequest:
|
|||
embedding_input=embedding_input,
|
||||
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
|
||||
|
||||
except ModelAttemptFailed as e:
|
||||
last_exception = e.original_exception or e
|
||||
logger.warning(f"模型 '{model_info.name}' 尝试失败,切换到下一个模型。原因: {e}")
|
||||
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)
|
||||
|
||||
if isinstance(last_exception, RespNotOkException) and last_exception.status_code == 400:
|
||||
logger.error("收到不可恢复的客户端错误 (400),中止所有尝试。")
|
||||
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} 个模型均尝试失败。")
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@
|
|||
"""
|
||||
|
||||
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.chat.emoji_system.emoji_manager import get_emoji_manager
|
||||
from src.chat.utils.utils_image import image_path_to_base64
|
||||
from src.chat.emoji_system.emoji_manager import get_emoji_manager, EMOJI_DIR
|
||||
from src.chat.utils.utils_image import image_path_to_base64, base64_to_image
|
||||
|
||||
logger = get_logger("emoji_api")
|
||||
|
||||
|
|
@ -245,6 +249,42 @@ def get_emotions() -> List[str]:
|
|||
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]:
|
||||
"""获取所有表情包描述
|
||||
|
||||
|
|
@ -264,3 +304,400 @@ def get_descriptions() -> List[str]:
|
|||
except Exception as e:
|
||||
logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}")
|
||||
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_description = "发送表情包辅助表达情绪"
|
||||
|
||||
# LLM判断提示词
|
||||
llm_judge_prompt = """
|
||||
判定是否需要使用表情动作的条件:
|
||||
1. 用户明确要求使用表情包
|
||||
2. 这是一个适合表达强烈情绪的场合
|
||||
3. 不要发送太多表情包,如果你已经发送过多个表情包则回答"否"
|
||||
|
||||
请回答"是"或"否"。
|
||||
"""
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters = {}
|
||||
|
||||
|
|
@ -56,8 +46,9 @@ class EmojiAction(BaseAction):
|
|||
"""执行表情动作"""
|
||||
try:
|
||||
# 1. 获取发送表情的原因
|
||||
reason = self.action_data.get("reason", "表达当前情绪")
|
||||
|
||||
# reason = self.action_data.get("reason", "表达当前情绪")
|
||||
reason = self.reasoning
|
||||
|
||||
# 2. 随机获取20个表情包
|
||||
sampled_emojis = await emoji_api.get_random(30)
|
||||
if not sampled_emojis:
|
||||
|
|
@ -72,6 +63,9 @@ class EmojiAction(BaseAction):
|
|||
emotion_map[emo].append((b64, desc))
|
||||
|
||||
available_emotions = list(emotion_map.keys())
|
||||
available_emotions_str = ""
|
||||
for emotion in available_emotions:
|
||||
available_emotions_str += f"{emotion}\n"
|
||||
|
||||
if not available_emotions:
|
||||
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
|
||||
|
|
@ -90,14 +84,15 @@ class EmojiAction(BaseAction):
|
|||
)
|
||||
|
||||
# 4. 构建prompt让LLM选择情感
|
||||
prompt = f"""
|
||||
你是一个正在进行聊天的网友,你需要根据一个理由和最近的聊天记录,从一个情感标签列表中选择最匹配的一个。
|
||||
这是最近的聊天记录:
|
||||
{messages_text}
|
||||
|
||||
这是理由:“{reason}”
|
||||
这里是可用的情感标签:{available_emotions}
|
||||
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
|
||||
prompt = f"""你正在进行QQ聊天,你需要根据聊天记录,选出一个合适的情感标签。
|
||||
请你根据以下原因和聊天记录进行选择
|
||||
原因:{reason}
|
||||
聊天记录:
|
||||
{messages_text}
|
||||
|
||||
这里是可用的情感标签:
|
||||
{available_emotions_str}
|
||||
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
|
||||
"""
|
||||
|
||||
if global_config.debug.show_prompt:
|
||||
|
|
@ -107,10 +102,10 @@ class EmojiAction(BaseAction):
|
|||
|
||||
# 5. 调用LLM
|
||||
models = llm_api.get_available_models()
|
||||
chat_model_config = models.get("utils_small") # 使用字典访问方式
|
||||
chat_model_config = models.get("replyer") # 使用字典访问方式
|
||||
if not chat_model_config:
|
||||
logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM")
|
||||
return False, "未找到'utils_small'模型配置"
|
||||
logger.error(f"{self.log_prefix} 未找到'replyer'模型配置,无法调用LLM")
|
||||
return False, "未找到'replyer'模型配置"
|
||||
|
||||
success, chosen_emotion, _, _ = await llm_api.generate_with_model(
|
||||
prompt, model_config=chat_model_config, request_type="emoji"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[inner]
|
||||
version = "1.7.0"
|
||||
version = "1.7.2"
|
||||
|
||||
# 配置文件版本号迭代规则同bot_config.toml
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ retry_interval = 5
|
|||
|
||||
[[api_providers]] # 特殊:Google的Gimini使用特殊API,与OpenAI格式不兼容,需要配置client为"gemini"
|
||||
name = "Google"
|
||||
base_url = "https://api.google.com/v1"
|
||||
base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||
api_key = "your-google-api-key-1"
|
||||
client_type = "gemini"
|
||||
max_retry = 2
|
||||
|
|
|
|||
Loading…
Reference in New Issue