mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' of https://github.com/MaiM-with-u/MaiBot into gn-dev
commit
e45711fd58
16
README.md
16
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# 麦麦!MaiCore-MaiMBot (编辑中)
|
# 麦麦!MaiCore-MaiMBot (编辑中)
|
||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div style="text-align: center">
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p style="text-align: center">
|
||||||
<a href="https://github.com/MaiM-with-u/MaiBot/">
|
<a href="https://github.com/MaiM-with-u/MaiBot/">
|
||||||
<img src="depends-data/maimai.png" alt="Logo" style="width: 200px">
|
<img src="depends-data/maimai.png" alt="Logo" style="width: 200px">
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
画师:略nd
|
画师:略nd
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">MaiBot(麦麦)</h3>
|
<h3 style="text-align: center">MaiBot(麦麦)</h3>
|
||||||
<p align="center">
|
<p style="text-align: center">
|
||||||
一款专注于<strong> 群组聊天 </strong>的赛博网友
|
一款专注于<strong> 群组聊天 </strong>的赛博网友
|
||||||
<br />
|
<br />
|
||||||
<a href="https://docs.mai-mai.org"><strong>探索本项目的文档 »</strong></a>
|
<a href="https://docs.mai-mai.org"><strong>探索本项目的文档 »</strong></a>
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储
|
- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储
|
||||||
- 🔄 **动态人格系统**:自适应的性格特征
|
- 🔄 **动态人格系统**:自适应的性格特征
|
||||||
|
|
||||||
<div align="center">
|
<div style="text-align: center">
|
||||||
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1amAneGE3P" target="_blank">
|
||||||
<img src="depends-data/video.png" style="max-width: 200px" alt="麦麦演示视频">
|
<img src="depends-data/video.png" style="max-width: 200px" alt="麦麦演示视频">
|
||||||
<br>
|
<br>
|
||||||
|
|
@ -97,9 +97,9 @@
|
||||||
- [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】
|
- [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】
|
||||||
|
|
||||||
|
|
||||||
<div align="left">
|
|
||||||
<h2>📚 文档 </h2>
|
## 📚 文档
|
||||||
</div>
|
|
||||||
|
|
||||||
### (部分内容可能过时,请注意版本对应)
|
### (部分内容可能过时,请注意版本对应)
|
||||||
|
|
||||||
|
|
|
||||||
3
bot.py
3
bot.py
|
|
@ -122,7 +122,6 @@ async def graceful_shutdown():
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"麦麦关闭失败: {e}")
|
logger.error(f"麦麦关闭失败: {e}")
|
||||||
|
|
||||||
|
|
@ -134,9 +133,7 @@ def check_eula():
|
||||||
privacy_file = Path("PRIVACY.md")
|
privacy_file = Path("PRIVACY.md")
|
||||||
|
|
||||||
eula_updated = True
|
eula_updated = True
|
||||||
eula_new_hash = None
|
|
||||||
privacy_updated = True
|
privacy_updated = True
|
||||||
privacy_new_hash = None
|
|
||||||
|
|
||||||
eula_confirmed = False
|
eula_confirmed = False
|
||||||
privacy_confirmed = False
|
privacy_confirmed = False
|
||||||
|
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
|
@ -8,7 +8,6 @@ import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config
|
from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config
|
||||||
from src.plugins.knowledge.src.embedding_store import EmbeddingManager
|
from src.plugins.knowledge.src.embedding_store import EmbeddingManager
|
||||||
|
|
@ -26,8 +25,8 @@ logger = get_module_logger("LPMM知识库-OpenIE导入")
|
||||||
|
|
||||||
|
|
||||||
def hash_deduplicate(
|
def hash_deduplicate(
|
||||||
raw_paragraphs: Dict[str, str],
|
raw_paragraphs: dict[str, str],
|
||||||
triple_list_data: Dict[str, List[List[str]]],
|
triple_list_data: dict[str, list[list[str]]],
|
||||||
stored_pg_hashes: set,
|
stored_pg_hashes: set,
|
||||||
stored_paragraph_hashes: set,
|
stored_paragraph_hashes: set,
|
||||||
):
|
):
|
||||||
|
|
@ -126,7 +125,7 @@ def main():
|
||||||
)
|
)
|
||||||
|
|
||||||
# 初始化Embedding库
|
# 初始化Embedding库
|
||||||
embed_manager = embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]])
|
embed_manager = EmbeddingManager(llm_client_list[global_config["embedding"]["provider"]])
|
||||||
logger.info("正在从文件加载Embedding库")
|
logger.info("正在从文件加载Embedding库")
|
||||||
try:
|
try:
|
||||||
embed_manager.load_from_file()
|
embed_manager.load_from_file()
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ def process_single_text(pg_hash, raw_data, llm_client_list):
|
||||||
return doc_item, None
|
return doc_item, None
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(signum, frame):
|
def signal_handler(_signum, _frame):
|
||||||
"""处理Ctrl+C信号"""
|
"""处理Ctrl+C信号"""
|
||||||
logger.info("\n接收到中断信号,正在优雅地关闭程序...")
|
logger.info("\n接收到中断信号,正在优雅地关闭程序...")
|
||||||
shutdown_event.set()
|
shutdown_event.set()
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,26 @@ matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
|
||||||
matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号'-'显示为方块的问题
|
matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号'-'显示为方块的问题
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_color():
|
||||||
|
"""生成随机颜色用于区分线条"""
|
||||||
|
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
|
||||||
|
|
||||||
|
|
||||||
|
def format_timestamp(ts):
|
||||||
|
"""辅助函数:格式化时间戳,处理 None 或无效值"""
|
||||||
|
if ts is None:
|
||||||
|
return "N/A"
|
||||||
|
try:
|
||||||
|
# 假设 ts 是 float 类型的时间戳
|
||||||
|
dt_object = datetime.fromtimestamp(float(ts))
|
||||||
|
return dt_object.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return "Invalid Time"
|
||||||
|
|
||||||
|
|
||||||
class InterestMonitorApp:
|
class InterestMonitorApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
|
self._main_mind_loaded = None
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title(WINDOW_TITLE)
|
self.root.title(WINDOW_TITLE)
|
||||||
self.root.geometry("1800x800") # 调整窗口大小以适应图表
|
self.root.geometry("1800x800") # 调整窗口大小以适应图表
|
||||||
|
|
@ -173,10 +191,6 @@ class InterestMonitorApp:
|
||||||
"""当 Combobox 选择改变时调用,更新单个流的图表"""
|
"""当 Combobox 选择改变时调用,更新单个流的图表"""
|
||||||
self.update_single_stream_plot()
|
self.update_single_stream_plot()
|
||||||
|
|
||||||
def get_random_color(self):
|
|
||||||
"""生成随机颜色用于区分线条"""
|
|
||||||
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
|
|
||||||
|
|
||||||
def load_main_mind_history(self):
|
def load_main_mind_history(self):
|
||||||
"""只读取包含main_mind的日志行,维护历史想法队列"""
|
"""只读取包含main_mind的日志行,维护历史想法队列"""
|
||||||
if not os.path.exists(LOG_FILE_PATH):
|
if not os.path.exists(LOG_FILE_PATH):
|
||||||
|
|
@ -332,7 +346,7 @@ class InterestMonitorApp:
|
||||||
new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # 创建概率 deque
|
new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # 创建概率 deque
|
||||||
# 检查是否已有颜色,没有则分配
|
# 检查是否已有颜色,没有则分配
|
||||||
if stream_id not in self.stream_colors:
|
if stream_id not in self.stream_colors:
|
||||||
self.stream_colors[stream_id] = self.get_random_color()
|
self.stream_colors[stream_id] = get_random_color()
|
||||||
|
|
||||||
# *** 存储此 stream_id 最新的显示名称 ***
|
# *** 存储此 stream_id 最新的显示名称 ***
|
||||||
new_stream_display_names[stream_id] = group_name
|
new_stream_display_names[stream_id] = group_name
|
||||||
|
|
@ -593,17 +607,6 @@ class InterestMonitorApp:
|
||||||
# --- 新增:重新绘制画布 ---
|
# --- 新增:重新绘制画布 ---
|
||||||
self.canvas_single.draw()
|
self.canvas_single.draw()
|
||||||
|
|
||||||
def format_timestamp(self, ts):
|
|
||||||
"""辅助函数:格式化时间戳,处理 None 或无效值"""
|
|
||||||
if ts is None:
|
|
||||||
return "N/A"
|
|
||||||
try:
|
|
||||||
# 假设 ts 是 float 类型的时间戳
|
|
||||||
dt_object = datetime.fromtimestamp(float(ts))
|
|
||||||
return dt_object.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return "Invalid Time"
|
|
||||||
|
|
||||||
def update_single_stream_details(self, stream_id):
|
def update_single_stream_details(self, stream_id):
|
||||||
"""更新单个流详情区域的标签内容"""
|
"""更新单个流详情区域的标签内容"""
|
||||||
if stream_id:
|
if stream_id:
|
||||||
|
|
@ -616,8 +619,8 @@ class InterestMonitorApp:
|
||||||
self.single_stream_sub_mind.set(f"想法: {sub_mind}")
|
self.single_stream_sub_mind.set(f"想法: {sub_mind}")
|
||||||
self.single_stream_chat_state.set(f"状态: {chat_state}")
|
self.single_stream_chat_state.set(f"状态: {chat_state}")
|
||||||
self.single_stream_threshold.set(f"阈值以上: {'是' if threshold else '否'}")
|
self.single_stream_threshold.set(f"阈值以上: {'是' if threshold else '否'}")
|
||||||
self.single_stream_last_active.set(f"最后活跃: {self.format_timestamp(last_active_ts)}")
|
self.single_stream_last_active.set(f"最后活跃: {format_timestamp(last_active_ts)}")
|
||||||
self.single_stream_last_interaction.set(f"最后交互: {self.format_timestamp(last_interaction_ts)}")
|
self.single_stream_last_interaction.set(f"最后交互: {format_timestamp(last_interaction_ts)}")
|
||||||
else:
|
else:
|
||||||
# 如果没有选择流,则清空详情
|
# 如果没有选择流,则清空详情
|
||||||
self.single_stream_sub_mind.set("想法: N/A")
|
self.single_stream_sub_mind.set("想法: N/A")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, List, Optional
|
from typing import List, Optional
|
||||||
import strawberry
|
import strawberry
|
||||||
|
|
||||||
# from packaging.version import Version, InvalidVersion
|
# from packaging.version import Version, InvalidVersion
|
||||||
|
|
@ -128,22 +128,22 @@ class BotConfig:
|
||||||
enable_pfc_chatting: bool # 是否启用PFC聊天
|
enable_pfc_chatting: bool # 是否启用PFC聊天
|
||||||
|
|
||||||
# 模型配置
|
# 模型配置
|
||||||
llm_reasoning: Dict[str, str] # LLM推理
|
llm_reasoning: dict[str, str] # LLM推理
|
||||||
# llm_reasoning_minor: Dict[str, str]
|
# llm_reasoning_minor: dict[str, str]
|
||||||
llm_normal: Dict[str, str] # LLM普通
|
llm_normal: dict[str, str] # LLM普通
|
||||||
llm_topic_judge: Dict[str, str] # LLM话题判断
|
llm_topic_judge: dict[str, str] # LLM话题判断
|
||||||
llm_summary: Dict[str, str] # LLM话题总结
|
llm_summary: dict[str, str] # LLM话题总结
|
||||||
llm_emotion_judge: Dict[str, str] # LLM情感判断
|
llm_emotion_judge: dict[str, str] # LLM情感判断
|
||||||
embedding: Dict[str, str] # 嵌入
|
embedding: dict[str, str] # 嵌入
|
||||||
vlm: Dict[str, str] # VLM
|
vlm: dict[str, str] # VLM
|
||||||
moderation: Dict[str, str] # 审核
|
moderation: dict[str, str] # 审核
|
||||||
|
|
||||||
# 实验性
|
# 实验性
|
||||||
llm_observation: Dict[str, str] # LLM观察
|
llm_observation: dict[str, str] # LLM观察
|
||||||
llm_sub_heartflow: Dict[str, str] # LLM子心流
|
llm_sub_heartflow: dict[str, str] # LLM子心流
|
||||||
llm_heartflow: Dict[str, str] # LLM心流
|
llm_heartflow: dict[str, str] # LLM心流
|
||||||
|
|
||||||
api_urls: Dict[str, str] # API URLs
|
api_urls: dict[str, str] # API URLs
|
||||||
|
|
||||||
|
|
||||||
@strawberry.type
|
@strawberry.type
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from typing import Dict, Optional, Union, List, Tuple
|
from typing import Optional, Union, List, Tuple
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
@ -75,8 +75,8 @@ if default_handler_id is not None:
|
||||||
LoguruLogger = logger.__class__
|
LoguruLogger = logger.__class__
|
||||||
|
|
||||||
# 全局注册表:记录模块与处理器ID的映射
|
# 全局注册表:记录模块与处理器ID的映射
|
||||||
_handler_registry: Dict[str, List[int]] = {}
|
_handler_registry: dict[str, List[int]] = {}
|
||||||
_custom_style_handlers: Dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID
|
_custom_style_handlers: dict[Tuple[str, str], List[int]] = {} # 记录自定义样式处理器ID
|
||||||
|
|
||||||
# 获取日志存储根地址
|
# 获取日志存储根地址
|
||||||
current_file_path = Path(__file__).resolve()
|
current_file_path = Path(__file__).resolve()
|
||||||
|
|
@ -321,7 +321,7 @@ CHAT_STYLE_CONFIG = {
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <green>见闻</green> | <green>{message}</green>"), # noqa: E501
|
"console_format": "<level>{time:MM-DD HH:mm}</level> | <green>见闻</green> | <green>{message}</green>", # noqa: E501
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +353,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = {
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #3399FF>麦麦水群 | {message}</fg #3399FF>"), # noqa: E501
|
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #3399FF>麦麦水群 | {message}</fg #3399FF>", # noqa: E501
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +369,7 @@ SUB_HEARTFLOW_MIND_STYLE_CONFIG = {
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>麦麦小脑袋 | {message}</fg #66CCFF>"), # noqa: E501
|
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #66CCFF>麦麦小脑袋 | {message}</fg #66CCFF>", # noqa: E501
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -385,7 +385,7 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
"console_format": ("<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>麦麦水群[管理] | {message}</fg #005BA2>"), # noqa: E501
|
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>麦麦水群[管理] | {message}</fg #005BA2>", # noqa: E501
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -633,13 +633,12 @@ HFC_STYLE_CONFIG = {
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
"console_format": (
|
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>",
|
||||||
"<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天</light-green> | <light-green>{message}</light-green>"
|
|
||||||
),
|
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
CONFIRM_STYLE_CONFIG = {
|
CONFIRM_STYLE_CONFIG = {
|
||||||
"console_format": "<RED>{message}</RED>", # noqa: E501
|
"console_format": "<RED>{message}</RED>", # noqa: E501
|
||||||
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",
|
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",
|
||||||
|
|
@ -1032,7 +1031,7 @@ def add_custom_style_handler(
|
||||||
# retention=current_config["retention"],
|
# retention=current_config["retention"],
|
||||||
# compression=current_config["compression"],
|
# compression=current_config["compression"],
|
||||||
# encoding="utf-8",
|
# encoding="utf-8",
|
||||||
# filter=lambda record: record["extra"].get("module") == module_name
|
# message_filter=lambda record: record["extra"].get("module") == module_name
|
||||||
# and record["extra"].get("custom_style") == style_name,
|
# and record["extra"].get("custom_style") == style_name,
|
||||||
# enqueue=True,
|
# enqueue=True,
|
||||||
# )
|
# )
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
from src.common.database import db
|
from src.common.database import db
|
||||||
from src.common.logger import get_module_logger
|
from src.common.logger import get_module_logger
|
||||||
import traceback
|
import traceback
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Any, Optional
|
||||||
|
|
||||||
logger = get_module_logger(__name__)
|
logger = get_module_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def find_messages(
|
def find_messages(
|
||||||
filter: Dict[str, Any], sort: Optional[List[tuple[str, int]]] = None, limit: int = 0, limit_mode: str = "latest"
|
message_filter: dict[str, Any],
|
||||||
) -> List[Dict[str, Any]]:
|
sort: Optional[List[tuple[str, int]]] = None,
|
||||||
|
limit: int = 0,
|
||||||
|
limit_mode: str = "latest",
|
||||||
|
) -> List[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
根据提供的过滤器、排序和限制条件查找消息。
|
根据提供的过滤器、排序和限制条件查找消息。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filter: MongoDB 查询过滤器。
|
message_filter: MongoDB 查询过滤器。
|
||||||
sort: MongoDB 排序条件列表,例如 [('time', 1)]。仅在 limit 为 0 时生效。
|
sort: MongoDB 排序条件列表,例如 [('time', 1)]。仅在 limit 为 0 时生效。
|
||||||
limit: 返回的最大文档数,0表示不限制。
|
limit: 返回的最大文档数,0表示不限制。
|
||||||
limit_mode: 当 limit > 0 时生效。 'earliest' 表示获取最早的记录, 'latest' 表示获取最新的记录(结果仍按时间正序排列)。默认为 'latest'。
|
limit_mode: 当 limit > 0 时生效。 'earliest' 表示获取最早的记录, 'latest' 表示获取最新的记录(结果仍按时间正序排列)。默认为 'latest'。
|
||||||
|
|
@ -22,8 +25,7 @@ def find_messages(
|
||||||
消息文档列表,如果出错则返回空列表。
|
消息文档列表,如果出错则返回空列表。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
query = db.messages.find(filter)
|
query = db.messages.find(message_filter)
|
||||||
results: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
if limit > 0:
|
if limit > 0:
|
||||||
if limit_mode == "earliest":
|
if limit_mode == "earliest":
|
||||||
|
|
@ -46,28 +48,28 @@ def find_messages(
|
||||||
return results
|
return results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_message = (
|
log_message = (
|
||||||
f"查找消息失败 (filter={filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n"
|
f"查找消息失败 (filter={message_filter}, sort={sort}, limit={limit}, limit_mode={limit_mode}): {e}\n"
|
||||||
+ traceback.format_exc()
|
+ traceback.format_exc()
|
||||||
)
|
)
|
||||||
logger.error(log_message)
|
logger.error(log_message)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def count_messages(filter: Dict[str, Any]) -> int:
|
def count_messages(message_filter: dict[str, Any]) -> int:
|
||||||
"""
|
"""
|
||||||
根据提供的过滤器计算消息数量。
|
根据提供的过滤器计算消息数量。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filter: MongoDB 查询过滤器。
|
message_filter: MongoDB 查询过滤器。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
符合条件的消息数量,如果出错则返回 0。
|
符合条件的消息数量,如果出错则返回 0。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
count = db.messages.count_documents(filter)
|
count = db.messages.count_documents(message_filter)
|
||||||
return count
|
return count
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_message = f"计数消息失败 (filter={filter}): {e}\n" + traceback.format_exc()
|
log_message = f"计数消息失败 (message_filter={message_filter}): {e}\n" + traceback.format_exc()
|
||||||
logger.error(log_message)
|
logger.error(log_message)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ logger = get_logger("config")
|
||||||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||||
is_test = False
|
is_test = False
|
||||||
mai_version_main = "0.6.3"
|
mai_version_main = "0.6.3"
|
||||||
mai_version_fix = ""
|
mai_version_fix = "fix-1"
|
||||||
|
|
||||||
if mai_version_fix:
|
if mai_version_fix:
|
||||||
if is_test:
|
if is_test:
|
||||||
|
|
@ -278,8 +278,8 @@ class BotConfig:
|
||||||
NICKNAME_PROCESS_SLEEP_INTERVAL: float = 0.5 # 绰号处理进程休眠间隔(秒)
|
NICKNAME_PROCESS_SLEEP_INTERVAL: float = 0.5 # 绰号处理进程休眠间隔(秒)
|
||||||
|
|
||||||
# 模型配置
|
# 模型配置
|
||||||
llm_reasoning: Dict[str, str] = field(default_factory=lambda: {})
|
llm_reasoning: dict[str, str] = field(default_factory=lambda: {})
|
||||||
# llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {})
|
# llm_reasoning_minor: dict[str, str] = field(default_factory=lambda: {})
|
||||||
llm_normal: Dict[str, str] = field(default_factory=lambda: {})
|
llm_normal: Dict[str, str] = field(default_factory=lambda: {})
|
||||||
llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {})
|
llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {})
|
||||||
llm_summary: Dict[str, str] = field(default_factory=lambda: {})
|
llm_summary: Dict[str, str] = field(default_factory=lambda: {})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from src.config.config import global_config
|
||||||
from src.common.logger_manager import get_logger
|
from src.common.logger_manager import get_logger
|
||||||
from src.plugins.moods.moods import MoodManager
|
from src.plugins.moods.moods import MoodManager
|
||||||
|
|
||||||
from typing import Dict, Any
|
from typing import Any
|
||||||
|
|
||||||
logger = get_logger("change_mood_tool")
|
logger = get_logger("change_mood_tool")
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ class ChangeMoodTool(BaseTool):
|
||||||
"required": ["text", "response_set"],
|
"required": ["text", "response_set"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||||
"""执行心情改变
|
"""执行心情改变
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -30,7 +30,7 @@ class ChangeMoodTool(BaseTool):
|
||||||
message_txt: 原始消息文本
|
message_txt: 原始消息文本
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
dict: 工具执行结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response_set = function_args.get("response_set")
|
response_set = function_args.get("response_set")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, Any
|
from typing import Any
|
||||||
from src.common.logger_manager import get_logger
|
from src.common.logger_manager import get_logger
|
||||||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ class RelationshipTool(BaseTool):
|
||||||
"required": ["text", "changed_value", "reason"],
|
"required": ["text", "changed_value", "reason"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> dict:
|
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict:
|
||||||
"""执行工具功能
|
"""执行工具功能
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||||
from src.plugins.schedule.schedule_generator import bot_schedule
|
from src.plugins.schedule.schedule_generator import bot_schedule
|
||||||
from src.common.logger import get_module_logger
|
from src.common.logger import get_module_logger
|
||||||
from typing import Dict, Any
|
from typing import Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
logger = get_module_logger("get_current_task_tool")
|
logger = get_module_logger("get_current_task_tool")
|
||||||
|
|
@ -21,7 +21,7 @@ class GetCurrentTaskTool(BaseTool):
|
||||||
"required": ["start_time", "end_time"],
|
"required": ["start_time", "end_time"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||||
"""执行获取当前任务或指定时间段的日程信息
|
"""执行获取当前任务或指定时间段的日程信息
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -29,7 +29,7 @@ class GetCurrentTaskTool(BaseTool):
|
||||||
message_txt: 原始消息文本,此工具不使用
|
message_txt: 原始消息文本,此工具不使用
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
dict: 工具执行结果
|
||||||
"""
|
"""
|
||||||
start_time = function_args.get("start_time")
|
start_time = function_args.get("start_time")
|
||||||
end_time = function_args.get("end_time")
|
end_time = function_args.get("end_time")
|
||||||
|
|
@ -55,5 +55,6 @@ class GetCurrentTaskTool(BaseTool):
|
||||||
task_info = "\n".join(task_list)
|
task_info = "\n".join(task_list)
|
||||||
else:
|
else:
|
||||||
task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息"
|
task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息"
|
||||||
|
else:
|
||||||
|
task_info = "请提供有效的开始时间和结束时间"
|
||||||
return {"name": "get_current_task", "content": f"日程信息: {task_info}"}
|
return {"name": "get_current_task", "content": f"日程信息: {task_info}"}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||||
from src.common.logger import get_module_logger
|
from src.common.logger import get_module_logger
|
||||||
from typing import Dict, Any
|
from typing import Any
|
||||||
|
|
||||||
logger = get_module_logger("get_mid_memory_tool")
|
logger = get_module_logger("get_mid_memory_tool")
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ class GetMidMemoryTool(BaseTool):
|
||||||
"required": ["id"],
|
"required": ["id"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||||
"""执行记忆获取
|
"""执行记忆获取
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -26,7 +26,7 @@ class GetMidMemoryTool(BaseTool):
|
||||||
message_txt: 原始消息文本
|
message_txt: 原始消息文本
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
dict: 工具执行结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
id = function_args.get("id")
|
id = function_args.get("id")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||||
from src.common.logger import get_module_logger
|
from src.common.logger import get_module_logger
|
||||||
|
|
||||||
from typing import Dict, Any
|
from typing import Any
|
||||||
|
|
||||||
logger = get_module_logger("send_emoji_tool")
|
logger = get_module_logger("send_emoji_tool")
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ class SendEmojiTool(BaseTool):
|
||||||
"required": ["text"],
|
"required": ["text"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
|
||||||
text = function_args.get("text", message_txt)
|
text = function_args.get("text", message_txt)
|
||||||
return {
|
return {
|
||||||
"name": "send_emoji",
|
"name": "send_emoji",
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class MyNewTool(BaseTool):
|
||||||
message_txt: 原始消息文本
|
message_txt: 原始消息文本
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 包含执行结果的字典,必须包含name和content字段
|
dict: 包含执行结果的字典,必须包含name和content字段
|
||||||
"""
|
"""
|
||||||
# 实现工具逻辑
|
# 实现工具逻辑
|
||||||
result = f"工具执行结果: {function_args.get('param1')}"
|
result = f"工具执行结果: {function_args.get('param1')}"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, List, Any, Optional, Type
|
from typing import List, Any, Optional, Type
|
||||||
import inspect
|
import inspect
|
||||||
import importlib
|
import importlib
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
@ -22,11 +22,11 @@ class BaseTool:
|
||||||
parameters = None
|
parameters = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_tool_definition(cls) -> Dict[str, Any]:
|
def get_tool_definition(cls) -> dict[str, Any]:
|
||||||
"""获取工具定义,用于LLM工具调用
|
"""获取工具定义,用于LLM工具调用
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具定义字典
|
dict: 工具定义字典
|
||||||
"""
|
"""
|
||||||
if not cls.name or not cls.description or not cls.parameters:
|
if not cls.name or not cls.description or not cls.parameters:
|
||||||
raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性")
|
raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性")
|
||||||
|
|
@ -36,14 +36,14 @@ class BaseTool:
|
||||||
"function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters},
|
"function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""执行工具函数
|
"""执行工具函数
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function_args: 工具调用参数
|
function_args: 工具调用参数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
dict: 工具执行结果
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("子类必须实现execute方法")
|
raise NotImplementedError("子类必须实现execute方法")
|
||||||
|
|
||||||
|
|
@ -88,11 +88,11 @@ def discover_tools():
|
||||||
logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具")
|
logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具")
|
||||||
|
|
||||||
|
|
||||||
def get_all_tool_definitions() -> List[Dict[str, Any]]:
|
def get_all_tool_definitions() -> List[dict[str, Any]]:
|
||||||
"""获取所有已注册工具的定义
|
"""获取所有已注册工具的定义
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict]: 工具定义列表
|
List[dict]: 工具定义列表
|
||||||
"""
|
"""
|
||||||
return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()]
|
return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from src.do_tool.tool_can_use.base_tool import BaseTool
|
from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||||
from src.common.logger import get_module_logger
|
from src.common.logger import get_module_logger
|
||||||
from typing import Dict, Any
|
from typing import Any
|
||||||
|
|
||||||
logger = get_module_logger("compare_numbers_tool")
|
logger = get_module_logger("compare_numbers_tool")
|
||||||
|
|
||||||
|
|
@ -19,15 +19,14 @@ class CompareNumbersTool(BaseTool):
|
||||||
"required": ["num1", "num2"],
|
"required": ["num1", "num2"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""执行比较两个数的大小
|
"""执行比较两个数的大小
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function_args: 工具参数
|
function_args: 工具参数
|
||||||
message_txt: 原始消息文本
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
dict: 工具执行结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
num1 = function_args.get("num1")
|
num1 = function_args.get("num1")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool
|
||||||
from src.plugins.chat.utils import get_embedding
|
from src.plugins.chat.utils import get_embedding
|
||||||
from src.common.database import db
|
from src.common.database import db
|
||||||
from src.common.logger_manager import get_logger
|
from src.common.logger_manager import get_logger
|
||||||
from typing import Dict, Any, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
logger = get_logger("get_knowledge_tool")
|
logger = get_logger("get_knowledge_tool")
|
||||||
|
|
||||||
|
|
@ -21,15 +21,14 @@ class SearchKnowledgeTool(BaseTool):
|
||||||
"required": ["query"],
|
"required": ["query"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
|
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""执行知识库搜索
|
"""执行知识库搜索
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function_args: 工具参数
|
function_args: 工具参数
|
||||||
message_txt: 原始消息文本
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
dict: 工具执行结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
query = function_args.get("query")
|
query = function_args.get("query")
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ class GetMemoryTool(BaseTool):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function_args: 工具参数
|
function_args: 工具参数
|
||||||
message_txt: 原始消息文本
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
Dict: 工具执行结果
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ class GetCurrentDateTimeTool(BaseTool):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function_args: 工具参数(此工具不使用)
|
function_args: 工具参数(此工具不使用)
|
||||||
message_txt: 原始消息文本(此工具不使用)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
Dict: 工具执行结果
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
function_args: 工具参数
|
function_args: 工具参数
|
||||||
message_txt: 原始消息文本
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 工具执行结果
|
Dict: 工具执行结果
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,6 @@ class ToolUser:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message_txt: 用户消息文本
|
message_txt: 用户消息文本
|
||||||
sender_name: 发送者名称
|
|
||||||
chat_stream: 聊天流对象
|
chat_stream: 聊天流对象
|
||||||
observation: 观察对象(可选)
|
observation: 观察对象(可选)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,33 @@ STATE_UPDATE_INTERVAL_SECONDS = 60
|
||||||
LOG_INTERVAL_SECONDS = 3
|
LOG_INTERVAL_SECONDS = 3
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_periodic_loop(
|
||||||
|
task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs
|
||||||
|
):
|
||||||
|
"""周期性任务主循环"""
|
||||||
|
while True:
|
||||||
|
start_time = asyncio.get_event_loop().time()
|
||||||
|
# logger.debug(f"开始执行后台任务: {task_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await task_func(**kwargs) # 执行实际任务
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info(f"任务 {task_name} 已取消")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"任务 {task_name} 执行出错: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# 计算并执行间隔等待
|
||||||
|
elapsed = asyncio.get_event_loop().time() - start_time
|
||||||
|
sleep_time = max(0, interval - elapsed)
|
||||||
|
# if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点
|
||||||
|
# logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)")
|
||||||
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
|
logger.debug(f"任务循环结束: {task_name}") # 调整日志信息
|
||||||
|
|
||||||
|
|
||||||
class BackgroundTaskManager:
|
class BackgroundTaskManager:
|
||||||
"""管理 Heartflow 的后台周期性任务。"""
|
"""管理 Heartflow 的后台周期性任务。"""
|
||||||
|
|
||||||
|
|
@ -143,32 +170,6 @@ class BackgroundTaskManager:
|
||||||
# 第三步:清空任务列表
|
# 第三步:清空任务列表
|
||||||
self._tasks = [] # 重置任务列表
|
self._tasks = [] # 重置任务列表
|
||||||
|
|
||||||
async def _run_periodic_loop(
|
|
||||||
self, task_name: str, interval: int, task_func: Callable[..., Coroutine[Any, Any, None]], **kwargs
|
|
||||||
):
|
|
||||||
"""周期性任务主循环"""
|
|
||||||
while True:
|
|
||||||
start_time = asyncio.get_event_loop().time()
|
|
||||||
# logger.debug(f"开始执行后台任务: {task_name}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
await task_func(**kwargs) # 执行实际任务
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logger.info(f"任务 {task_name} 已取消")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"任务 {task_name} 执行出错: {e}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
# 计算并执行间隔等待
|
|
||||||
elapsed = asyncio.get_event_loop().time() - start_time
|
|
||||||
sleep_time = max(0, interval - elapsed)
|
|
||||||
# if sleep_time < 0.1: # 任务超时处理, DEBUG 时可能干扰断点
|
|
||||||
# logger.warning(f"任务 {task_name} 超时执行 ({elapsed:.2f}s > {interval}s)")
|
|
||||||
await asyncio.sleep(sleep_time)
|
|
||||||
|
|
||||||
logger.debug(f"任务循环结束: {task_name}") # 调整日志信息
|
|
||||||
|
|
||||||
async def _perform_state_update_work(self):
|
async def _perform_state_update_work(self):
|
||||||
"""执行状态更新工作"""
|
"""执行状态更新工作"""
|
||||||
previous_status = self.mai_state_info.get_current_state()
|
previous_status = self.mai_state_info.get_current_state()
|
||||||
|
|
@ -249,33 +250,29 @@ class BackgroundTaskManager:
|
||||||
|
|
||||||
# --- Specific Task Runners --- #
|
# --- Specific Task Runners --- #
|
||||||
async def _run_state_update_cycle(self, interval: int):
|
async def _run_state_update_cycle(self, interval: int):
|
||||||
await self._run_periodic_loop(
|
await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work)
|
||||||
task_name="State Update", interval=interval, task_func=self._perform_state_update_work
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _run_absent_into_chat(self, interval: int):
|
async def _run_absent_into_chat(self, interval: int):
|
||||||
await self._run_periodic_loop(
|
await _run_periodic_loop(task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat)
|
||||||
task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _run_normal_chat_timeout_check_cycle(self, interval: int):
|
async def _run_normal_chat_timeout_check_cycle(self, interval: int):
|
||||||
await self._run_periodic_loop(
|
await _run_periodic_loop(
|
||||||
task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work
|
task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _run_cleanup_cycle(self):
|
async def _run_cleanup_cycle(self):
|
||||||
await self._run_periodic_loop(
|
await _run_periodic_loop(
|
||||||
task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
|
task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _run_logging_cycle(self):
|
async def _run_logging_cycle(self):
|
||||||
await self._run_periodic_loop(
|
await _run_periodic_loop(
|
||||||
task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work
|
task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 新增兴趣评估任务运行器 ---
|
# --- 新增兴趣评估任务运行器 ---
|
||||||
async def _run_into_focus_cycle(self):
|
async def _run_into_focus_cycle(self):
|
||||||
await self._run_periodic_loop(
|
await _run_periodic_loop(
|
||||||
task_name="Into Focus",
|
task_name="Into Focus",
|
||||||
interval=INTEREST_EVAL_INTERVAL_SECONDS,
|
interval=INTEREST_EVAL_INTERVAL_SECONDS,
|
||||||
task_func=self._perform_into_focus_work,
|
task_func=self._perform_into_focus_work,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ LOG_DIRECTORY = "logs/interest"
|
||||||
HISTORY_LOG_FILENAME = "interest_history.log"
|
HISTORY_LOG_FILENAME = "interest_history.log"
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_log_directory():
|
||||||
|
"""确保日志目录存在。"""
|
||||||
|
os.makedirs(LOG_DIRECTORY, exist_ok=True)
|
||||||
|
logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在")
|
||||||
|
|
||||||
|
|
||||||
class InterestLogger:
|
class InterestLogger:
|
||||||
"""负责定期记录主心流和所有子心流的状态到日志文件。"""
|
"""负责定期记录主心流和所有子心流的状态到日志文件。"""
|
||||||
|
|
||||||
|
|
@ -37,12 +43,7 @@ class InterestLogger:
|
||||||
self.subheartflow_manager = subheartflow_manager
|
self.subheartflow_manager = subheartflow_manager
|
||||||
self.heartflow = heartflow # 存储 Heartflow 实例
|
self.heartflow = heartflow # 存储 Heartflow 实例
|
||||||
self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME)
|
self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME)
|
||||||
self._ensure_log_directory()
|
_ensure_log_directory()
|
||||||
|
|
||||||
def _ensure_log_directory(self):
|
|
||||||
"""确保日志目录存在。"""
|
|
||||||
os.makedirs(LOG_DIRECTORY, exist_ok=True)
|
|
||||||
logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在")
|
|
||||||
|
|
||||||
async def get_all_subflow_states(self) -> Dict[str, Dict]:
|
async def get_all_subflow_states(self) -> Dict[str, Dict]:
|
||||||
"""并发获取所有活跃子心流的当前完整状态。"""
|
"""并发获取所有活跃子心流的当前完整状态。"""
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ logger = get_logger("mai_state")
|
||||||
|
|
||||||
|
|
||||||
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
|
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
|
||||||
|
# The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls
|
||||||
|
# whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to
|
||||||
|
# `False`, it means that the debugging feature for unlimited focused chatting is disabled.
|
||||||
# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
|
# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
|
||||||
enable_unlimited_hfc_chat = True
|
enable_unlimited_hfc_chat = True
|
||||||
prevent_offline_state = True
|
prevent_offline_state = True
|
||||||
|
|
@ -59,6 +62,7 @@ class MaiState(enum.Enum):
|
||||||
return MAX_NORMAL_CHAT_NUM_NORMAL
|
return MAX_NORMAL_CHAT_NUM_NORMAL
|
||||||
elif self == MaiState.FOCUSED_CHAT:
|
elif self == MaiState.FOCUSED_CHAT:
|
||||||
return MAX_NORMAL_CHAT_NUM_FOCUSED
|
return MAX_NORMAL_CHAT_NUM_FOCUSED
|
||||||
|
return None
|
||||||
|
|
||||||
def get_focused_chat_max_num(self):
|
def get_focused_chat_max_num(self):
|
||||||
# 调试用
|
# 调试用
|
||||||
|
|
@ -73,6 +77,7 @@ class MaiState(enum.Enum):
|
||||||
return MAX_FOCUSED_CHAT_NUM_NORMAL
|
return MAX_FOCUSED_CHAT_NUM_NORMAL
|
||||||
elif self == MaiState.FOCUSED_CHAT:
|
elif self == MaiState.FOCUSED_CHAT:
|
||||||
return MAX_FOCUSED_CHAT_NUM_FOCUSED
|
return MAX_FOCUSED_CHAT_NUM_FOCUSED
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class MaiStateInfo:
|
class MaiStateInfo:
|
||||||
|
|
@ -132,7 +137,8 @@ class MaiStateManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_and_decide_next_state(self, current_state_info: MaiStateInfo) -> Optional[MaiState]:
|
@staticmethod
|
||||||
|
def check_and_decide_next_state(current_state_info: MaiStateInfo) -> Optional[MaiState]:
|
||||||
"""
|
"""
|
||||||
根据当前状态和规则检查是否需要转换状态,并决定下一个状态。
|
根据当前状态和规则检查是否需要转换状态,并决定下一个状态。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from src.heart_flow.sub_mind import SubMind
|
||||||
# 定义常量 (从 interest.py 移动过来)
|
# 定义常量 (从 interest.py 移动过来)
|
||||||
MAX_INTEREST = 15.0
|
MAX_INTEREST = 15.0
|
||||||
|
|
||||||
logger = get_logger("subheartflow")
|
logger = get_logger("sub_heartflow")
|
||||||
|
|
||||||
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
|
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
|
||||||
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
|
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
|
||||||
|
|
@ -346,7 +346,7 @@ class SubHeartflow:
|
||||||
return True # 已经在运行
|
return True # 已经在运行
|
||||||
|
|
||||||
# 如果实例不存在,则创建并启动
|
# 如果实例不存在,则创建并启动
|
||||||
logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...")
|
logger.info(f"{log_prefix} 麦麦准备开始专注聊天...")
|
||||||
try:
|
try:
|
||||||
# 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
|
# 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
|
||||||
self.heart_fc_instance = HeartFChatting(
|
self.heart_fc_instance = HeartFChatting(
|
||||||
|
|
@ -359,7 +359,7 @@ class SubHeartflow:
|
||||||
# 初始化并启动 HeartFChatting
|
# 初始化并启动 HeartFChatting
|
||||||
if await self.heart_fc_instance._initialize():
|
if await self.heart_fc_instance._initialize():
|
||||||
await self.heart_fc_instance.start()
|
await self.heart_fc_instance.start()
|
||||||
logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
|
logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。")
|
logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。")
|
||||||
|
|
@ -397,7 +397,7 @@ class SubHeartflow:
|
||||||
# 移除限额检查逻辑
|
# 移除限额检查逻辑
|
||||||
logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
|
logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
|
||||||
if await self._start_heart_fc_chat():
|
if await self._start_heart_fc_chat():
|
||||||
logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
|
logger.debug(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
|
||||||
state_changed = True
|
state_changed = True
|
||||||
else:
|
else:
|
||||||
logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
|
logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
|
||||||
|
|
@ -511,12 +511,12 @@ class SubHeartflow:
|
||||||
|
|
||||||
# 取消可能存在的旧后台任务 (self.task)
|
# 取消可能存在的旧后台任务 (self.task)
|
||||||
if self.task and not self.task.done():
|
if self.task and not self.task.done():
|
||||||
logger.info(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
|
logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
|
||||||
self.task.cancel()
|
self.task.cancel()
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消
|
await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
|
logger.debug(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。")
|
logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ def calculate_replacement_probability(similarity: float) -> float:
|
||||||
# p = 3.5 * s - 1.4
|
# p = 3.5 * s - 1.4
|
||||||
probability = 3.5 * similarity - 1.4
|
probability = 3.5 * similarity - 1.4
|
||||||
return max(0.0, probability)
|
return max(0.0, probability)
|
||||||
elif 0.6 < similarity < 0.9:
|
else: # 0.6 < similarity < 0.9
|
||||||
# p = s + 0.1
|
# p = s + 0.1
|
||||||
probability = similarity + 0.1
|
probability = similarity + 0.1
|
||||||
return min(1.0, max(0.0, probability))
|
return min(1.0, max(0.0, probability))
|
||||||
|
|
@ -86,6 +86,7 @@ def calculate_replacement_probability(similarity: float) -> float:
|
||||||
|
|
||||||
class SubMind:
|
class SubMind:
|
||||||
def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation):
|
def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation):
|
||||||
|
self.last_active_time = None
|
||||||
self.subheartflow_id = subheartflow_id
|
self.subheartflow_id = subheartflow_id
|
||||||
|
|
||||||
self.llm_model = LLMRequest(
|
self.llm_model = LLMRequest(
|
||||||
|
|
@ -168,7 +169,7 @@ class SubMind:
|
||||||
|
|
||||||
last_cycle = history_cycle[-1] if history_cycle else None
|
last_cycle = history_cycle[-1] if history_cycle else None
|
||||||
# 上一次决策信息
|
# 上一次决策信息
|
||||||
if last_cycle != None:
|
if last_cycle is not None:
|
||||||
last_action = last_cycle.action_type
|
last_action = last_cycle.action_type
|
||||||
last_reasoning = last_cycle.reasoning
|
last_reasoning = last_cycle.reasoning
|
||||||
is_replan = last_cycle.replanned
|
is_replan = last_cycle.replanned
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,40 @@ INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
|
||||||
NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟
|
NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟
|
||||||
|
|
||||||
|
|
||||||
|
async def _try_set_subflow_absent_internal(subflow: "SubHeartflow", log_prefix: str) -> bool:
|
||||||
|
"""
|
||||||
|
尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subflow: 子心流对象。
|
||||||
|
log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。
|
||||||
|
"""
|
||||||
|
flow_id = subflow.subheartflow_id
|
||||||
|
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
|
||||||
|
|
||||||
|
if subflow.chat_state.chat_status != ChatState.ABSENT:
|
||||||
|
logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT")
|
||||||
|
try:
|
||||||
|
await subflow.change_chat_state(ChatState.ABSENT)
|
||||||
|
# 再次检查以确认状态已更改 (change_chat_state 内部应确保)
|
||||||
|
if subflow.chat_state.chat_status == ChatState.ABSENT:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态")
|
||||||
|
return True # 已经是目标状态,视为成功
|
||||||
|
|
||||||
|
|
||||||
class SubHeartflowManager:
|
class SubHeartflowManager:
|
||||||
"""管理所有活跃的 SubHeartflow 实例。"""
|
"""管理所有活跃的 SubHeartflow 实例。"""
|
||||||
|
|
||||||
|
|
@ -109,38 +143,6 @@ class SubHeartflowManager:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT ---
|
# --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT ---
|
||||||
async def _try_set_subflow_absent_internal(self, subflow: "SubHeartflow", log_prefix: str) -> bool:
|
|
||||||
"""
|
|
||||||
尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
subflow: 子心流对象。
|
|
||||||
log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。
|
|
||||||
"""
|
|
||||||
flow_id = subflow.subheartflow_id
|
|
||||||
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
|
|
||||||
|
|
||||||
if subflow.chat_state.chat_status != ChatState.ABSENT:
|
|
||||||
logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT")
|
|
||||||
try:
|
|
||||||
await subflow.change_chat_state(ChatState.ABSENT)
|
|
||||||
# 再次检查以确认状态已更改 (change_chat_state 内部应确保)
|
|
||||||
if subflow.chat_state.chat_status == ChatState.ABSENT:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态")
|
|
||||||
return True # 已经是目标状态,视为成功
|
|
||||||
|
|
||||||
# --- 结束新增 ---
|
# --- 结束新增 ---
|
||||||
|
|
||||||
|
|
@ -154,7 +156,7 @@ class SubHeartflowManager:
|
||||||
logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}")
|
logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}")
|
||||||
|
|
||||||
# 调用内部方法处理状态变更
|
# 调用内部方法处理状态变更
|
||||||
success = await self._try_set_subflow_absent_internal(subheartflow, log_prefix)
|
success = await _try_set_subflow_absent_internal(subheartflow, log_prefix)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
# 锁在此处自动释放
|
# 锁在此处自动释放
|
||||||
|
|
@ -241,7 +243,7 @@ class SubHeartflowManager:
|
||||||
# 记录原始状态,以便统计实际改变的数量
|
# 记录原始状态,以便统计实际改变的数量
|
||||||
original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT
|
original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT
|
||||||
|
|
||||||
success = await self._try_set_subflow_absent_internal(subflow, log_prefix)
|
success = await _try_set_subflow_absent_internal(subflow, log_prefix)
|
||||||
|
|
||||||
# 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数
|
# 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数
|
||||||
if success and not original_state_was_absent:
|
if success and not original_state_was_absent:
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ class Individuality:
|
||||||
p_pronoun = "我"
|
p_pronoun = "我"
|
||||||
prompt_personality = f"{p_pronoun}{self.personality.personality_core}"
|
prompt_personality = f"{p_pronoun}{self.personality.personality_core}"
|
||||||
else: # x_person == 0
|
else: # x_person == 0
|
||||||
p_pronoun = "" # 无人称
|
|
||||||
# 对于无人称,直接描述核心特征
|
# 对于无人称,直接描述核心特征
|
||||||
prompt_personality = f"{self.personality.personality_core}"
|
prompt_personality = f"{self.personality.personality_core}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import json
|
import json
|
||||||
from typing import Dict
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def load_scenes() -> Dict:
|
def load_scenes() -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
从JSON文件加载场景数据
|
从JSON文件加载场景数据
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ def load_scenes() -> Dict:
|
||||||
PERSONALITY_SCENES = load_scenes()
|
PERSONALITY_SCENES = load_scenes()
|
||||||
|
|
||||||
|
|
||||||
def get_scene_by_factor(factor: str) -> Dict:
|
def get_scene_by_factor(factor: str) -> dict | None:
|
||||||
"""
|
"""
|
||||||
根据人格因子获取对应的情景测试
|
根据人格因子获取对应的情景测试
|
||||||
|
|
||||||
|
|
@ -28,12 +28,12 @@ def get_scene_by_factor(factor: str) -> Dict:
|
||||||
factor (str): 人格因子名称
|
factor (str): 人格因子名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 包含情景描述的字典
|
dict: 包含情景描述的字典
|
||||||
"""
|
"""
|
||||||
return PERSONALITY_SCENES.get(factor, None)
|
return PERSONALITY_SCENES.get(factor, None)
|
||||||
|
|
||||||
|
|
||||||
def get_all_scenes() -> Dict:
|
def get_all_scenes() -> dict:
|
||||||
"""
|
"""
|
||||||
获取所有情景测试
|
获取所有情景测试
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,6 @@ class ActionPlanner:
|
||||||
# --- 知识信息字符串构建结束 ---
|
# --- 知识信息字符串构建结束 ---
|
||||||
|
|
||||||
# 获取聊天历史记录 (chat_history_text)
|
# 获取聊天历史记录 (chat_history_text)
|
||||||
chat_history_text = ""
|
|
||||||
try:
|
try:
|
||||||
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||||
chat_history_text = observation_info.chat_history_str
|
chat_history_text = observation_info.chat_history_str
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class ChatObserver:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
stream_id: 聊天流ID
|
stream_id: 聊天流ID
|
||||||
|
private_name: 私聊名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ChatObserver: 观察器实例
|
ChatObserver: 观察器实例
|
||||||
|
|
@ -37,6 +38,10 @@ class ChatObserver:
|
||||||
Args:
|
Args:
|
||||||
stream_id: 聊天流ID
|
stream_id: 聊天流ID
|
||||||
"""
|
"""
|
||||||
|
self.last_check_time = None
|
||||||
|
self.last_check_time = None
|
||||||
|
self.last_bot_speak_time = None
|
||||||
|
self.last_user_speak_time = None
|
||||||
if stream_id in self._instances:
|
if stream_id in self._instances:
|
||||||
raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.")
|
raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.")
|
||||||
|
|
||||||
|
|
@ -118,11 +123,11 @@ class ChatObserver:
|
||||||
self.last_cold_chat_check = current_time
|
self.last_cold_chat_check = current_time
|
||||||
|
|
||||||
# 判断是否冷场
|
# 判断是否冷场
|
||||||
is_cold = False
|
is_cold = (
|
||||||
if self.last_message_time is None:
|
True
|
||||||
is_cold = True
|
if self.last_message_time is None
|
||||||
else:
|
else (current_time - self.last_message_time) > self.cold_chat_threshold
|
||||||
is_cold = (current_time - self.last_message_time) > self.cold_chat_threshold
|
)
|
||||||
|
|
||||||
# 如果冷场状态发生变化,发送通知
|
# 如果冷场状态发生变化,发送通知
|
||||||
if is_cold != self.is_cold_chat_state:
|
if is_cold != self.is_cold_chat_state:
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,9 @@ class MongoDBMessageStorage(MessageStorage):
|
||||||
"""MongoDB消息存储实现"""
|
"""MongoDB消息存储实现"""
|
||||||
|
|
||||||
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
|
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
|
||||||
query = {"chat_id": chat_id}
|
query = {"chat_id": chat_id, "time": {"$gt": message_time}}
|
||||||
# print(f"storage_check_message: {message_time}")
|
# print(f"storage_check_message: {message_time}")
|
||||||
|
|
||||||
query["time"] = {"$gt": message_time}
|
|
||||||
|
|
||||||
return list(db.messages.find(query).sort("time", 1))
|
return list(db.messages.find(query).sort("time", 1))
|
||||||
|
|
||||||
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
|
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,10 @@ class ObservationInfo:
|
||||||
# meta_plan_trigger: bool = False
|
# meta_plan_trigger: bool = False
|
||||||
|
|
||||||
# --- 修改:移除 __post_init__ 的参数 ---
|
# --- 修改:移除 __post_init__ 的参数 ---
|
||||||
|
def __init__(self):
|
||||||
|
self.chat_observer = None
|
||||||
|
self.chat_observer = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""初始化后创建handler并进行必要的设置"""
|
"""初始化后创建handler并进行必要的设置"""
|
||||||
self.chat_observer: Optional[ChatObserver] = None # 添加类型提示
|
self.chat_observer: Optional[ChatObserver] = None # 添加类型提示
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,26 @@ if TYPE_CHECKING:
|
||||||
logger = get_module_logger("pfc")
|
logger = get_module_logger("pfc")
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_similarity(goal1: str, goal2: str) -> float:
|
||||||
|
"""简单计算两个目标之间的相似度
|
||||||
|
|
||||||
|
这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法
|
||||||
|
|
||||||
|
Args:
|
||||||
|
goal1: 第一个目标
|
||||||
|
goal2: 第二个目标
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: 相似度得分 (0-1)
|
||||||
|
"""
|
||||||
|
# 简单实现:检查重叠字数比例
|
||||||
|
words1 = set(goal1)
|
||||||
|
words2 = set(goal2)
|
||||||
|
overlap = len(words1.intersection(words2))
|
||||||
|
total = len(words1.union(words2))
|
||||||
|
return overlap / total if total > 0 else 0
|
||||||
|
|
||||||
|
|
||||||
class GoalAnalyzer:
|
class GoalAnalyzer:
|
||||||
"""对话目标分析器"""
|
"""对话目标分析器"""
|
||||||
|
|
||||||
|
|
@ -147,14 +167,14 @@ class GoalAnalyzer:
|
||||||
# 返回第一个目标作为当前主要目标(如果有)
|
# 返回第一个目标作为当前主要目标(如果有)
|
||||||
if result:
|
if result:
|
||||||
first_goal = result[0]
|
first_goal = result[0]
|
||||||
return (first_goal.get("goal", ""), "", first_goal.get("reasoning", ""))
|
return first_goal.get("goal", ""), "", first_goal.get("reasoning", "")
|
||||||
else:
|
else:
|
||||||
# 单个目标的情况
|
# 单个目标的情况
|
||||||
conversation_info.goal_list.append(result)
|
conversation_info.goal_list.append(result)
|
||||||
return (goal, "", reasoning)
|
return goal, "", reasoning
|
||||||
|
|
||||||
# 如果解析失败,返回默认值
|
# 如果解析失败,返回默认值
|
||||||
return ("", "", "")
|
return "", "", ""
|
||||||
|
|
||||||
async def _update_goals(self, new_goal: str, method: str, reasoning: str):
|
async def _update_goals(self, new_goal: str, method: str, reasoning: str):
|
||||||
"""更新目标列表
|
"""更新目标列表
|
||||||
|
|
@ -166,7 +186,7 @@ class GoalAnalyzer:
|
||||||
"""
|
"""
|
||||||
# 检查新目标是否与现有目标相似
|
# 检查新目标是否与现有目标相似
|
||||||
for i, (existing_goal, _, _) in enumerate(self.goals):
|
for i, (existing_goal, _, _) in enumerate(self.goals):
|
||||||
if self._calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值
|
if _calculate_similarity(new_goal, existing_goal) > 0.7: # 相似度阈值
|
||||||
# 更新现有目标
|
# 更新现有目标
|
||||||
self.goals[i] = (new_goal, method, reasoning)
|
self.goals[i] = (new_goal, method, reasoning)
|
||||||
# 将此目标移到列表前面(最主要的位置)
|
# 将此目标移到列表前面(最主要的位置)
|
||||||
|
|
@ -180,25 +200,6 @@ class GoalAnalyzer:
|
||||||
if len(self.goals) > self.max_goals:
|
if len(self.goals) > self.max_goals:
|
||||||
self.goals.pop() # 移除最老的目标
|
self.goals.pop() # 移除最老的目标
|
||||||
|
|
||||||
def _calculate_similarity(self, goal1: str, goal2: str) -> float:
|
|
||||||
"""简单计算两个目标之间的相似度
|
|
||||||
|
|
||||||
这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法
|
|
||||||
|
|
||||||
Args:
|
|
||||||
goal1: 第一个目标
|
|
||||||
goal2: 第二个目标
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 相似度得分 (0-1)
|
|
||||||
"""
|
|
||||||
# 简单实现:检查重叠字数比例
|
|
||||||
words1 = set(goal1)
|
|
||||||
words2 = set(goal2)
|
|
||||||
overlap = len(words1.intersection(words2))
|
|
||||||
total = len(words1.union(words2))
|
|
||||||
return overlap / total if total > 0 else 0
|
|
||||||
|
|
||||||
async def get_all_goals(self) -> List[Tuple[str, str, str]]:
|
async def get_all_goals(self) -> List[Tuple[str, str, str]]:
|
||||||
"""获取所有当前目标
|
"""获取所有当前目标
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class PFCManager:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
stream_id: 聊天流ID
|
stream_id: 聊天流ID
|
||||||
|
private_name: 私聊名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[Conversation]: 对话实例,创建失败则返回None
|
Optional[Conversation]: 对话实例,创建失败则返回None
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ def get_items_from_json(
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: 包含JSON的文本
|
content: 包含JSON的文本
|
||||||
|
private_name: 私聊名称
|
||||||
*items: 要提取的字段名
|
*items: 要提取的字段名
|
||||||
default_values: 字段的默认值,格式为 {字段名: 默认值}
|
default_values: 字段的默认值,格式为 {字段名: 默认值}
|
||||||
required_types: 字段的必需类型,格式为 {字段名: 类型}
|
required_types: 字段的必需类型,格式为 {字段名: 类型}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ class ReplyChecker:
|
||||||
Args:
|
Args:
|
||||||
reply: 生成的回复
|
reply: 生成的回复
|
||||||
goal: 对话目标
|
goal: 对话目标
|
||||||
|
chat_history: 对话历史记录
|
||||||
|
chat_history_text: 对话历史记录文本
|
||||||
retry_count: 当前重试次数
|
retry_count: 当前重试次数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
from ..moods.moods import MoodManager # 导入情绪管理器
|
from ..moods.moods import MoodManager # 导入情绪管理器
|
||||||
from ...config.config import global_config
|
from ...config.config import global_config
|
||||||
from .message import MessageRecv
|
from .message import MessageRecv
|
||||||
|
|
@ -46,7 +48,7 @@ class ChatBot:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"创建PFC聊天失败: {e}")
|
logger.error(f"创建PFC聊天失败: {e}")
|
||||||
|
|
||||||
async def message_process(self, message_data: str) -> None:
|
async def message_process(self, message_data: Dict[str, Any]) -> None:
|
||||||
"""处理转化后的统一格式消息
|
"""处理转化后的统一格式消息
|
||||||
这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中
|
这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中
|
||||||
heart_flow模式:使用思维流系统进行回复
|
heart_flow模式:使用思维流系统进行回复
|
||||||
|
|
@ -82,7 +84,7 @@ class ChatBot:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 群聊黑名单拦截
|
# 群聊黑名单拦截
|
||||||
if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups:
|
if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups:
|
||||||
logger.trace(f"群{groupinfo.group_id}被禁止回复")
|
logger.trace(f"群{groupinfo.group_id}被禁止回复")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import time
|
import time
|
||||||
|
from abc import abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Optional, Any
|
||||||
|
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
|
@ -58,12 +59,37 @@ class Message(MessageBase):
|
||||||
# 回复消息
|
# 回复消息
|
||||||
self.reply = reply
|
self.reply = reply
|
||||||
|
|
||||||
|
async def _process_message_segments(self, segment: Seg) -> str:
|
||||||
|
"""递归处理消息段,转换为文字描述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
segment: 要处理的消息段
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 处理后的文本
|
||||||
|
"""
|
||||||
|
if segment.type == "seglist":
|
||||||
|
# 处理消息段列表
|
||||||
|
segments_text = []
|
||||||
|
for seg in segment.data:
|
||||||
|
processed = await self._process_message_segments(seg)
|
||||||
|
if processed:
|
||||||
|
segments_text.append(processed)
|
||||||
|
return " ".join(segments_text)
|
||||||
|
else:
|
||||||
|
# 处理单个消息段
|
||||||
|
return await self._process_single_segment(segment)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def _process_single_segment(self, segment):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MessageRecv(Message):
|
class MessageRecv(Message):
|
||||||
"""接收消息类,用于处理从MessageCQ序列化的消息"""
|
"""接收消息类,用于处理从MessageCQ序列化的消息"""
|
||||||
|
|
||||||
def __init__(self, message_dict: Dict):
|
def __init__(self, message_dict: dict[str, Any]):
|
||||||
"""从MessageCQ的字典初始化
|
"""从MessageCQ的字典初始化
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -90,27 +116,6 @@ class MessageRecv(Message):
|
||||||
self.processed_plain_text = await self._process_message_segments(self.message_segment)
|
self.processed_plain_text = await self._process_message_segments(self.message_segment)
|
||||||
self.detailed_plain_text = self._generate_detailed_text()
|
self.detailed_plain_text = self._generate_detailed_text()
|
||||||
|
|
||||||
async def _process_message_segments(self, segment: Seg) -> str:
|
|
||||||
"""递归处理消息段,转换为文字描述
|
|
||||||
|
|
||||||
Args:
|
|
||||||
segment: 要处理的消息段
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 处理后的文本
|
|
||||||
"""
|
|
||||||
if segment.type == "seglist":
|
|
||||||
# 处理消息段列表
|
|
||||||
segments_text = []
|
|
||||||
for seg in segment.data:
|
|
||||||
processed = await self._process_message_segments(seg)
|
|
||||||
if processed:
|
|
||||||
segments_text.append(processed)
|
|
||||||
return " ".join(segments_text)
|
|
||||||
else:
|
|
||||||
# 处理单个消息段
|
|
||||||
return await self._process_single_segment(segment)
|
|
||||||
|
|
||||||
async def _process_single_segment(self, seg: Seg) -> str:
|
async def _process_single_segment(self, seg: Seg) -> str:
|
||||||
"""处理单个消息段
|
"""处理单个消息段
|
||||||
|
|
||||||
|
|
@ -179,28 +184,7 @@ class MessageProcessBase(Message):
|
||||||
self.thinking_time = round(time.time() - self.thinking_start_time, 2)
|
self.thinking_time = round(time.time() - self.thinking_start_time, 2)
|
||||||
return self.thinking_time
|
return self.thinking_time
|
||||||
|
|
||||||
async def _process_message_segments(self, segment: Seg) -> str:
|
async def _process_single_segment(self, seg: Seg) -> str | None:
|
||||||
"""递归处理消息段,转换为文字描述
|
|
||||||
|
|
||||||
Args:
|
|
||||||
segment: 要处理的消息段
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 处理后的文本
|
|
||||||
"""
|
|
||||||
if segment.type == "seglist":
|
|
||||||
# 处理消息段列表
|
|
||||||
segments_text = []
|
|
||||||
for seg in segment.data:
|
|
||||||
processed = await self._process_message_segments(seg)
|
|
||||||
if processed:
|
|
||||||
segments_text.append(processed)
|
|
||||||
return " ".join(segments_text)
|
|
||||||
else:
|
|
||||||
# 处理单个消息段
|
|
||||||
return await self._process_single_segment(segment)
|
|
||||||
|
|
||||||
async def _process_single_segment(self, seg: Seg) -> Union[str, None]:
|
|
||||||
"""处理单个消息段
|
"""处理单个消息段
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -278,7 +262,7 @@ class MessageSending(MessageProcessBase):
|
||||||
message_id: str,
|
message_id: str,
|
||||||
chat_stream: ChatStream,
|
chat_stream: ChatStream,
|
||||||
bot_user_info: UserInfo,
|
bot_user_info: UserInfo,
|
||||||
sender_info: UserInfo, # 用来记录发送者信息,用于私聊回复
|
sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复
|
||||||
message_segment: Seg,
|
message_segment: Seg,
|
||||||
reply: Optional["MessageRecv"] = None,
|
reply: Optional["MessageRecv"] = None,
|
||||||
is_head: bool = False,
|
is_head: bool = False,
|
||||||
|
|
@ -303,7 +287,7 @@ class MessageSending(MessageProcessBase):
|
||||||
self.is_emoji = is_emoji
|
self.is_emoji = is_emoji
|
||||||
self.apply_set_reply_logic = apply_set_reply_logic
|
self.apply_set_reply_logic = apply_set_reply_logic
|
||||||
|
|
||||||
def set_reply(self, reply: Optional["MessageRecv"] = None) -> None:
|
def set_reply(self, reply: Optional["MessageRecv"] = None):
|
||||||
"""设置回复消息"""
|
"""设置回复消息"""
|
||||||
if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format:
|
if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format:
|
||||||
if reply:
|
if reply:
|
||||||
|
|
@ -317,7 +301,6 @@ class MessageSending(MessageProcessBase):
|
||||||
self.message_segment,
|
self.message_segment,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return self
|
|
||||||
|
|
||||||
async def process(self) -> None:
|
async def process(self) -> None:
|
||||||
"""处理消息内容,生成纯文本和详细文本"""
|
"""处理消息内容,生成纯文本和详细文本"""
|
||||||
|
|
@ -342,6 +325,7 @@ class MessageSending(MessageProcessBase):
|
||||||
reply=thinking.reply,
|
reply=thinking.reply,
|
||||||
is_head=is_head,
|
is_head=is_head,
|
||||||
is_emoji=is_emoji,
|
is_emoji=is_emoji,
|
||||||
|
sender_info=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
@ -361,7 +345,7 @@ class MessageSet:
|
||||||
def __init__(self, chat_stream: ChatStream, message_id: str):
|
def __init__(self, chat_stream: ChatStream, message_id: str):
|
||||||
self.chat_stream = chat_stream
|
self.chat_stream = chat_stream
|
||||||
self.message_id = message_id
|
self.message_id = message_id
|
||||||
self.messages: List[MessageSending] = []
|
self.messages: list[MessageSending] = []
|
||||||
self.time = round(time.time(), 3) # 保留3位小数
|
self.time = round(time.time(), 3) # 保留3位小数
|
||||||
|
|
||||||
def add_message(self, message: MessageSending) -> None:
|
def add_message(self, message: MessageSending) -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
# src/plugins/chat/message_sender.py
|
# src/plugins/chat/message_sender.py
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Union
|
||||||
|
|
||||||
# from ...common.database import db # 数据库依赖似乎不需要了,注释掉
|
# from ...common.database import db # 数据库依赖似乎不需要了,注释掉
|
||||||
from ..message.api import global_api
|
|
||||||
from .message import MessageSending, MessageThinking, MessageSet
|
from .message import MessageSending, MessageThinking, MessageSet
|
||||||
|
|
||||||
from ..storage.storage import MessageStorage
|
from ..storage.storage import MessageStorage
|
||||||
|
|
@ -17,6 +16,40 @@ from src.common.logger_manager import get_logger
|
||||||
logger = get_logger("sender")
|
logger = get_logger("sender")
|
||||||
|
|
||||||
|
|
||||||
|
async def send_via_ws(message: MessageSending) -> None:
|
||||||
|
"""通过 WebSocket 发送消息"""
|
||||||
|
try:
|
||||||
|
await send_message(message)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"WS发送失败: {e}")
|
||||||
|
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||||
|
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
message: MessageSending,
|
||||||
|
) -> None:
|
||||||
|
"""发送消息(核心发送逻辑)"""
|
||||||
|
|
||||||
|
# --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) ---
|
||||||
|
typing_time = calculate_typing_time(
|
||||||
|
input_string=message.processed_plain_text,
|
||||||
|
thinking_start_time=message.thinking_start_time,
|
||||||
|
is_emoji=message.is_emoji,
|
||||||
|
)
|
||||||
|
# logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志
|
||||||
|
await asyncio.sleep(typing_time)
|
||||||
|
# logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志
|
||||||
|
# --- 结束打字延迟 ---
|
||||||
|
|
||||||
|
message_preview = truncate_message(message.processed_plain_text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await send_via_ws(message)
|
||||||
|
logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
class MessageSender:
|
class MessageSender:
|
||||||
"""发送器 (不再是单例)"""
|
"""发送器 (不再是单例)"""
|
||||||
|
|
||||||
|
|
@ -29,39 +62,6 @@ class MessageSender:
|
||||||
"""设置当前bot实例"""
|
"""设置当前bot实例"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def send_via_ws(self, message: MessageSending) -> None:
|
|
||||||
"""通过 WebSocket 发送消息"""
|
|
||||||
try:
|
|
||||||
await global_api.send_message(message)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"WS发送失败: {e}")
|
|
||||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
|
||||||
|
|
||||||
async def send_message(
|
|
||||||
self,
|
|
||||||
message: MessageSending,
|
|
||||||
) -> None:
|
|
||||||
"""发送消息(核心发送逻辑)"""
|
|
||||||
|
|
||||||
# --- 添加计算打字和延迟的逻辑 (从 heartflow_message_sender 移动并调整) ---
|
|
||||||
typing_time = calculate_typing_time(
|
|
||||||
input_string=message.processed_plain_text,
|
|
||||||
thinking_start_time=message.thinking_start_time,
|
|
||||||
is_emoji=message.is_emoji,
|
|
||||||
)
|
|
||||||
# logger.trace(f"{message.processed_plain_text},{typing_time},计算输入时间结束") # 减少日志
|
|
||||||
await asyncio.sleep(typing_time)
|
|
||||||
# logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志
|
|
||||||
# --- 结束打字延迟 ---
|
|
||||||
|
|
||||||
message_preview = truncate_message(message.processed_plain_text)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.send_via_ws(message)
|
|
||||||
logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
class MessageContainer:
|
class MessageContainer:
|
||||||
"""单个聊天流的发送/思考消息容器"""
|
"""单个聊天流的发送/思考消息容器"""
|
||||||
|
|
@ -69,7 +69,7 @@ class MessageContainer:
|
||||||
def __init__(self, chat_id: str, max_size: int = 100):
|
def __init__(self, chat_id: str, max_size: int = 100):
|
||||||
self.chat_id = chat_id
|
self.chat_id = chat_id
|
||||||
self.max_size = max_size
|
self.max_size = max_size
|
||||||
self.messages: List[Union[MessageThinking, MessageSending]] = [] # 明确类型
|
self.messages: list[MessageThinking | MessageSending] = [] # 明确类型
|
||||||
self.last_send_time = 0
|
self.last_send_time = 0
|
||||||
self.thinking_wait_timeout = 20 # 思考等待超时时间(秒) - 从旧 sender 合并
|
self.thinking_wait_timeout = 20 # 思考等待超时时间(秒) - 从旧 sender 合并
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ class MessageContainer:
|
||||||
"""计算当前容器中思考消息的数量"""
|
"""计算当前容器中思考消息的数量"""
|
||||||
return sum(1 for msg in self.messages if isinstance(msg, MessageThinking))
|
return sum(1 for msg in self.messages if isinstance(msg, MessageThinking))
|
||||||
|
|
||||||
def get_timeout_sending_messages(self) -> List[MessageSending]:
|
def get_timeout_sending_messages(self) -> list[MessageSending]:
|
||||||
"""获取所有超时的MessageSending对象(思考时间超过20秒),按thinking_start_time排序 - 从旧 sender 合并"""
|
"""获取所有超时的MessageSending对象(思考时间超过20秒),按thinking_start_time排序 - 从旧 sender 合并"""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
timeout_messages = []
|
timeout_messages = []
|
||||||
|
|
@ -93,7 +93,7 @@ class MessageContainer:
|
||||||
timeout_messages.sort(key=lambda x: x.thinking_start_time)
|
timeout_messages.sort(key=lambda x: x.thinking_start_time)
|
||||||
return timeout_messages
|
return timeout_messages
|
||||||
|
|
||||||
def get_earliest_message(self) -> Optional[Union[MessageThinking, MessageSending]]:
|
def get_earliest_message(self):
|
||||||
"""获取thinking_start_time最早的消息对象"""
|
"""获取thinking_start_time最早的消息对象"""
|
||||||
if not self.messages:
|
if not self.messages:
|
||||||
return None
|
return None
|
||||||
|
|
@ -107,7 +107,7 @@ class MessageContainer:
|
||||||
earliest_message = msg
|
earliest_message = msg
|
||||||
return earliest_message
|
return earliest_message
|
||||||
|
|
||||||
def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]) -> None:
|
def add_message(self, message: Union[MessageThinking, MessageSending, MessageSet]):
|
||||||
"""添加消息到队列"""
|
"""添加消息到队列"""
|
||||||
if isinstance(message, MessageSet):
|
if isinstance(message, MessageSet):
|
||||||
for single_message in message.messages:
|
for single_message in message.messages:
|
||||||
|
|
@ -115,11 +115,11 @@ class MessageContainer:
|
||||||
else:
|
else:
|
||||||
self.messages.append(message)
|
self.messages.append(message)
|
||||||
|
|
||||||
def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]) -> bool:
|
def remove_message(self, message_to_remove: Union[MessageThinking, MessageSending]):
|
||||||
"""移除指定的消息对象,如果消息存在则返回True,否则返回False"""
|
"""移除指定的消息对象,如果消息存在则返回True,否则返回False"""
|
||||||
try:
|
try:
|
||||||
_initial_len = len(self.messages)
|
_initial_len = len(self.messages)
|
||||||
# 使用列表推导式或 filter 创建新列表,排除要删除的元素
|
# 使用列表推导式或 message_filter 创建新列表,排除要删除的元素
|
||||||
# self.messages = [msg for msg in self.messages if msg is not message_to_remove]
|
# self.messages = [msg for msg in self.messages if msg is not message_to_remove]
|
||||||
# 或者直接 remove (如果确定对象唯一性)
|
# 或者直接 remove (如果确定对象唯一性)
|
||||||
if message_to_remove in self.messages:
|
if message_to_remove in self.messages:
|
||||||
|
|
@ -137,7 +137,7 @@ class MessageContainer:
|
||||||
"""检查是否有待发送的消息"""
|
"""检查是否有待发送的消息"""
|
||||||
return bool(self.messages)
|
return bool(self.messages)
|
||||||
|
|
||||||
def get_all_messages(self) -> List[Union[MessageSending, MessageThinking]]:
|
def get_all_messages(self) -> list[MessageThinking | MessageSending]:
|
||||||
"""获取所有消息"""
|
"""获取所有消息"""
|
||||||
return list(self.messages) # 返回副本
|
return list(self.messages) # 返回副本
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ class MessageManager:
|
||||||
"""管理所有聊天流的消息容器 (不再是单例)"""
|
"""管理所有聊天流的消息容器 (不再是单例)"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.containers: Dict[str, MessageContainer] = {}
|
self.containers: dict[str, MessageContainer] = {}
|
||||||
self.storage = MessageStorage() # 添加 storage 实例
|
self.storage = MessageStorage() # 添加 storage 实例
|
||||||
self._running = True # 处理器运行状态
|
self._running = True # 处理器运行状态
|
||||||
self._container_lock = asyncio.Lock() # 保护 containers 字典的锁
|
self._container_lock = asyncio.Lock() # 保护 containers 字典的锁
|
||||||
|
|
@ -226,7 +226,7 @@ class MessageManager:
|
||||||
await message.process() # 预处理消息内容
|
await message.process() # 预处理消息内容
|
||||||
|
|
||||||
# 使用全局 message_sender 实例
|
# 使用全局 message_sender 实例
|
||||||
await message_sender.send_message(message)
|
await send_message(message)
|
||||||
await self.storage.store_message(message, message.chat_stream)
|
await self.storage.store_message(message, message.chat_stream)
|
||||||
|
|
||||||
# 移除消息要在发送 *之后*
|
# 移除消息要在发送 *之后*
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import random
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
import jieba
|
import jieba
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
@ -26,7 +25,7 @@ def is_english_letter(char: str) -> bool:
|
||||||
return "a" <= char.lower() <= "z"
|
return "a" <= char.lower() <= "z"
|
||||||
|
|
||||||
|
|
||||||
def db_message_to_str(message_dict: Dict) -> str:
|
def db_message_to_str(message_dict: dict) -> str:
|
||||||
logger.debug(f"message_dict: {message_dict}")
|
logger.debug(f"message_dict: {message_dict}")
|
||||||
time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message_dict["time"]))
|
time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(message_dict["time"]))
|
||||||
try:
|
try:
|
||||||
|
|
@ -77,13 +76,13 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
|
||||||
if not is_mentioned:
|
if not is_mentioned:
|
||||||
# 判断是否被回复
|
# 判断是否被回复
|
||||||
if re.match(
|
if re.match(
|
||||||
f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?\],说:", message.processed_plain_text
|
f"\[回复 [\s\S]*?\({str(global_config.BOT_QQ)}\):[\s\S]*?],说:", message.processed_plain_text
|
||||||
):
|
):
|
||||||
is_mentioned = True
|
is_mentioned = True
|
||||||
else:
|
else:
|
||||||
# 判断内容中是否被提及
|
# 判断内容中是否被提及
|
||||||
message_content = re.sub(r"@[\s\S]*?((\d+))", "", message.processed_plain_text)
|
message_content = re.sub(r"@[\s\S]*?((\d+))", "", message.processed_plain_text)
|
||||||
message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?\],说:", "", message_content)
|
message_content = re.sub(r"\[回复 [\s\S]*?\(((\d+)|未知id)\):[\s\S]*?],说:", "", message_content)
|
||||||
for keyword in keywords:
|
for keyword in keywords:
|
||||||
if keyword in message_content:
|
if keyword in message_content:
|
||||||
is_mentioned = True
|
is_mentioned = True
|
||||||
|
|
@ -157,7 +156,7 @@ async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list:
|
||||||
return message_objects
|
return message_objects
|
||||||
|
|
||||||
|
|
||||||
def get_recent_group_detailed_plain_text(chat_stream_id: int, limit: int = 12, combine=False):
|
def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False):
|
||||||
recent_messages = list(
|
recent_messages = list(
|
||||||
db.messages.find(
|
db.messages.find(
|
||||||
{"chat_id": chat_stream_id},
|
{"chat_id": chat_stream_id},
|
||||||
|
|
@ -223,7 +222,7 @@ def get_recent_group_speaker(chat_stream_id: int, sender, limit: int = 12) -> li
|
||||||
return who_chat_in_group
|
return who_chat_in_group
|
||||||
|
|
||||||
|
|
||||||
def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
|
def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
|
||||||
"""将文本分割成句子,并根据概率合并
|
"""将文本分割成句子,并根据概率合并
|
||||||
1. 识别分割点(, , 。 ; 空格),但如果分割点左右都是英文字母则不分割。
|
1. 识别分割点(, , 。 ; 空格),但如果分割点左右都是英文字母则不分割。
|
||||||
2. 将文本分割成 (内容, 分隔符) 的元组。
|
2. 将文本分割成 (内容, 分隔符) 的元组。
|
||||||
|
|
@ -263,7 +262,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
|
||||||
if char in separators:
|
if char in separators:
|
||||||
# 检查分割条件:如果分隔符左右都是英文字母,则不分割
|
# 检查分割条件:如果分隔符左右都是英文字母,则不分割
|
||||||
can_split = True
|
can_split = True
|
||||||
if i > 0 and i < len(text) - 1:
|
if 0 < i < len(text) - 1:
|
||||||
prev_char = text[i - 1]
|
prev_char = text[i - 1]
|
||||||
next_char = text[i + 1]
|
next_char = text[i + 1]
|
||||||
# if is_english_letter(prev_char) and is_english_letter(next_char) and char == ' ': # 原计划只对空格应用此规则,现应用于所有分隔符
|
# if is_english_letter(prev_char) and is_english_letter(next_char) and char == ' ': # 原计划只对空格应用此规则,现应用于所有分隔符
|
||||||
|
|
@ -370,7 +369,7 @@ def random_remove_punctuation(text: str) -> str:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def process_llm_response(text: str) -> List[str]:
|
def process_llm_response(text: str) -> list[str]:
|
||||||
# 先保护颜文字
|
# 先保护颜文字
|
||||||
if global_config.enable_kaomoji_protection:
|
if global_config.enable_kaomoji_protection:
|
||||||
protected_text, kaomoji_mapping = protect_kaomoji(text)
|
protected_text, kaomoji_mapping = protect_kaomoji(text)
|
||||||
|
|
@ -379,7 +378,7 @@ def process_llm_response(text: str) -> List[str]:
|
||||||
protected_text = text
|
protected_text = text
|
||||||
kaomoji_mapping = {}
|
kaomoji_mapping = {}
|
||||||
# 提取被 () 或 [] 包裹且包含中文的内容
|
# 提取被 () 或 [] 包裹且包含中文的内容
|
||||||
pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]")
|
pattern = re.compile(r"[(\[(](?=.*[一-鿿]).*?[)\])]")
|
||||||
# _extracted_contents = pattern.findall(text)
|
# _extracted_contents = pattern.findall(text)
|
||||||
_extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
|
_extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
|
||||||
# 去除 () 和 [] 及其包裹的内容
|
# 去除 () 和 [] 及其包裹的内容
|
||||||
|
|
@ -554,7 +553,7 @@ def protect_kaomoji(sentence):
|
||||||
r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配)
|
r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配)
|
||||||
r"[^一-龥a-zA-Z0-9\s]" # 非中文、非英文、非数字、非空格字符(必须包含至少一个)
|
r"[^一-龥a-zA-Z0-9\s]" # 非中文、非英文、非数字、非空格字符(必须包含至少一个)
|
||||||
r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配)
|
r"[^()\[\]()【】]*?" # 非括号字符(惰性匹配)
|
||||||
r"[\)\])】" # 右括号
|
r"[)\])】" # 右括号
|
||||||
r"]"
|
r"]"
|
||||||
r")"
|
r")"
|
||||||
r"|"
|
r"|"
|
||||||
|
|
@ -704,7 +703,7 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) -
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
|
|
||||||
def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> Optional[str]:
|
def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal") -> str:
|
||||||
"""将时间戳转换为人类可读的时间格式
|
"""将时间戳转换为人类可读的时间格式
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -732,10 +731,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
|
||||||
return f"{int(diff / 86400)}天前:\n"
|
return f"{int(diff / 86400)}天前:\n"
|
||||||
else:
|
else:
|
||||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n"
|
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n"
|
||||||
elif mode == "lite":
|
else: # mode = "lite" or unknown
|
||||||
# 只返回时分秒格式,喵~
|
# 只返回时分秒格式,喵~
|
||||||
return time.strftime("%H:%M:%S", time.localtime(timestamp))
|
return time.strftime("%H:%M:%S", time.localtime(timestamp))
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_text_timestamps(text: str, mode: str = "normal") -> str:
|
def parse_text_timestamps(text: str, mode: str = "normal") -> str:
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ from ..chat.utils_image import image_path_to_base64, image_manager
|
||||||
from ..models.utils_model import LLMRequest
|
from ..models.utils_model import LLMRequest
|
||||||
from src.common.logger_manager import get_logger
|
from src.common.logger_manager import get_logger
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("emoji")
|
logger = get_logger("emoji")
|
||||||
|
|
||||||
BASE_DIR = os.path.join("data")
|
BASE_DIR = os.path.join("data")
|
||||||
|
|
@ -24,7 +23,6 @@ EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录
|
||||||
EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录
|
EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录
|
||||||
MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换的 prompt 中
|
MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换的 prompt 中
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
还没经过测试,有些地方数据库和内存数据同步可能不完全
|
还没经过测试,有些地方数据库和内存数据同步可能不完全
|
||||||
|
|
||||||
|
|
@ -34,9 +32,12 @@ MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换
|
||||||
class MaiEmoji:
|
class MaiEmoji:
|
||||||
"""定义一个表情包"""
|
"""定义一个表情包"""
|
||||||
|
|
||||||
def __init__(self, filename: str, path: str):
|
def __init__(self, full_path: str):
|
||||||
self.path = path # 存储目录路径
|
if not full_path:
|
||||||
self.filename = filename
|
raise ValueError("full_path cannot be empty")
|
||||||
|
self.full_path = full_path # 文件的完整路径 (包括文件名)
|
||||||
|
self.path = os.path.dirname(full_path) # 文件所在的目录路径
|
||||||
|
self.filename = os.path.basename(full_path) # 文件名
|
||||||
self.embedding = []
|
self.embedding = []
|
||||||
self.hash = "" # 初始为空,在创建实例时会计算
|
self.hash = "" # 初始为空,在创建实例时会计算
|
||||||
self.description = ""
|
self.description = ""
|
||||||
|
|
@ -48,35 +49,56 @@ class MaiEmoji:
|
||||||
self.format = ""
|
self.format = ""
|
||||||
|
|
||||||
async def initialize_hash_format(self):
|
async def initialize_hash_format(self):
|
||||||
"""从文件创建表情包实例
|
"""从文件创建表情包实例, 计算哈希值和格式"""
|
||||||
|
|
||||||
参数:
|
|
||||||
file_path: 文件的完整路径
|
|
||||||
|
|
||||||
返回:
|
|
||||||
MaiEmoji: 创建的表情包实例,如果失败则返回None
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
file_path = os.path.join(self.path, self.filename)
|
# 使用 full_path 检查文件是否存在
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(self.full_path):
|
||||||
logger.error(f"[错误] 表情包文件不存在: {file_path}")
|
logger.error(f"[初始化错误] 表情包文件不存在: {self.full_path}")
|
||||||
|
self.is_deleted = True
|
||||||
return None
|
return None
|
||||||
|
|
||||||
image_base64 = image_path_to_base64(file_path)
|
# 使用 full_path 读取文件
|
||||||
|
logger.debug(f"[初始化] 正在读取文件: {self.full_path}")
|
||||||
|
image_base64 = image_path_to_base64(self.full_path)
|
||||||
if image_base64 is None:
|
if image_base64 is None:
|
||||||
logger.error(f"[错误] 无法读取图片: {file_path}")
|
logger.error(f"[初始化错误] 无法读取或转换Base64: {self.full_path}")
|
||||||
|
self.is_deleted = True
|
||||||
return None
|
return None
|
||||||
|
logger.debug(f"[初始化] 文件读取成功 (Base64预览: {image_base64[:50]}...)")
|
||||||
|
|
||||||
# 计算哈希值
|
# 计算哈希值
|
||||||
|
logger.debug(f"[初始化] 正在解码Base64并计算哈希: {self.filename}")
|
||||||
image_bytes = base64.b64decode(image_base64)
|
image_bytes = base64.b64decode(image_base64)
|
||||||
self.hash = hashlib.md5(image_bytes).hexdigest()
|
self.hash = hashlib.md5(image_bytes).hexdigest()
|
||||||
|
logger.debug(f"[初始化] 哈希计算成功: {self.hash}")
|
||||||
|
|
||||||
# 获取图片格式
|
# 获取图片格式
|
||||||
self.format = Image.open(io.BytesIO(image_bytes)).format.lower()
|
logger.debug(f"[初始化] 正在使用Pillow获取格式: {self.filename}")
|
||||||
|
try:
|
||||||
|
with Image.open(io.BytesIO(image_bytes)) as img:
|
||||||
|
self.format = img.format.lower()
|
||||||
|
logger.debug(f"[初始化] 格式获取成功: {self.format}")
|
||||||
|
except Exception as pil_error:
|
||||||
|
logger.error(f"[初始化错误] Pillow无法处理图片 ({self.filename}): {pil_error}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
self.is_deleted = True
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 如果所有步骤成功,返回 True
|
||||||
|
return True
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}")
|
||||||
|
self.is_deleted = True
|
||||||
|
return None
|
||||||
|
except base64.binascii.Error as b64_error:
|
||||||
|
logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}")
|
||||||
|
self.is_deleted = True
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 初始化表情包失败: {str(e)}")
|
logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
self.is_deleted = True
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def register_to_db(self):
|
async def register_to_db(self):
|
||||||
|
|
@ -87,44 +109,47 @@ class MaiEmoji:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 确保目标目录存在
|
# 确保目标目录存在
|
||||||
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
# 源路径是当前实例的完整路径
|
# 源路径是当前实例的完整路径 self.full_path
|
||||||
source_path = os.path.join(self.path, self.filename)
|
source_full_path = self.full_path
|
||||||
# 目标路径
|
# 目标完整路径
|
||||||
destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
|
destination_full_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
|
||||||
|
|
||||||
# 检查源文件是否存在
|
# 检查源文件是否存在
|
||||||
if not os.path.exists(source_path):
|
if not os.path.exists(source_full_path):
|
||||||
logger.error(f"[错误] 源文件不存在: {source_path}")
|
logger.error(f"[错误] 源文件不存在: {source_full_path}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# --- 文件移动 ---
|
# --- 文件移动 ---
|
||||||
try:
|
try:
|
||||||
# 如果目标文件已存在,先删除 (确保移动成功)
|
# 如果目标文件已存在,先删除 (确保移动成功)
|
||||||
if os.path.exists(destination_path):
|
if os.path.exists(destination_full_path):
|
||||||
os.remove(destination_path)
|
os.remove(destination_full_path)
|
||||||
|
|
||||||
os.rename(source_path, destination_path)
|
os.rename(source_full_path, destination_full_path)
|
||||||
logger.debug(f"[移动] 文件从 {source_path} 移动到 {destination_path}")
|
logger.debug(f"[移动] 文件从 {source_full_path} 移动到 {destination_full_path}")
|
||||||
# 更新实例的路径属性为新目录
|
# 更新实例的路径属性为新路径
|
||||||
|
self.full_path = destination_full_path
|
||||||
self.path = EMOJI_REGISTED_DIR
|
self.path = EMOJI_REGISTED_DIR
|
||||||
|
# self.filename 保持不变
|
||||||
except Exception as move_error:
|
except Exception as move_error:
|
||||||
logger.error(f"[错误] 移动文件失败: {str(move_error)}")
|
logger.error(f"[错误] 移动文件失败: {str(move_error)}")
|
||||||
return False # 文件移动失败,不继续
|
# 如果移动失败,尝试将实例状态恢复?暂时不处理,仅返回失败
|
||||||
|
return False
|
||||||
|
|
||||||
# --- 数据库操作 ---
|
# --- 数据库操作 ---
|
||||||
try:
|
try:
|
||||||
# 准备数据库记录 for emoji collection
|
# 准备数据库记录 for emoji collection
|
||||||
emoji_record = {
|
emoji_record = {
|
||||||
"filename": self.filename,
|
"filename": self.filename,
|
||||||
"path": os.path.join(self.path, self.filename), # 使用更新后的路径
|
"path": self.path, # 存储目录路径
|
||||||
|
"full_path": self.full_path, # 存储完整文件路径
|
||||||
"embedding": self.embedding,
|
"embedding": self.embedding,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"emotion": self.emotion, # 添加情感标签字段
|
"emotion": self.emotion,
|
||||||
"hash": self.hash,
|
"hash": self.hash,
|
||||||
"format": self.format,
|
"format": self.format,
|
||||||
"timestamp": int(self.register_time), # 使用实例的注册时间
|
"timestamp": int(self.register_time),
|
||||||
"usage_count": self.usage_count,
|
"usage_count": self.usage_count,
|
||||||
"last_used_time": self.last_used_time,
|
"last_used_time": self.last_used_time,
|
||||||
}
|
}
|
||||||
|
|
@ -132,17 +157,24 @@ class MaiEmoji:
|
||||||
# 使用upsert确保记录存在或被更新
|
# 使用upsert确保记录存在或被更新
|
||||||
db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
|
db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
|
||||||
|
|
||||||
logger.success(f"[注册] 表情包信息保存到数据库: {self.emotion}")
|
logger.success(f"[注册] 表情包信息保存到数据库: {self.filename} ({self.emotion})")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as db_error:
|
except Exception as db_error:
|
||||||
logger.error(f"[错误] 保存数据库失败: {str(db_error)}")
|
logger.error(f"[错误] 保存数据库失败 ({self.filename}): {str(db_error)}")
|
||||||
# 考虑是否需要将文件移回?为了简化,暂时只记录错误
|
# 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误
|
||||||
|
# 可以考虑在这里尝试删除已移动的文件,避免残留
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.full_path): # full_path 此时是目标路径
|
||||||
|
os.remove(self.full_path)
|
||||||
|
logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}")
|
||||||
|
except Exception as remove_error:
|
||||||
|
logger.error(f"[错误] 回滚删除文件失败: {remove_error}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 注册表情包失败: {str(e)}")
|
logger.error(f"[错误] 注册表情包失败 ({self.filename}): {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -156,33 +188,173 @@ class MaiEmoji:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. 删除文件
|
# 1. 删除文件
|
||||||
if os.path.exists(os.path.join(self.path, self.filename)):
|
file_to_delete = self.full_path
|
||||||
|
if os.path.exists(file_to_delete):
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(self.path, self.filename))
|
os.remove(file_to_delete)
|
||||||
logger.debug(f"[删除] 文件: {os.path.join(self.path, self.filename)}")
|
logger.debug(f"[删除] 文件: {file_to_delete}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}")
|
logger.error(f"[错误] 删除文件失败 {file_to_delete}: {str(e)}")
|
||||||
# 继续执行,即使文件删除失败也尝试删除数据库记录
|
# 文件删除失败,但仍然尝试删除数据库记录
|
||||||
|
|
||||||
# 2. 删除数据库记录
|
# 2. 删除数据库记录
|
||||||
result = db.emoji.delete_one({"hash": self.hash})
|
result = db.emoji.delete_one({"hash": self.hash})
|
||||||
deleted_in_db = result.deleted_count > 0
|
deleted_in_db = result.deleted_count > 0
|
||||||
|
|
||||||
if deleted_in_db:
|
if deleted_in_db:
|
||||||
logger.info(f"[删除] 表情包 {self.filename} 无对应文件,已删除")
|
logger.info(f"[删除] 表情包数据库记录 {self.filename} (Hash: {self.hash})")
|
||||||
|
|
||||||
# 3. 标记对象已被删除
|
# 3. 标记对象已被删除
|
||||||
self.is_deleted = True
|
self.is_deleted = True
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"[错误] 删除表情包记录失败: {self.hash}")
|
# 如果数据库记录删除失败,但文件可能已删除,记录一个警告
|
||||||
|
if not os.path.exists(file_to_delete):
|
||||||
|
logger.warning(
|
||||||
|
f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 删除表情包失败: {str(e)}")
|
logger.error(f"[错误] 删除表情包失败 ({self.filename}): {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _emoji_objects_to_readable_list(emoji_objects):
|
||||||
|
"""将表情包对象列表转换为可读的字符串列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
emoji_objects: MaiEmoji对象列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
list[str]: 可读的表情包信息字符串列表
|
||||||
|
"""
|
||||||
|
emoji_info_list = []
|
||||||
|
for i, emoji in enumerate(emoji_objects):
|
||||||
|
# 转换时间戳为可读时间
|
||||||
|
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time))
|
||||||
|
# 构建每个表情包的信息字符串
|
||||||
|
emoji_info = f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n"
|
||||||
|
emoji_info_list.append(emoji_info)
|
||||||
|
return emoji_info_list
|
||||||
|
|
||||||
|
|
||||||
|
def _to_emoji_objects(data):
|
||||||
|
emoji_objects = []
|
||||||
|
load_errors = 0
|
||||||
|
emoji_data_list = list(data)
|
||||||
|
|
||||||
|
for emoji_data in emoji_data_list:
|
||||||
|
full_path = emoji_data.get("full_path")
|
||||||
|
if not full_path:
|
||||||
|
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
|
||||||
|
load_errors += 1
|
||||||
|
continue # 跳过缺少 full_path 的记录
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用 full_path 初始化 MaiEmoji 对象
|
||||||
|
emoji = MaiEmoji(full_path=full_path)
|
||||||
|
|
||||||
|
# 设置从数据库加载的属性
|
||||||
|
emoji.hash = emoji_data.get("hash", "")
|
||||||
|
# 如果 hash 为空,也跳过?取决于业务逻辑
|
||||||
|
if not emoji.hash:
|
||||||
|
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
|
||||||
|
load_errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
emoji.description = emoji_data.get("description", "")
|
||||||
|
emoji.emotion = emoji_data.get("emotion", [])
|
||||||
|
emoji.usage_count = emoji_data.get("usage_count", 0)
|
||||||
|
# 优先使用 last_used_time,否则用 timestamp,最后用当前时间
|
||||||
|
last_used = emoji_data.get("last_used_time")
|
||||||
|
timestamp = emoji_data.get("timestamp")
|
||||||
|
emoji.last_used_time = (
|
||||||
|
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
|
||||||
|
)
|
||||||
|
emoji.register_time = timestamp if timestamp is not None else time.time()
|
||||||
|
emoji.format = emoji_data.get("format", "") # 加载格式
|
||||||
|
|
||||||
|
# 不需要再手动设置 path 和 filename,__init__ 会自动处理
|
||||||
|
|
||||||
|
emoji_objects.append(emoji)
|
||||||
|
|
||||||
|
except ValueError as ve: # 捕获 __init__ 可能的错误
|
||||||
|
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
|
||||||
|
load_errors += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
|
||||||
|
load_errors += 1
|
||||||
|
return emoji_objects, load_errors
|
||||||
|
return emoji_objects, load_errors
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_emoji_dir():
|
||||||
|
"""确保表情存储目录存在"""
|
||||||
|
os.makedirs(EMOJI_DIR, exist_ok=True)
|
||||||
|
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_temp_emoji():
|
||||||
|
"""清理临时表情包
|
||||||
|
清理/data/emoji和/data/image目录下的所有文件
|
||||||
|
当目录中文件数超过100时,会全部删除
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info("[清理] 开始清理缓存...")
|
||||||
|
|
||||||
|
for need_clear in (os.path.join(BASE_DIR, "emoji"), os.path.join(BASE_DIR, "image")):
|
||||||
|
if os.path.exists(need_clear):
|
||||||
|
files = os.listdir(need_clear)
|
||||||
|
# 如果文件数超过50就全部删除
|
||||||
|
if len(files) > 100:
|
||||||
|
for filename in files:
|
||||||
|
file_path = os.path.join(need_clear, filename)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
logger.debug(f"[清理] 删除: {filename}")
|
||||||
|
|
||||||
|
logger.success("[清理] 完成")
|
||||||
|
|
||||||
|
|
||||||
|
async def clean_unused_emojis(emoji_dir, emoji_objects):
|
||||||
|
"""清理指定目录中未被 emoji_objects 追踪的表情包文件"""
|
||||||
|
if not os.path.exists(emoji_dir):
|
||||||
|
logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取内存中所有有效表情包的完整路径集合
|
||||||
|
tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
|
||||||
|
cleaned_count = 0
|
||||||
|
|
||||||
|
# 遍历指定目录中的所有文件
|
||||||
|
for file_name in os.listdir(emoji_dir):
|
||||||
|
file_full_path = os.path.join(emoji_dir, file_name)
|
||||||
|
|
||||||
|
# 确保处理的是文件而不是子目录
|
||||||
|
if not os.path.isfile(file_full_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果文件不在被追踪的集合中,则删除
|
||||||
|
if file_full_path not in tracked_full_paths:
|
||||||
|
try:
|
||||||
|
os.remove(file_full_path)
|
||||||
|
logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
|
||||||
|
cleaned_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
|
||||||
|
|
||||||
|
if cleaned_count > 0:
|
||||||
|
logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
|
||||||
|
else:
|
||||||
|
logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
class EmojiManager:
|
class EmojiManager:
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
|
|
@ -193,6 +365,7 @@ class EmojiManager:
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._initialized = None
|
||||||
self._scan_task = None
|
self._scan_task = None
|
||||||
self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
|
self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
|
||||||
self.llm_emotion_judge = LLMRequest(
|
self.llm_emotion_judge = LLMRequest(
|
||||||
|
|
@ -206,22 +379,18 @@ class EmojiManager:
|
||||||
|
|
||||||
logger.info("启动表情包管理器")
|
logger.info("启动表情包管理器")
|
||||||
|
|
||||||
def _ensure_emoji_dir(self):
|
|
||||||
"""确保表情存储目录存在"""
|
|
||||||
os.makedirs(EMOJI_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""初始化数据库连接和表情目录"""
|
"""初始化数据库连接和表情目录"""
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
try:
|
try:
|
||||||
self._ensure_emoji_collection()
|
self._ensure_emoji_collection()
|
||||||
self._ensure_emoji_dir()
|
_ensure_emoji_dir()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
# 更新表情包数量
|
# 更新表情包数量
|
||||||
# 启动时执行一次完整性检查
|
# 启动时执行一次完整性检查
|
||||||
# await self.check_emoji_file_integrity()
|
# await self.check_emoji_file_integrity()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.exception("初始化表情管理器失败")
|
logger.exception(f"初始化表情管理器失败: {e}")
|
||||||
|
|
||||||
def _ensure_db(self):
|
def _ensure_db(self):
|
||||||
"""确保数据库已初始化"""
|
"""确保数据库已初始化"""
|
||||||
|
|
@ -248,12 +417,12 @@ class EmojiManager:
|
||||||
db.emoji.create_index([("embedding", "2dsphere")])
|
db.emoji.create_index([("embedding", "2dsphere")])
|
||||||
db.emoji.create_index([("filename", 1)], unique=True)
|
db.emoji.create_index([("filename", 1)], unique=True)
|
||||||
|
|
||||||
def record_usage(self, hash: str):
|
def record_usage(self, emoji_hash: str):
|
||||||
"""记录表情使用次数"""
|
"""记录表情使用次数"""
|
||||||
try:
|
try:
|
||||||
db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}})
|
db.emoji.update_one({"hash": emoji_hash}, {"$inc": {"usage_count": 1}})
|
||||||
for emoji in self.emoji_objects:
|
for emoji in self.emoji_objects:
|
||||||
if emoji.hash == hash:
|
if emoji.hash == emoji_hash:
|
||||||
emoji.usage_count += 1
|
emoji.usage_count += 1
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -265,22 +434,27 @@ class EmojiManager:
|
||||||
Args:
|
Args:
|
||||||
text_emotion: 输入的情感描述文本
|
text_emotion: 输入的情感描述文本
|
||||||
Returns:
|
Returns:
|
||||||
Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None
|
Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述),如果没有找到则返回None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._ensure_db()
|
self._ensure_db()
|
||||||
_time_start = time.time()
|
_time_start = time.time()
|
||||||
|
|
||||||
# 获取所有表情包
|
# 获取所有表情包 (从内存缓存中获取)
|
||||||
all_emojis = self.emoji_objects
|
all_emojis = self.emoji_objects
|
||||||
|
|
||||||
if not all_emojis:
|
if not all_emojis:
|
||||||
logger.warning("数据库中没有任何表情包")
|
logger.warning("内存中没有任何表情包对象")
|
||||||
|
# 可以考虑再查一次数据库?或者依赖定期任务更新
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 计算每个表情包与输入文本的最大情感相似度
|
# 计算每个表情包与输入文本的最大情感相似度
|
||||||
emoji_similarities = []
|
emoji_similarities = []
|
||||||
for emoji in all_emojis:
|
for emoji in all_emojis:
|
||||||
|
# 跳过已标记为删除的对象
|
||||||
|
if emoji.is_deleted:
|
||||||
|
continue
|
||||||
|
|
||||||
emotions = emoji.emotion
|
emotions = emoji.emotion
|
||||||
if not emotions:
|
if not emotions:
|
||||||
continue
|
continue
|
||||||
|
|
@ -321,9 +495,10 @@ class EmojiManager:
|
||||||
_time_end = time.time()
|
_time_end = time.time()
|
||||||
|
|
||||||
logger.info( # 使用匹配到的 emotion 记录日志喵~
|
logger.info( # 使用匹配到的 emotion 记录日志喵~
|
||||||
f"为[{text_emotion}]找到表情包: {matched_emotion},({similarity:.4f})"
|
f"为[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}"
|
||||||
)
|
)
|
||||||
return selected_emoji.path, f"[ {selected_emoji.description} ]"
|
# 返回完整文件路径和描述
|
||||||
|
return selected_emoji.full_path, f"[ {selected_emoji.description} ]"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 获取表情包失败: {str(e)}")
|
logger.error(f"[错误] 获取表情包失败: {str(e)}")
|
||||||
|
|
@ -371,40 +546,50 @@ class EmojiManager:
|
||||||
self.emoji_num = total_count
|
self.emoji_num = total_count
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
# 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
|
# 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
|
||||||
for emoji in self.emoji_objects[:]:
|
objects_to_remove = []
|
||||||
|
for emoji in self.emoji_objects:
|
||||||
try:
|
try:
|
||||||
|
# 跳过已经标记为删除的,避免重复处理
|
||||||
|
if emoji.is_deleted:
|
||||||
|
objects_to_remove.append(emoji) # 收集起来一次性移除
|
||||||
|
continue
|
||||||
|
|
||||||
# 检查文件是否存在
|
# 检查文件是否存在
|
||||||
if not os.path.exists(emoji.path):
|
if not os.path.exists(emoji.full_path):
|
||||||
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
|
logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}")
|
||||||
# 执行表情包对象的删除方法
|
# 执行表情包对象的删除方法
|
||||||
await emoji.delete()
|
await emoji.delete() # delete 方法现在会标记 is_deleted
|
||||||
# 从列表中移除该对象
|
objects_to_remove.append(emoji) # 标记删除后,也收集起来移除
|
||||||
self.emoji_objects.remove(emoji)
|
|
||||||
# 更新计数
|
# 更新计数
|
||||||
self.emoji_num -= 1
|
self.emoji_num -= 1
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if emoji.description == None:
|
# 检查描述是否为空 (如果为空也视为无效)
|
||||||
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
|
if not emoji.description:
|
||||||
# 执行表情包对象的删除方法
|
logger.warning(f"[检查] 表情包描述为空,视为无效: {emoji.filename}")
|
||||||
await emoji.delete()
|
await emoji.delete()
|
||||||
# 从列表中移除该对象
|
objects_to_remove.append(emoji)
|
||||||
self.emoji_objects.remove(emoji)
|
|
||||||
# 更新计数
|
|
||||||
self.emoji_num -= 1
|
self.emoji_num -= 1
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except Exception as item_error:
|
except Exception as item_error:
|
||||||
logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
|
logger.error(f"[错误] 处理表情包记录时出错 ({emoji.filename}): {str(item_error)}")
|
||||||
|
# 即使出错,也尝试继续检查下一个
|
||||||
continue
|
continue
|
||||||
|
|
||||||
await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
|
# 从 self.emoji_objects 中移除标记的对象
|
||||||
|
if objects_to_remove:
|
||||||
|
self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove]
|
||||||
|
|
||||||
|
# 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件
|
||||||
|
await clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
|
||||||
|
|
||||||
# 输出清理结果
|
# 输出清理结果
|
||||||
if removed_count > 0:
|
if removed_count > 0:
|
||||||
logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
|
logger.success(f"[清理] 已清理 {removed_count} 个失效/文件丢失的表情包记录")
|
||||||
logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}")
|
logger.info(f"[统计] 清理前记录数: {total_count} | 清理后有效记录数: {len(self.emoji_objects)}")
|
||||||
else:
|
else:
|
||||||
logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
|
logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
|
||||||
|
|
||||||
|
|
@ -418,7 +603,7 @@ class EmojiManager:
|
||||||
while True:
|
while True:
|
||||||
logger.info("[扫描] 开始检查表情包完整性...")
|
logger.info("[扫描] 开始检查表情包完整性...")
|
||||||
await self.check_emoji_file_integrity()
|
await self.check_emoji_file_integrity()
|
||||||
await self.clear_temp_emoji()
|
await clear_temp_emoji()
|
||||||
logger.info("[扫描] 开始扫描新表情包...")
|
logger.info("[扫描] 开始扫描新表情包...")
|
||||||
|
|
||||||
# 检查表情包目录是否存在
|
# 检查表情包目录是否存在
|
||||||
|
|
@ -467,48 +652,31 @@ class EmojiManager:
|
||||||
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
|
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
|
||||||
|
|
||||||
async def get_all_emoji_from_db(self):
|
async def get_all_emoji_from_db(self):
|
||||||
"""获取所有表情包并初始化为MaiEmoji类对象
|
"""获取所有表情包并初始化为MaiEmoji类对象,更新 self.emoji_objects"""
|
||||||
|
|
||||||
参数:
|
|
||||||
hash: 可选,如果提供则只返回指定哈希值的表情包
|
|
||||||
|
|
||||||
返回:
|
|
||||||
list[MaiEmoji]: 表情包对象列表
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
self._ensure_db()
|
self._ensure_db()
|
||||||
|
logger.info("[数据库] 开始加载所有表情包记录...")
|
||||||
|
|
||||||
# 获取所有表情包
|
emoji_objects, load_errors = _to_emoji_objects(db.emoji.find())
|
||||||
all_emoji_data = list(db.emoji.find())
|
|
||||||
|
|
||||||
# 将数据库记录转换为MaiEmoji对象
|
# 更新内存中的列表和数量
|
||||||
emoji_objects = []
|
|
||||||
for emoji_data in all_emoji_data:
|
|
||||||
emoji = MaiEmoji(
|
|
||||||
filename=emoji_data.get("filename", ""),
|
|
||||||
path=emoji_data.get("path", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置额外属性
|
|
||||||
emoji.hash = emoji_data.get("hash", "")
|
|
||||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
|
||||||
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
|
|
||||||
emoji.register_time = emoji_data.get("timestamp", time.time())
|
|
||||||
emoji.description = emoji_data.get("description", "")
|
|
||||||
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
|
|
||||||
emoji_objects.append(emoji)
|
|
||||||
|
|
||||||
# 存储到EmojiManager中
|
|
||||||
self.emoji_objects = emoji_objects
|
self.emoji_objects = emoji_objects
|
||||||
|
self.emoji_num = len(emoji_objects)
|
||||||
|
|
||||||
|
logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。")
|
||||||
|
if load_errors > 0:
|
||||||
|
logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
|
logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}")
|
||||||
|
self.emoji_objects = [] # 加载失败则清空列表
|
||||||
|
self.emoji_num = 0
|
||||||
|
|
||||||
async def get_emoji_from_db(self, hash=None):
|
async def get_emoji_from_db(self, emoji_hash=None):
|
||||||
"""获取所有表情包并初始化为MaiEmoji类对象
|
"""获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找)
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
hash: 可选,如果提供则只返回指定哈希值的表情包
|
emoji_hash: 可选,如果提供则只返回指定哈希值的表情包
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
list[MaiEmoji]: 表情包对象列表
|
list[MaiEmoji]: 表情包对象列表
|
||||||
|
|
@ -516,50 +684,38 @@ class EmojiManager:
|
||||||
try:
|
try:
|
||||||
self._ensure_db()
|
self._ensure_db()
|
||||||
|
|
||||||
# 准备查询条件
|
|
||||||
query = {}
|
query = {}
|
||||||
if hash:
|
if emoji_hash:
|
||||||
query = {"hash": hash}
|
query = {"hash": emoji_hash}
|
||||||
|
else:
|
||||||
# 获取所有表情包
|
logger.warning(
|
||||||
all_emoji_data = list(db.emoji.find(query))
|
"[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。"
|
||||||
|
|
||||||
# 将数据库记录转换为MaiEmoji对象
|
|
||||||
emoji_objects = []
|
|
||||||
for emoji_data in all_emoji_data:
|
|
||||||
emoji = MaiEmoji(
|
|
||||||
filename=emoji_data.get("filename", ""),
|
|
||||||
path=emoji_data.get("path", ""),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设置额外属性
|
emoji_objects, load_errors = _to_emoji_objects(db.emoji.find(query))
|
||||||
emoji.usage_count = emoji_data.get("usage_count", 0)
|
|
||||||
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
|
|
||||||
emoji.register_time = emoji_data.get("timestamp", time.time())
|
|
||||||
emoji.description = emoji_data.get("description", "")
|
|
||||||
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
|
|
||||||
|
|
||||||
emoji_objects.append(emoji)
|
if load_errors > 0:
|
||||||
|
logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。")
|
||||||
# 存储到EmojiManager中
|
|
||||||
self.emoji_objects = emoji_objects
|
|
||||||
|
|
||||||
return emoji_objects
|
return emoji_objects
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
|
logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def get_emoji_from_manager(self, hash) -> MaiEmoji:
|
async def get_emoji_from_manager(self, emoji_hash) -> Optional[MaiEmoji]:
|
||||||
"""从EmojiManager中获取表情包
|
"""从内存中的 emoji_objects 列表获取表情包
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
hash:如果提供则只返回指定哈希值的表情包
|
emoji_hash: 要查找的表情包哈希值
|
||||||
|
返回:
|
||||||
|
MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None
|
||||||
"""
|
"""
|
||||||
for emoji in self.emoji_objects:
|
for emoji in self.emoji_objects:
|
||||||
if emoji.hash == hash:
|
# 确保对象未被标记为删除且哈希值匹配
|
||||||
|
if not emoji.is_deleted and emoji.hash == emoji_hash:
|
||||||
return emoji
|
return emoji
|
||||||
return None
|
return None # 如果循环结束还没找到,则返回 None
|
||||||
|
|
||||||
async def delete_emoji(self, emoji_hash: str) -> bool:
|
async def delete_emoji(self, emoji_hash: str) -> bool:
|
||||||
"""根据哈希值删除表情包
|
"""根据哈希值删除表情包
|
||||||
|
|
@ -600,26 +756,6 @@ class EmojiManager:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _emoji_objects_to_readable_list(self, emoji_objects):
|
|
||||||
"""将表情包对象列表转换为可读的字符串列表
|
|
||||||
|
|
||||||
参数:
|
|
||||||
emoji_objects: MaiEmoji对象列表
|
|
||||||
|
|
||||||
返回:
|
|
||||||
list[str]: 可读的表情包信息字符串列表
|
|
||||||
"""
|
|
||||||
emoji_info_list = []
|
|
||||||
for i, emoji in enumerate(emoji_objects):
|
|
||||||
# 转换时间戳为可读时间
|
|
||||||
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time))
|
|
||||||
# 构建每个表情包的信息字符串
|
|
||||||
emoji_info = (
|
|
||||||
f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n"
|
|
||||||
)
|
|
||||||
emoji_info_list.append(emoji_info)
|
|
||||||
return emoji_info_list
|
|
||||||
|
|
||||||
async def replace_a_emoji(self, new_emoji: MaiEmoji):
|
async def replace_a_emoji(self, new_emoji: MaiEmoji):
|
||||||
"""替换一个表情包
|
"""替换一个表情包
|
||||||
|
|
||||||
|
|
@ -646,7 +782,7 @@ class EmojiManager:
|
||||||
)
|
)
|
||||||
|
|
||||||
# 将表情包信息转换为可读的字符串
|
# 将表情包信息转换为可读的字符串
|
||||||
emoji_info_list = self._emoji_objects_to_readable_list(selected_emojis)
|
emoji_info_list = _emoji_objects_to_readable_list(selected_emojis)
|
||||||
|
|
||||||
# 构建提示词
|
# 构建提示词
|
||||||
prompt = (
|
prompt = (
|
||||||
|
|
@ -744,7 +880,7 @@ class EmojiManager:
|
||||||
'''
|
'''
|
||||||
content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
|
content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
|
||||||
if content == "否":
|
if content == "否":
|
||||||
return None, []
|
return "", []
|
||||||
|
|
||||||
# 分析情感含义
|
# 分析情感含义
|
||||||
emotion_prompt = f"""
|
emotion_prompt = f"""
|
||||||
|
|
@ -779,105 +915,106 @@ class EmojiManager:
|
||||||
Returns:
|
Returns:
|
||||||
bool: 注册是否成功
|
bool: 注册是否成功
|
||||||
"""
|
"""
|
||||||
|
file_full_path = os.path.join(EMOJI_DIR, filename)
|
||||||
|
if not os.path.exists(file_full_path):
|
||||||
|
logger.error(f"[注册失败] 文件不存在: {file_full_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用MaiEmoji类创建表情包实例
|
# 1. 创建 MaiEmoji 实例并初始化哈希和格式
|
||||||
new_emoji = MaiEmoji(filename, EMOJI_DIR)
|
new_emoji = MaiEmoji(full_path=file_full_path)
|
||||||
await new_emoji.initialize_hash_format()
|
init_result = await new_emoji.initialize_hash_format()
|
||||||
emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename))
|
if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误
|
||||||
description, emotions = await self.build_emoji_description(emoji_base64)
|
logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}")
|
||||||
if description == "" or description == None:
|
# 是否需要删除源文件?看业务需求,暂时不删
|
||||||
return False
|
return False
|
||||||
new_emoji.description = description
|
|
||||||
new_emoji.emotion = emotions
|
|
||||||
|
|
||||||
# 检查是否已经注册过
|
# 2. 检查哈希是否已存在 (在内存中检查)
|
||||||
# 对比内存中是否存在相同哈希值的表情包
|
|
||||||
if await self.get_emoji_from_manager(new_emoji.hash):
|
if await self.get_emoji_from_manager(new_emoji.hash):
|
||||||
logger.warning(f"[警告] 表情包已存在: {filename}")
|
logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}")
|
||||||
|
# 删除重复的源文件
|
||||||
|
try:
|
||||||
|
os.remove(file_full_path)
|
||||||
|
logger.info(f"[清理] 删除重复的待注册文件: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 删除重复文件失败: {str(e)}")
|
||||||
|
return False # 返回 False 表示未注册新表情
|
||||||
|
|
||||||
|
# 3. 构建描述和情感
|
||||||
|
try:
|
||||||
|
emoji_base64 = image_path_to_base64(file_full_path)
|
||||||
|
if emoji_base64 is None: # 再次检查读取
|
||||||
|
logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}")
|
||||||
|
return False
|
||||||
|
description, emotions = await self.build_emoji_description(emoji_base64)
|
||||||
|
if not description: # 检查描述是否成功生成或审核通过
|
||||||
|
logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}")
|
||||||
|
# 删除未能生成描述的文件
|
||||||
|
try:
|
||||||
|
os.remove(file_full_path)
|
||||||
|
logger.info(f"[清理] 删除描述生成失败的文件: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 删除描述生成失败文件时出错: {str(e)}")
|
||||||
|
return False
|
||||||
|
new_emoji.description = description
|
||||||
|
new_emoji.emotion = emotions
|
||||||
|
except Exception as build_desc_error:
|
||||||
|
logger.error(f"[注册失败] 生成描述/情感时出错 ({filename}): {build_desc_error}")
|
||||||
|
# 同样考虑删除文件
|
||||||
|
try:
|
||||||
|
os.remove(file_full_path)
|
||||||
|
logger.info(f"[清理] 删除描述生成异常的文件: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 删除描述生成异常文件时出错: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 4. 检查容量并决定是否替换或直接注册
|
||||||
if self.emoji_num >= self.emoji_num_max:
|
if self.emoji_num >= self.emoji_num_max:
|
||||||
logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})")
|
logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),尝试替换...")
|
||||||
replaced = await self.replace_a_emoji(new_emoji)
|
replaced = await self.replace_a_emoji(new_emoji)
|
||||||
if not replaced:
|
if not replaced:
|
||||||
logger.error("[错误] 替换表情包失败,无法完成注册")
|
logger.error("[注册失败] 替换表情包失败,无法完成注册")
|
||||||
|
# 替换失败,删除新表情包文件
|
||||||
|
try:
|
||||||
|
os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径
|
||||||
|
logger.info(f"[清理] 删除替换失败的新表情文件: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
# 替换成功时,replace_a_emoji 内部已处理 new_emoji 的注册和添加到列表
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# 修复:等待异步注册完成
|
# 直接注册
|
||||||
register_success = await new_emoji.register_to_db()
|
register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB
|
||||||
if register_success:
|
if register_success:
|
||||||
|
# 注册成功后,添加到内存列表
|
||||||
self.emoji_objects.append(new_emoji)
|
self.emoji_objects.append(new_emoji)
|
||||||
self.emoji_num += 1
|
self.emoji_num += 1
|
||||||
logger.success(f"[成功] 注册: {filename}")
|
logger.success(f"[成功] 注册新表情包: {filename} (当前: {self.emoji_num}/{self.emoji_num_max})")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"[错误] 注册表情包到数据库失败: {filename}")
|
logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}")
|
||||||
|
# register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在
|
||||||
|
# 是否需要删除源文件?
|
||||||
|
if os.path.exists(file_full_path):
|
||||||
|
try:
|
||||||
|
os.remove(file_full_path)
|
||||||
|
logger.info(f"[清理] 删除注册失败的源文件: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[错误] 注册表情包失败: {str(e)}")
|
logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return False
|
# 尝试删除源文件以避免循环处理
|
||||||
|
if os.path.exists(file_full_path):
|
||||||
async def clear_temp_emoji(self):
|
|
||||||
"""每天清理临时表情包
|
|
||||||
清理/data/emoji和/data/image目录下的所有文件
|
|
||||||
当目录中文件数超过50时,会全部删除
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.info("[清理] 开始清理缓存...")
|
|
||||||
|
|
||||||
# 清理emoji目录
|
|
||||||
emoji_dir = os.path.join(BASE_DIR, "emoji")
|
|
||||||
if os.path.exists(emoji_dir):
|
|
||||||
files = os.listdir(emoji_dir)
|
|
||||||
# 如果文件数超过50就全部删除
|
|
||||||
if len(files) > 50:
|
|
||||||
for filename in files:
|
|
||||||
file_path = os.path.join(emoji_dir, filename)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
os.remove(file_path)
|
|
||||||
logger.debug(f"[清理] 删除: {filename}")
|
|
||||||
|
|
||||||
# 清理image目录
|
|
||||||
image_dir = os.path.join(BASE_DIR, "image")
|
|
||||||
if os.path.exists(image_dir):
|
|
||||||
files = os.listdir(image_dir)
|
|
||||||
# 如果文件数超过50就全部删除
|
|
||||||
if len(files) > 50:
|
|
||||||
for filename in files:
|
|
||||||
file_path = os.path.join(image_dir, filename)
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
os.remove(file_path)
|
|
||||||
logger.debug(f"[清理] 删除图片: {filename}")
|
|
||||||
|
|
||||||
logger.success("[清理] 完成")
|
|
||||||
|
|
||||||
async def clean_unused_emojis(self, emoji_dir, emoji_objects):
|
|
||||||
"""清理未使用的表情包文件
|
|
||||||
遍历指定文件夹中的所有文件,删除未在emoji_objects列表中的文件
|
|
||||||
"""
|
|
||||||
# 首先检查目录是否存在喵~
|
|
||||||
if not os.path.exists(emoji_dir):
|
|
||||||
logger.warning(f"[清理] 表情包目录不存在,跳过清理: {emoji_dir}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 获取所有表情包路径
|
|
||||||
emoji_paths = {emoji.path for emoji in emoji_objects}
|
|
||||||
|
|
||||||
# 遍历文件夹中的所有文件
|
|
||||||
for file_name in os.listdir(emoji_dir):
|
|
||||||
file_path = os.path.join(emoji_dir, file_name)
|
|
||||||
|
|
||||||
# 检查文件是否在表情包路径列表中
|
|
||||||
if file_path not in emoji_paths:
|
|
||||||
try:
|
try:
|
||||||
# 删除未在表情包列表中的文件
|
os.remove(file_full_path)
|
||||||
os.remove(file_path)
|
logger.info(f"[清理] 删除处理异常的源文件: {filename}")
|
||||||
logger.info(f"[清理] 删除未使用的表情包文件: {file_path}")
|
except Exception as remove_error:
|
||||||
except Exception as e:
|
logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}")
|
||||||
logger.error(f"[错误] 删除文件时出错: {str(e)}")
|
return False
|
||||||
|
|
||||||
|
|
||||||
# 创建全局单例
|
# 创建全局单例
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import asyncio
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import random # <--- 添加导入
|
import random # <--- 添加导入
|
||||||
|
import json # <--- 确保导入 json
|
||||||
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
|
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
|
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
|
||||||
|
|
@ -14,9 +15,7 @@ from src.plugins.models.utils_model import LLMRequest
|
||||||
from src.config.config import global_config
|
from src.config.config import global_config
|
||||||
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
|
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
|
||||||
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
|
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
|
||||||
from src.do_tool.tool_use import ToolUser
|
|
||||||
from src.plugins.emoji_system.emoji_manager import emoji_manager
|
from src.plugins.emoji_system.emoji_manager import emoji_manager
|
||||||
from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments
|
|
||||||
from src.heart_flow.sub_mind import SubMind
|
from src.heart_flow.sub_mind import SubMind
|
||||||
from src.heart_flow.observation import Observation
|
from src.heart_flow.observation import Observation
|
||||||
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
|
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
|
||||||
|
|
@ -39,7 +38,7 @@ EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
|
||||||
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
|
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("HFC") # Logger Name Changed
|
logger = get_logger("hfc") # Logger Name Changed
|
||||||
|
|
||||||
|
|
||||||
# 默认动作定义
|
# 默认动作定义
|
||||||
|
|
@ -121,35 +120,6 @@ class ActionManager:
|
||||||
"""重置为默认动作集"""
|
"""重置为默认动作集"""
|
||||||
self._available_actions = DEFAULT_ACTIONS.copy()
|
self._available_actions = DEFAULT_ACTIONS.copy()
|
||||||
|
|
||||||
def get_planner_tool_definition(self) -> List[Dict[str, Any]]:
|
|
||||||
"""获取当前动作集对应的规划器工具定义"""
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "decide_reply_action",
|
|
||||||
"description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"action": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": list(self._available_actions.keys()),
|
|
||||||
"description": "决定采取的行动:"
|
|
||||||
+ ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]),
|
|
||||||
},
|
|
||||||
"reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
|
|
||||||
"emoji_query": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["action", "reasoning"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# 在文件开头添加自定义异常类
|
# 在文件开头添加自定义异常类
|
||||||
class HeartFCError(Exception):
|
class HeartFCError(Exception):
|
||||||
|
|
@ -176,6 +146,25 @@ class SenderError(HeartFCError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_cycle_delay(action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
|
||||||
|
"""处理循环延迟"""
|
||||||
|
cycle_duration = time.monotonic() - cycle_start_time
|
||||||
|
|
||||||
|
try:
|
||||||
|
sleep_duration = 0.0
|
||||||
|
if not action_taken_this_cycle and cycle_duration < 1:
|
||||||
|
sleep_duration = 1 - cycle_duration
|
||||||
|
elif cycle_duration < 0.2:
|
||||||
|
sleep_duration = 0.2
|
||||||
|
|
||||||
|
if sleep_duration > 0:
|
||||||
|
await asyncio.sleep(sleep_duration)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class HeartFChatting:
|
class HeartFChatting:
|
||||||
"""
|
"""
|
||||||
管理一个连续的Plan-Replier-Sender循环
|
管理一个连续的Plan-Replier-Sender循环
|
||||||
|
|
@ -187,7 +176,7 @@ class HeartFChatting:
|
||||||
self,
|
self,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
sub_mind: SubMind,
|
sub_mind: SubMind,
|
||||||
observations: Observation,
|
observations: list[Observation],
|
||||||
on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]],
|
on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
@ -224,7 +213,6 @@ class HeartFChatting:
|
||||||
max_tokens=256,
|
max_tokens=256,
|
||||||
request_type="response_heartflow",
|
request_type="response_heartflow",
|
||||||
)
|
)
|
||||||
self.tool_user = ToolUser()
|
|
||||||
self.heart_fc_sender = HeartFCSender()
|
self.heart_fc_sender = HeartFCSender()
|
||||||
|
|
||||||
# LLM规划器配置
|
# LLM规划器配置
|
||||||
|
|
@ -263,7 +251,7 @@ class HeartFChatting:
|
||||||
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
|
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
|
||||||
|
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ")
|
logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
|
|
@ -294,7 +282,7 @@ class HeartFChatting:
|
||||||
pass # 忽略取消或超时错误
|
pass # 忽略取消或超时错误
|
||||||
self._loop_task = None # 清理旧任务引用
|
self._loop_task = None # 清理旧任务引用
|
||||||
|
|
||||||
logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
|
logger.debug(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
|
||||||
# 创建新的循环任务
|
# 创建新的循环任务
|
||||||
self._loop_task = asyncio.create_task(self._hfc_loop())
|
self._loop_task = asyncio.create_task(self._hfc_loop())
|
||||||
# 添加完成回调
|
# 添加完成回调
|
||||||
|
|
@ -360,7 +348,7 @@ class HeartFChatting:
|
||||||
self._current_cycle.timers = cycle_timers
|
self._current_cycle.timers = cycle_timers
|
||||||
|
|
||||||
# 防止循环过快消耗资源
|
# 防止循环过快消耗资源
|
||||||
await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix)
|
await _handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix)
|
||||||
|
|
||||||
# 完成当前循环并保存历史
|
# 完成当前循环并保存历史
|
||||||
self._current_cycle.complete_cycle()
|
self._current_cycle.complete_cycle()
|
||||||
|
|
@ -471,6 +459,17 @@ class HeartFChatting:
|
||||||
return False, ""
|
return False, ""
|
||||||
|
|
||||||
# execute:执行
|
# execute:执行
|
||||||
|
|
||||||
|
# 在此处添加日志记录
|
||||||
|
if action == "text_reply":
|
||||||
|
action_str = "回复"
|
||||||
|
elif action == "emoji_reply":
|
||||||
|
action_str = "回复表情"
|
||||||
|
else:
|
||||||
|
action_str = "不回复"
|
||||||
|
|
||||||
|
logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'")
|
||||||
|
|
||||||
return await self._handle_action(
|
return await self._handle_action(
|
||||||
action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
|
action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
|
||||||
)
|
)
|
||||||
|
|
@ -646,19 +645,18 @@ class HeartFChatting:
|
||||||
observation = self.observations[0] if self.observations else None
|
observation = self.observations[0] if self.observations else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dang_qian_deng_dai = 0.0 # 初始化本次等待时间
|
|
||||||
with Timer("等待新消息", cycle_timers):
|
with Timer("等待新消息", cycle_timers):
|
||||||
# 等待新消息、超时或关闭信号,并获取结果
|
# 等待新消息、超时或关闭信号,并获取结果
|
||||||
await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix)
|
await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix)
|
||||||
# 从计时器获取实际等待时间
|
# 从计时器获取实际等待时间
|
||||||
dang_qian_deng_dai = cycle_timers.get("等待新消息", 0.0)
|
current_waiting = cycle_timers.get("等待新消息", 0.0)
|
||||||
|
|
||||||
if not self._shutting_down:
|
if not self._shutting_down:
|
||||||
self._lian_xu_bu_hui_fu_ci_shu += 1
|
self._lian_xu_bu_hui_fu_ci_shu += 1
|
||||||
self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间
|
self._lian_xu_deng_dai_shi_jian += current_waiting # 累加等待时间
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
|
f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
|
||||||
f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒"
|
f"本次等待: {current_waiting:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查是否同时达到次数和时间阈值
|
# 检查是否同时达到次数和时间阈值
|
||||||
|
|
@ -833,24 +831,6 @@ class HeartFChatting:
|
||||||
if not self._shutting_down:
|
if not self._shutting_down:
|
||||||
logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
|
logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
|
||||||
|
|
||||||
async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
|
|
||||||
"""处理循环延迟"""
|
|
||||||
cycle_duration = time.monotonic() - cycle_start_time
|
|
||||||
|
|
||||||
try:
|
|
||||||
sleep_duration = 0.0
|
|
||||||
if not action_taken_this_cycle and cycle_duration < 1:
|
|
||||||
sleep_duration = 1 - cycle_duration
|
|
||||||
elif cycle_duration < 0.2:
|
|
||||||
sleep_duration = 0.2
|
|
||||||
|
|
||||||
if sleep_duration > 0:
|
|
||||||
await asyncio.sleep(sleep_duration)
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _get_submind_thinking(self, cycle_timers: dict) -> str:
|
async def _get_submind_thinking(self, cycle_timers: dict) -> str:
|
||||||
"""
|
"""
|
||||||
获取子思维的思考结果
|
获取子思维的思考结果
|
||||||
|
|
@ -881,41 +861,36 @@ class HeartFChatting:
|
||||||
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
|
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
|
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
|
||||||
|
重构为:让LLM返回结构化JSON文本,然后在代码中解析。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
current_mind: 子思维的当前思考结果
|
current_mind: 子思维的当前思考结果
|
||||||
cycle_timers: 计时器字典
|
cycle_timers: 计时器字典
|
||||||
is_re_planned: 是否为重新规划
|
is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑)
|
||||||
"""
|
"""
|
||||||
logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器")
|
logger.info(f"{self.log_prefix}开始想要做什么")
|
||||||
|
|
||||||
# --- 新增:检查历史动作并调整可用动作 ---
|
|
||||||
lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
|
|
||||||
actions_to_remove_temporarily = []
|
actions_to_remove_temporarily = []
|
||||||
probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断
|
# --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
|
||||||
|
lian_xu_wen_ben_hui_fu = 0
|
||||||
# 反向遍历最近的循环历史
|
probability_roll = random.random()
|
||||||
for cycle in reversed(self._cycle_history):
|
for cycle in reversed(self._cycle_history):
|
||||||
# 只关心实际执行了动作的循环
|
|
||||||
if cycle.action_taken:
|
if cycle.action_taken:
|
||||||
if cycle.action_type == "text_reply":
|
if cycle.action_type == "text_reply":
|
||||||
lian_xu_wen_ben_hui_fu += 1
|
lian_xu_wen_ben_hui_fu += 1
|
||||||
else:
|
else:
|
||||||
break # 遇到非文本回复,中断计数
|
break
|
||||||
# 检查最近的3个循环即可,避免检查过多历史 (如果历史很长)
|
|
||||||
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
|
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
|
||||||
len(self._cycle_history) - 4
|
len(self._cycle_history) - 4
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
|
|
||||||
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
|
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
|
||||||
|
|
||||||
# 根据连续次数决定临时移除哪些动作
|
|
||||||
if lian_xu_wen_ben_hui_fu >= 3:
|
if lian_xu_wen_ben_hui_fu >= 3:
|
||||||
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
|
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
|
||||||
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
|
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
|
||||||
elif lian_xu_wen_ben_hui_fu == 2:
|
elif lian_xu_wen_ben_hui_fu == 2:
|
||||||
if probability_roll < 0.8: # 80% 概率
|
if probability_roll < 0.8:
|
||||||
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)")
|
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)")
|
||||||
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
|
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
|
||||||
else:
|
else:
|
||||||
|
|
@ -923,183 +898,179 @@ class HeartFChatting:
|
||||||
f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)"
|
f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)"
|
||||||
)
|
)
|
||||||
elif lian_xu_wen_ben_hui_fu == 1:
|
elif lian_xu_wen_ben_hui_fu == 1:
|
||||||
if probability_roll < 0.4: # 40% 概率
|
if probability_roll < 0.4:
|
||||||
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (触发)")
|
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (触发)")
|
||||||
actions_to_remove_temporarily.append("text_reply")
|
actions_to_remove_temporarily.append("text_reply")
|
||||||
else:
|
else:
|
||||||
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (未触发)")
|
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (未触发)")
|
||||||
# 如果 lian_xu_wen_ben_hui_fu == 0,则不移除任何动作
|
# --- 结束检查历史动作 ---
|
||||||
# --- 结束:检查历史动作 ---
|
|
||||||
|
|
||||||
# 获取观察信息
|
# 获取观察信息
|
||||||
observation = self.observations[0]
|
observation = self.observations[0]
|
||||||
if is_re_planned:
|
# if is_re_planned: # 暂时简化,不处理重新规划
|
||||||
await observation.observe()
|
# await observation.observe()
|
||||||
observed_messages = observation.talking_message
|
observed_messages = observation.talking_message
|
||||||
observed_messages_str = observation.talking_message_str_truncate
|
observed_messages_str = observation.talking_message_str_truncate
|
||||||
|
|
||||||
# --- 使用 LLM 进行决策 --- #
|
# --- 使用 LLM 进行决策 (JSON 输出模式) --- #
|
||||||
reasoning = "默认决策或获取决策失败"
|
action = "no_reply" # 默认动作
|
||||||
llm_error = False # LLM错误标志
|
reasoning = "规划器初始化默认"
|
||||||
arguments = None # 初始化参数变量
|
emoji_query = ""
|
||||||
emoji_query = "" # <--- 在这里初始化 emoji_query
|
llm_error = False # LLM 请求或解析错误标志
|
||||||
|
|
||||||
|
# 获取我们将传递给 prompt 构建器和用于验证的当前可用动作
|
||||||
|
current_available_actions = self.action_manager.get_available_actions()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# --- 新增:应用临时动作移除 ---
|
# --- 应用临时动作移除 ---
|
||||||
if actions_to_remove_temporarily:
|
if actions_to_remove_temporarily:
|
||||||
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
|
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
|
||||||
|
# 更新 current_available_actions 以反映移除后的状态
|
||||||
|
current_available_actions = self.action_manager.get_available_actions()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
|
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 构建提示词 ---
|
# --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
|
||||||
replan_prompt_str = ""
|
# replan_prompt_str = "" # 暂时简化
|
||||||
if is_re_planned:
|
# if is_re_planned:
|
||||||
replan_prompt_str = await self._build_replan_prompt(
|
# replan_prompt_str = await self._build_replan_prompt(
|
||||||
self._current_cycle.action_type, self._current_cycle.reasoning
|
# self._current_cycle.action_type, self._current_cycle.reasoning
|
||||||
)
|
# )
|
||||||
prompt = await self._build_planner_prompt(
|
prompt = await self._build_planner_prompt(
|
||||||
observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str
|
observed_messages_str,
|
||||||
|
current_mind,
|
||||||
|
self.sub_mind.structured_info,
|
||||||
|
"", # replan_prompt_str,
|
||||||
|
current_available_actions, # <--- 传入当前可用动作
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 调用 LLM ---
|
# --- 调用 LLM (普通文本生成) ---
|
||||||
|
llm_content = None
|
||||||
try:
|
try:
|
||||||
planner_tools = self.action_manager.get_planner_tool_definition()
|
# 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name)
|
||||||
logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具
|
# 我们只需要 content
|
||||||
_response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async(
|
# !! 注意:这里假设 self.planner_llm 有 generate_response 方法
|
||||||
prompt=prompt,
|
# !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改
|
||||||
tools=planner_tools,
|
llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
|
||||||
)
|
logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
|
||||||
logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
|
|
||||||
except Exception as req_e:
|
except Exception as req_e:
|
||||||
logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}")
|
logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
|
||||||
action = "error"
|
reasoning = f"LLM 请求失败: {req_e}"
|
||||||
reasoning = f"LLM请求失败: {req_e}"
|
|
||||||
llm_error = True
|
llm_error = True
|
||||||
# 直接返回错误结果
|
# 直接使用默认动作返回错误结果
|
||||||
return {
|
action = "no_reply" # 明确设置为默认值
|
||||||
"action": action,
|
emoji_query = "" # 明确设置为空
|
||||||
"reasoning": reasoning,
|
# 不再立即返回,而是继续执行 finally 块以恢复动作
|
||||||
"emoji_query": "",
|
# return { ... }
|
||||||
"current_mind": current_mind,
|
|
||||||
"observed_messages": observed_messages,
|
|
||||||
"llm_error": llm_error,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 默认错误状态
|
# --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) ---
|
||||||
action = "error"
|
if not llm_error and llm_content:
|
||||||
reasoning = "处理工具调用时出错"
|
try:
|
||||||
llm_error = True
|
# 尝试去除可能的 markdown 代码块标记
|
||||||
|
cleaned_content = (
|
||||||
|
llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
|
||||||
|
)
|
||||||
|
if not cleaned_content:
|
||||||
|
raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0)
|
||||||
|
parsed_json = json.loads(cleaned_content)
|
||||||
|
|
||||||
# 1. 验证工具调用
|
# 提取决策,提供默认值
|
||||||
success, valid_tool_calls, error_msg = process_llm_tool_calls(
|
extracted_action = parsed_json.get("action", "no_reply")
|
||||||
tool_calls, log_prefix=f"{self.log_prefix}[Planner] "
|
extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
|
||||||
)
|
extracted_emoji_query = parsed_json.get("emoji_query", "")
|
||||||
|
|
||||||
if success and valid_tool_calls:
|
# 验证动作是否在当前可用列表中
|
||||||
# 2. 提取第一个调用并获取参数
|
# !! 使用调用 prompt 时实际可用的动作列表进行验证
|
||||||
first_tool_call = valid_tool_calls[0]
|
if extracted_action not in current_available_actions:
|
||||||
tool_name = first_tool_call.get("function", {}).get("name")
|
|
||||||
arguments = extract_tool_call_arguments(first_tool_call, None)
|
|
||||||
|
|
||||||
# 3. 检查名称和参数
|
|
||||||
expected_tool_name = "decide_reply_action"
|
|
||||||
if tool_name == expected_tool_name and arguments is not None:
|
|
||||||
# 4. 成功,提取决策
|
|
||||||
extracted_action = arguments.get("action", "no_reply")
|
|
||||||
# 验证动作
|
|
||||||
if extracted_action not in self.action_manager.get_available_actions():
|
|
||||||
# 如果LLM返回了一个此时不该用的动作(因为被临时移除了)
|
|
||||||
# 或者完全无效的动作
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'"
|
f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
|
||||||
)
|
)
|
||||||
action = "no_reply"
|
action = "no_reply"
|
||||||
reasoning = f"LLM返回了当前不可用的动作: {extracted_action}"
|
reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
|
||||||
emoji_query = ""
|
emoji_query = ""
|
||||||
llm_error = False # 视为逻辑修正而非 LLM 错误
|
# 检查 no_reply 是否也恰好被移除了 (极端情况)
|
||||||
# --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) ---
|
if "no_reply" not in current_available_actions:
|
||||||
if "no_reply" not in self.action_manager.get_available_actions():
|
|
||||||
logger.error(
|
logger.error(
|
||||||
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
|
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
|
||||||
)
|
)
|
||||||
action = "error" # 回退到错误状态
|
action = "error" # 回退到错误状态
|
||||||
reasoning = "无法执行任何有效动作,包括 no_reply"
|
reasoning = "无法执行任何有效动作,包括 no_reply"
|
||||||
llm_error = True
|
llm_error = True # 标记为严重错误
|
||||||
|
else:
|
||||||
|
llm_error = False # 视为逻辑修正而非 LLM 错误
|
||||||
else:
|
else:
|
||||||
# 动作有效且可用,使用提取的值
|
# 动作有效且可用
|
||||||
action = extracted_action
|
action = extracted_action
|
||||||
reasoning = arguments.get("reasoning", "未提供理由")
|
reasoning = extracted_reasoning
|
||||||
emoji_query = arguments.get("emoji_query", "")
|
emoji_query = extracted_emoji_query
|
||||||
llm_error = False # 成功处理
|
llm_error = False # 解析成功
|
||||||
# 记录决策结果
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
|
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
|
||||||
)
|
)
|
||||||
elif tool_name != expected_tool_name:
|
|
||||||
reasoning = f"LLM返回了非预期的工具: {tool_name}"
|
|
||||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
|
||||||
else: # arguments is None
|
|
||||||
reasoning = f"无法提取工具 {tool_name} 的参数"
|
|
||||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
|
||||||
elif not success:
|
|
||||||
reasoning = f"验证工具调用失败: {error_msg}"
|
|
||||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
|
||||||
else: # not valid_tool_calls
|
|
||||||
# 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
|
|
||||||
available_actions = list(self.action_manager.get_available_actions().keys())
|
|
||||||
if available_actions == ["no_reply"]:
|
|
||||||
logger.info(
|
|
||||||
f"{self.log_prefix}[Planner] LLM未返回工具调用,但当前唯一可用动作是 'no_reply',将执行 'no_reply'"
|
|
||||||
)
|
|
||||||
action = "no_reply"
|
|
||||||
reasoning = "LLM未返回工具调用,且当前仅 'no_reply' 可用"
|
|
||||||
emoji_query = ""
|
|
||||||
llm_error = False # 视为逻辑选择而非错误
|
|
||||||
else:
|
|
||||||
reasoning = "LLM未返回有效的工具调用"
|
|
||||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
|
||||||
# llm_error 保持为 True
|
|
||||||
# 如果 llm_error 仍然是 True,说明在处理过程中有错误发生
|
|
||||||
|
|
||||||
except Exception as llm_e:
|
except json.JSONDecodeError as json_e:
|
||||||
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}")
|
logger.warning(
|
||||||
|
f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'"
|
||||||
|
)
|
||||||
|
reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'."
|
||||||
|
action = "no_reply" # 解析失败则默认不回复
|
||||||
|
emoji_query = ""
|
||||||
|
llm_error = True # 标记解析错误
|
||||||
|
except Exception as parse_e:
|
||||||
|
logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}")
|
||||||
|
reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'."
|
||||||
|
action = "no_reply"
|
||||||
|
emoji_query = ""
|
||||||
|
llm_error = True
|
||||||
|
elif not llm_error and not llm_content:
|
||||||
|
# LLM 请求成功但返回空内容
|
||||||
|
logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。")
|
||||||
|
reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'."
|
||||||
|
action = "no_reply"
|
||||||
|
emoji_query = ""
|
||||||
|
llm_error = True # 标记为空响应错误
|
||||||
|
|
||||||
|
# 如果 llm_error 在此阶段为 True,意味着请求成功但解析失败或返回空
|
||||||
|
# 如果 llm_error 在请求阶段就为 True,则跳过了此解析块
|
||||||
|
|
||||||
|
except Exception as outer_e:
|
||||||
|
logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
action = "error"
|
action = "error" # 发生未知错误,标记为 error 动作
|
||||||
reasoning = f"Planner内部处理错误: {llm_e}"
|
reasoning = f"Planner 内部处理错误: {outer_e}"
|
||||||
|
emoji_query = ""
|
||||||
llm_error = True
|
llm_error = True
|
||||||
# --- 新增:确保动作恢复 ---
|
|
||||||
finally:
|
finally:
|
||||||
if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复
|
# --- 确保动作恢复 ---
|
||||||
|
# 检查 self._original_actions_backup 是否有值来判断是否需要恢复
|
||||||
|
if self.action_manager._original_actions_backup is not None:
|
||||||
self.action_manager.restore_actions()
|
self.action_manager.restore_actions()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
|
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
|
||||||
)
|
)
|
||||||
# --- 结束:确保动作恢复 ---
|
# --- 结束确保动作恢复 ---
|
||||||
|
|
||||||
# --- 新增:概率性忽略文本回复附带的表情(正确的位置)---
|
|
||||||
|
|
||||||
|
# --- 概率性忽略文本回复附带的表情 (逻辑保持不变) ---
|
||||||
if action == "text_reply" and emoji_query:
|
if action == "text_reply" and emoji_query:
|
||||||
logger.debug(f"{self.log_prefix}[Planner] 大模型想让麦麦发文字时带表情: '{emoji_query}'")
|
logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'")
|
||||||
# 掷骰子看看要不要听它的
|
|
||||||
if random.random() > EMOJI_SEND_PRO:
|
if random.random() > EMOJI_SEND_PRO:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
|
f"{self.log_prefix}但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
|
||||||
)
|
)
|
||||||
emoji_query = "" # 把表情请求清空,就不发了
|
emoji_query = "" # 清空表情请求
|
||||||
else:
|
else:
|
||||||
logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'")
|
logger.info(f"{self.log_prefix}好吧,加上表情 '{emoji_query}'")
|
||||||
# --- 结束:概率性忽略 ---
|
# --- 结束概率性忽略 ---
|
||||||
|
|
||||||
# --- 结束 LLM 决策 --- #
|
|
||||||
|
|
||||||
|
# 返回结果字典
|
||||||
return {
|
return {
|
||||||
"action": action,
|
"action": action,
|
||||||
"reasoning": reasoning,
|
"reasoning": reasoning,
|
||||||
"emoji_query": emoji_query,
|
"emoji_query": emoji_query,
|
||||||
"current_mind": current_mind,
|
"current_mind": current_mind,
|
||||||
"observed_messages": observed_messages,
|
"observed_messages": observed_messages,
|
||||||
"llm_error": llm_error,
|
"llm_error": llm_error, # 返回错误状态
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _get_anchor_message(self) -> Optional[MessageRecv]:
|
async def _get_anchor_message(self) -> Optional[MessageRecv]:
|
||||||
|
|
@ -1128,9 +1099,7 @@ class HeartFChatting:
|
||||||
}
|
}
|
||||||
anchor_message = MessageRecv(placeholder_msg_dict)
|
anchor_message = MessageRecv(placeholder_msg_dict)
|
||||||
anchor_message.update_chat_stream(self.chat_stream)
|
anchor_message.update_chat_stream(self.chat_stream)
|
||||||
logger.info(
|
logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
|
||||||
f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
|
|
||||||
)
|
|
||||||
return anchor_message
|
return anchor_message
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1243,8 +1212,9 @@ class HeartFChatting:
|
||||||
current_mind: Optional[str],
|
current_mind: Optional[str],
|
||||||
structured_info: Dict[str, Any],
|
structured_info: Dict[str, Any],
|
||||||
replan_prompt: str,
|
replan_prompt: str,
|
||||||
|
current_available_actions: Dict[str, str],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""构建 Planner LLM 的提示词"""
|
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
|
||||||
try:
|
try:
|
||||||
# 准备结构化信息块
|
# 准备结构化信息块
|
||||||
structured_info_block = ""
|
structured_info_block = ""
|
||||||
|
|
@ -1260,12 +1230,13 @@ class HeartFChatting:
|
||||||
else:
|
else:
|
||||||
chat_content_block = "当前没有观察到新的聊天内容。\n"
|
chat_content_block = "当前没有观察到新的聊天内容。\n"
|
||||||
|
|
||||||
# 准备当前思维块
|
# 准备当前思维块 (修改以匹配模板)
|
||||||
current_mind_block = ""
|
current_mind_block = ""
|
||||||
if current_mind:
|
if current_mind:
|
||||||
current_mind_block = f"{current_mind}"
|
# 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
|
||||||
|
current_mind_block = f"你的内心想法:\n{current_mind}"
|
||||||
else:
|
else:
|
||||||
current_mind_block = "[没有特别的想法]"
|
current_mind_block = "你的内心想法:\n[没有特别的想法]"
|
||||||
|
|
||||||
# 准备循环信息块 (分析最近的活动循环)
|
# 准备循环信息块 (分析最近的活动循环)
|
||||||
recent_active_cycles = []
|
recent_active_cycles = []
|
||||||
|
|
@ -1305,23 +1276,40 @@ class HeartFChatting:
|
||||||
|
|
||||||
# 包装提示块,增加可读性,即使没有连续回复也给个标记
|
# 包装提示块,增加可读性,即使没有连续回复也给个标记
|
||||||
if cycle_info_block:
|
if cycle_info_block:
|
||||||
|
# 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
|
||||||
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
|
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
|
||||||
else:
|
else:
|
||||||
# 如果最近的活动循环不是文本回复,或者没有活动循环
|
# 如果最近的活动循环不是文本回复,或者没有活动循环
|
||||||
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
|
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
|
||||||
|
|
||||||
individuality = Individuality.get_instance()
|
individuality = Individuality.get_instance()
|
||||||
|
# 模板中占位符是 {prompt_personality}
|
||||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||||
|
|
||||||
# 获取提示词模板并填充数据
|
# --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
|
||||||
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format(
|
action_options_text = "当前你可以选择的行动有:\n"
|
||||||
|
action_keys = list(current_available_actions.keys())
|
||||||
|
for name in action_keys:
|
||||||
|
desc = current_available_actions[name]
|
||||||
|
action_options_text += f"- '{name}': {desc}\n"
|
||||||
|
|
||||||
|
# --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
|
||||||
|
example_action_key = action_keys[0] if action_keys else "no_reply"
|
||||||
|
|
||||||
|
# --- 获取提示词模板 ---
|
||||||
|
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
|
||||||
|
|
||||||
|
# --- 填充模板 ---
|
||||||
|
prompt = planner_prompt_template.format(
|
||||||
bot_name=global_config.BOT_NICKNAME,
|
bot_name=global_config.BOT_NICKNAME,
|
||||||
prompt_personality=prompt_personality,
|
prompt_personality=prompt_personality,
|
||||||
structured_info_block=structured_info_block,
|
structured_info_block=structured_info_block,
|
||||||
chat_content_block=chat_content_block,
|
chat_content_block=chat_content_block,
|
||||||
current_mind_block=current_mind_block,
|
current_mind_block=current_mind_block,
|
||||||
replan=replan_prompt,
|
replan="", # 暂时留空 replan 信息
|
||||||
cycle_info_block=cycle_info_block,
|
cycle_info_block=cycle_info_block,
|
||||||
|
action_options_text=action_options_text, # 传入可用动作描述
|
||||||
|
example_action=example_action_key, # 传入示例动作键
|
||||||
)
|
)
|
||||||
|
|
||||||
return prompt
|
return prompt
|
||||||
|
|
@ -1329,7 +1317,7 @@ class HeartFChatting:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
|
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return ""
|
return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
|
||||||
|
|
||||||
# --- 回复器 (Replier) 的定义 --- #
|
# --- 回复器 (Replier) 的定义 --- #
|
||||||
async def _replier_work(
|
async def _replier_work(
|
||||||
|
|
@ -1370,7 +1358,7 @@ class HeartFChatting:
|
||||||
try:
|
try:
|
||||||
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
||||||
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
|
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
|
||||||
logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
|
# logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
|
||||||
# 捕捉 LLM 输出信息
|
# 捕捉 LLM 输出信息
|
||||||
info_catcher.catch_after_llm_generated(
|
info_catcher.catch_after_llm_generated(
|
||||||
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
|
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# src/plugins/heartFC_chat/heartFC_sender.py
|
# src/plugins/heartFC_chat/heartFC_sender.py
|
||||||
import asyncio # 重新导入 asyncio
|
import asyncio # 重新导入 asyncio
|
||||||
from typing import Dict, Optional # 重新导入类型
|
from typing import Dict, Optional # 重新导入类型
|
||||||
from ..message.api import global_api
|
|
||||||
from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking
|
from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking
|
||||||
from ..storage.storage import MessageStorage
|
from ..storage.storage import MessageStorage
|
||||||
from ..chat.utils import truncate_message
|
from ..chat.utils import truncate_message
|
||||||
|
|
@ -12,6 +11,22 @@ from src.plugins.chat.utils import calculate_typing_time
|
||||||
logger = get_logger("sender")
|
logger = get_logger("sender")
|
||||||
|
|
||||||
|
|
||||||
|
async def send_message(message: MessageSending) -> None:
|
||||||
|
"""合并后的消息发送函数,包含WS发送和日志记录"""
|
||||||
|
message_preview = truncate_message(message.processed_plain_text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 直接调用API发送消息
|
||||||
|
await send_message(message)
|
||||||
|
logger.success(f"发送消息 '{message_preview}' 成功")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
||||||
|
if not message.message_info.platform:
|
||||||
|
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||||
|
raise e # 重新抛出其他异常
|
||||||
|
|
||||||
|
|
||||||
class HeartFCSender:
|
class HeartFCSender:
|
||||||
"""管理消息的注册、即时处理、发送和存储,并跟踪思考状态。"""
|
"""管理消息的注册、即时处理、发送和存储,并跟踪思考状态。"""
|
||||||
|
|
||||||
|
|
@ -21,21 +36,6 @@ class HeartFCSender:
|
||||||
self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {}
|
self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {}
|
||||||
self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁
|
self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁
|
||||||
|
|
||||||
async def send_message(self, message: MessageSending) -> None:
|
|
||||||
"""合并后的消息发送函数,包含WS发送和日志记录"""
|
|
||||||
message_preview = truncate_message(message.processed_plain_text)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 直接调用API发送消息
|
|
||||||
await global_api.send_message(message)
|
|
||||||
logger.success(f"发送消息 '{message_preview}' 成功")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
|
|
||||||
if not message.message_info.platform:
|
|
||||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
|
||||||
raise e # 重新抛出其他异常
|
|
||||||
|
|
||||||
async def register_thinking(self, thinking_message: MessageThinking):
|
async def register_thinking(self, thinking_message: MessageThinking):
|
||||||
"""注册一个思考中的消息。"""
|
"""注册一个思考中的消息。"""
|
||||||
if not thinking_message.chat_stream or not thinking_message.message_info.message_id:
|
if not thinking_message.chat_stream or not thinking_message.message_info.message_id:
|
||||||
|
|
@ -73,7 +73,7 @@ class HeartFCSender:
|
||||||
thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
|
thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
|
||||||
return thinking_message.thinking_start_time if thinking_message else None
|
return thinking_message.thinking_start_time if thinking_message else None
|
||||||
|
|
||||||
async def type_and_send_message(self, message: MessageSending, type=False):
|
async def type_and_send_message(self, message: MessageSending, typing=False):
|
||||||
"""
|
"""
|
||||||
立即处理、发送并存储单个 MessageSending 消息。
|
立即处理、发送并存储单个 MessageSending 消息。
|
||||||
调用此方法前,应先调用 register_thinking 注册对应的思考消息。
|
调用此方法前,应先调用 register_thinking 注册对应的思考消息。
|
||||||
|
|
@ -100,7 +100,7 @@ class HeartFCSender:
|
||||||
|
|
||||||
await message.process()
|
await message.process()
|
||||||
|
|
||||||
if type:
|
if typing:
|
||||||
typing_time = calculate_typing_time(
|
typing_time = calculate_typing_time(
|
||||||
input_string=message.processed_plain_text,
|
input_string=message.processed_plain_text,
|
||||||
thinking_start_time=message.thinking_start_time,
|
thinking_start_time=message.thinking_start_time,
|
||||||
|
|
@ -108,7 +108,7 @@ class HeartFCSender:
|
||||||
)
|
)
|
||||||
await asyncio.sleep(typing_time)
|
await asyncio.sleep(typing_time)
|
||||||
|
|
||||||
await self.send_message(message)
|
await send_message(message)
|
||||||
await self.storage.store_message(message, message.chat_stream)
|
await self.storage.store_message(message, message.chat_stream)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -136,7 +136,7 @@ class HeartFCSender:
|
||||||
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
await self.send_message(message) # 使用现有的发送方法
|
await send_message(message) # 使用现有的发送方法
|
||||||
await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法
|
await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,134 @@ from ..chat.chat_stream import chat_manager
|
||||||
from ..chat.message_buffer import message_buffer
|
from ..chat.message_buffer import message_buffer
|
||||||
from ..utils.timer_calculator import Timer
|
from ..utils.timer_calculator import Timer
|
||||||
from src.plugins.person_info.relationship_manager import relationship_manager
|
from src.plugins.person_info.relationship_manager import relationship_manager
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, Dict, Any
|
||||||
|
|
||||||
logger = get_logger("chat")
|
logger = get_logger("chat")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_error(error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
|
||||||
|
"""统一的错误处理函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error: 捕获到的异常
|
||||||
|
context: 错误发生的上下文描述
|
||||||
|
message: 可选的消息对象,用于记录相关消息内容
|
||||||
|
"""
|
||||||
|
logger.error(f"{context}: {error}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
if message and hasattr(message, "raw_message"):
|
||||||
|
logger.error(f"相关消息原始内容: {message.raw_message}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _process_relationship(message: MessageRecv) -> None:
|
||||||
|
"""处理用户关系逻辑
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 消息对象,包含用户信息
|
||||||
|
"""
|
||||||
|
platform = message.message_info.platform
|
||||||
|
user_id = message.message_info.user_info.user_id
|
||||||
|
nickname = message.message_info.user_info.user_nickname
|
||||||
|
cardname = message.message_info.user_info.user_cardname or nickname
|
||||||
|
|
||||||
|
is_known = await relationship_manager.is_known_some_one(platform, user_id)
|
||||||
|
|
||||||
|
if not is_known:
|
||||||
|
logger.info(f"首次认识用户: {nickname}")
|
||||||
|
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
||||||
|
elif not await relationship_manager.is_qved_name(platform, user_id):
|
||||||
|
logger.info(f"给用户({nickname},{cardname})取名: {nickname}")
|
||||||
|
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
||||||
|
|
||||||
|
|
||||||
|
async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
|
||||||
|
"""计算消息的兴趣度
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 待处理的消息对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[float, bool]: (兴趣度, 是否被提及)
|
||||||
|
"""
|
||||||
|
is_mentioned, _ = is_mentioned_bot_in_message(message)
|
||||||
|
interested_rate = 0.0
|
||||||
|
|
||||||
|
with Timer("记忆激活"):
|
||||||
|
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
|
||||||
|
message.processed_plain_text,
|
||||||
|
fast_retrieval=True,
|
||||||
|
)
|
||||||
|
logger.trace(f"记忆激活率: {interested_rate:.2f}")
|
||||||
|
|
||||||
|
if is_mentioned:
|
||||||
|
interest_increase_on_mention = 1
|
||||||
|
interested_rate += interest_increase_on_mention
|
||||||
|
|
||||||
|
return interested_rate, is_mentioned
|
||||||
|
|
||||||
|
|
||||||
|
def _get_message_type(message: MessageRecv) -> str:
|
||||||
|
"""获取消息类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 消息对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 消息类型
|
||||||
|
"""
|
||||||
|
if message.message_segment.type != "seglist":
|
||||||
|
return message.message_segment.type
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(message.message_segment.data, list)
|
||||||
|
and all(isinstance(x, Seg) for x in message.message_segment.data)
|
||||||
|
and len(message.message_segment.data) == 1
|
||||||
|
):
|
||||||
|
return message.message_segment.data[0].type
|
||||||
|
|
||||||
|
return "seglist"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_ban_words(text: str, chat, userinfo) -> bool:
|
||||||
|
"""检查消息是否包含过滤词
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 待检查的文本
|
||||||
|
chat: 聊天对象
|
||||||
|
userinfo: 用户信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否包含过滤词
|
||||||
|
"""
|
||||||
|
for word in global_config.ban_words:
|
||||||
|
if word in text:
|
||||||
|
chat_name = chat.group_info.group_name if chat.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, userinfo) -> bool:
|
||||||
|
"""检查消息是否匹配过滤正则表达式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 待检查的文本
|
||||||
|
chat: 聊天对象
|
||||||
|
userinfo: 用户信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否匹配过滤正则
|
||||||
|
"""
|
||||||
|
for pattern in global_config.ban_msgs_regex:
|
||||||
|
if pattern.search(text):
|
||||||
|
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
|
||||||
|
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
||||||
|
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class HeartFCProcessor:
|
class HeartFCProcessor:
|
||||||
"""心流处理器,负责处理接收到的消息并计算兴趣度"""
|
"""心流处理器,负责处理接收到的消息并计算兴趣度"""
|
||||||
|
|
||||||
|
|
@ -24,86 +147,7 @@ class HeartFCProcessor:
|
||||||
"""初始化心流处理器,创建消息存储实例"""
|
"""初始化心流处理器,创建消息存储实例"""
|
||||||
self.storage = MessageStorage()
|
self.storage = MessageStorage()
|
||||||
|
|
||||||
async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
|
async def process_message(self, message_data: Dict[str, Any]) -> None:
|
||||||
"""统一的错误处理函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: 捕获到的异常
|
|
||||||
context: 错误发生的上下文描述
|
|
||||||
message: 可选的消息对象,用于记录相关消息内容
|
|
||||||
"""
|
|
||||||
logger.error(f"{context}: {error}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
if message and hasattr(message, "raw_message"):
|
|
||||||
logger.error(f"相关消息原始内容: {message.raw_message}")
|
|
||||||
|
|
||||||
async def _process_relationship(self, message: MessageRecv) -> None:
|
|
||||||
"""处理用户关系逻辑
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 消息对象,包含用户信息
|
|
||||||
"""
|
|
||||||
platform = message.message_info.platform
|
|
||||||
user_id = message.message_info.user_info.user_id
|
|
||||||
nickname = message.message_info.user_info.user_nickname
|
|
||||||
cardname = message.message_info.user_info.user_cardname or nickname
|
|
||||||
|
|
||||||
is_known = await relationship_manager.is_known_some_one(platform, user_id)
|
|
||||||
|
|
||||||
if not is_known:
|
|
||||||
logger.info(f"首次认识用户: {nickname}")
|
|
||||||
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
|
||||||
elif not await relationship_manager.is_qved_name(platform, user_id):
|
|
||||||
logger.info(f"给用户({nickname},{cardname})取名: {nickname}")
|
|
||||||
await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
|
|
||||||
|
|
||||||
async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]:
|
|
||||||
"""计算消息的兴趣度
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 待处理的消息对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[float, bool]: (兴趣度, 是否被提及)
|
|
||||||
"""
|
|
||||||
is_mentioned, _ = is_mentioned_bot_in_message(message)
|
|
||||||
interested_rate = 0.0
|
|
||||||
|
|
||||||
with Timer("记忆激活"):
|
|
||||||
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
|
|
||||||
message.processed_plain_text,
|
|
||||||
fast_retrieval=True,
|
|
||||||
)
|
|
||||||
logger.trace(f"记忆激活率: {interested_rate:.2f}")
|
|
||||||
|
|
||||||
if is_mentioned:
|
|
||||||
interest_increase_on_mention = 1
|
|
||||||
interested_rate += interest_increase_on_mention
|
|
||||||
|
|
||||||
return interested_rate, is_mentioned
|
|
||||||
|
|
||||||
def _get_message_type(self, message: MessageRecv) -> str:
|
|
||||||
"""获取消息类型
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 消息对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 消息类型
|
|
||||||
"""
|
|
||||||
if message.message_segment.type != "seglist":
|
|
||||||
return message.message_segment.type
|
|
||||||
|
|
||||||
if (
|
|
||||||
isinstance(message.message_segment.data, list)
|
|
||||||
and all(isinstance(x, Seg) for x in message.message_segment.data)
|
|
||||||
and len(message.message_segment.data) == 1
|
|
||||||
):
|
|
||||||
return message.message_segment.data[0].type
|
|
||||||
|
|
||||||
return "seglist"
|
|
||||||
|
|
||||||
async def process_message(self, message_data: str) -> None:
|
|
||||||
"""处理接收到的原始消息数据
|
"""处理接收到的原始消息数据
|
||||||
|
|
||||||
主要流程:
|
主要流程:
|
||||||
|
|
@ -138,7 +182,7 @@ class HeartFCProcessor:
|
||||||
await message.process()
|
await message.process()
|
||||||
|
|
||||||
# 3. 过滤检查
|
# 3. 过滤检查
|
||||||
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
|
if _check_ban_words(message.processed_plain_text, chat, userinfo) or _check_ban_regex(
|
||||||
message.raw_message, chat, userinfo
|
message.raw_message, chat, userinfo
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
@ -146,7 +190,7 @@ class HeartFCProcessor:
|
||||||
# 4. 缓冲检查
|
# 4. 缓冲检查
|
||||||
buffer_result = await message_buffer.query_buffer_result(message)
|
buffer_result = await message_buffer.query_buffer_result(message)
|
||||||
if not buffer_result:
|
if not buffer_result:
|
||||||
msg_type = self._get_message_type(message)
|
msg_type = _get_message_type(message)
|
||||||
type_messages = {
|
type_messages = {
|
||||||
"text": f"触发缓冲,消息:{message.processed_plain_text}",
|
"text": f"触发缓冲,消息:{message.processed_plain_text}",
|
||||||
"image": "触发缓冲,表情包/图片等待中",
|
"image": "触发缓冲,表情包/图片等待中",
|
||||||
|
|
@ -160,7 +204,7 @@ class HeartFCProcessor:
|
||||||
logger.trace(f"存储成功: {message.processed_plain_text}")
|
logger.trace(f"存储成功: {message.processed_plain_text}")
|
||||||
|
|
||||||
# 6. 兴趣度计算与更新
|
# 6. 兴趣度计算与更新
|
||||||
interested_rate, is_mentioned = await self._calculate_interest(message)
|
interested_rate, is_mentioned = await _calculate_interest(message)
|
||||||
await subheartflow.interest_chatting.increase_interest(value=interested_rate)
|
await subheartflow.interest_chatting.increase_interest(value=interested_rate)
|
||||||
subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned)
|
subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned)
|
||||||
|
|
||||||
|
|
@ -175,45 +219,7 @@ class HeartFCProcessor:
|
||||||
)
|
)
|
||||||
|
|
||||||
# 8. 关系处理
|
# 8. 关系处理
|
||||||
await self._process_relationship(message)
|
await _process_relationship(message)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self._handle_error(e, "消息处理失败", message)
|
await _handle_error(e, "消息处理失败", message)
|
||||||
|
|
||||||
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
|
|
||||||
"""检查消息是否包含过滤词
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: 待检查的文本
|
|
||||||
chat: 聊天对象
|
|
||||||
userinfo: 用户信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否包含过滤词
|
|
||||||
"""
|
|
||||||
for word in global_config.ban_words:
|
|
||||||
if word in text:
|
|
||||||
chat_name = chat.group_info.group_name if chat.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(self, text: str, chat, userinfo) -> bool:
|
|
||||||
"""检查消息是否匹配过滤正则表达式
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: 待检查的文本
|
|
||||||
chat: 聊天对象
|
|
||||||
userinfo: 用户信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否匹配过滤正则
|
|
||||||
"""
|
|
||||||
for pattern in global_config.ban_msgs_regex:
|
|
||||||
if pattern.search(text):
|
|
||||||
chat_name = chat.group_info.group_name if chat.group_info else "私聊"
|
|
||||||
logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
|
|
||||||
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -49,17 +49,15 @@ def init_prompt():
|
||||||
"info_from_tools",
|
"info_from_tools",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Planner提示词 - 优化版
|
# Planner提示词 - 修改为要求 JSON 输出
|
||||||
Prompt(
|
Prompt(
|
||||||
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
|
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
|
||||||
{structured_info_block}
|
{structured_info_block}
|
||||||
{chat_content_block}
|
{chat_content_block}
|
||||||
你的内心想法:
|
|
||||||
{current_mind_block}
|
{current_mind_block}
|
||||||
{replan}
|
|
||||||
{cycle_info_block}
|
{cycle_info_block}
|
||||||
|
|
||||||
请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意:
|
请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
|
||||||
|
|
||||||
【回复原则】
|
【回复原则】
|
||||||
1. 不回复(no_reply)适用:
|
1. 不回复(no_reply)适用:
|
||||||
|
|
@ -83,14 +81,34 @@ def init_prompt():
|
||||||
- 避免重复或评价自己的发言
|
- 避免重复或评价自己的发言
|
||||||
- 不要和自己聊天
|
- 不要和自己聊天
|
||||||
|
|
||||||
【必须遵守】
|
【决策任务】
|
||||||
- 遵守回复原则
|
{action_options_text}
|
||||||
- 必须调用工具并包含action和reasoning
|
|
||||||
- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply)
|
你必须从上面列出的可用行动中选择一个,并说明原因。
|
||||||
- 并不是所有选择都可用
|
你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
|
||||||
- 选择text_reply或emoji_reply时必须提供emoji_query
|
JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
|
||||||
- 保持回复自然,符合日常聊天习惯""",
|
{{
|
||||||
"planner_prompt",
|
"action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
|
||||||
|
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
|
||||||
|
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
|
||||||
|
}}
|
||||||
|
|
||||||
|
例如:
|
||||||
|
{{
|
||||||
|
"action": "text_reply",
|
||||||
|
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
|
||||||
|
"emoji_query": "微笑"
|
||||||
|
}}
|
||||||
|
或
|
||||||
|
{{
|
||||||
|
"action": "no_reply",
|
||||||
|
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
|
||||||
|
"emoji_query": ""
|
||||||
|
}}
|
||||||
|
|
||||||
|
请输出你的决策 JSON:
|
||||||
|
""", # 使用三引号避免内部引号问题
|
||||||
|
"planner_prompt", # 保持名称不变,替换内容
|
||||||
)
|
)
|
||||||
|
|
||||||
Prompt(
|
Prompt(
|
||||||
|
|
@ -135,6 +153,126 @@ def init_prompt():
|
||||||
Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
|
Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> tuple[str, str]:
|
||||||
|
individuality = Individuality.get_instance()
|
||||||
|
prompt_personality = individuality.get_prompt(x_person=0, level=2)
|
||||||
|
# 日程构建
|
||||||
|
# schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}'''
|
||||||
|
|
||||||
|
if chat_stream.group_info:
|
||||||
|
chat_in_group = True
|
||||||
|
else:
|
||||||
|
chat_in_group = False
|
||||||
|
|
||||||
|
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||||||
|
chat_id=chat_stream.stream_id,
|
||||||
|
timestamp=time.time(),
|
||||||
|
limit=global_config.observation_context_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_talking_prompt = await build_readable_messages(
|
||||||
|
message_list_before_now,
|
||||||
|
replace_bot_name=True,
|
||||||
|
merge_messages=False,
|
||||||
|
timestamp_mode="normal",
|
||||||
|
read_mark=0.0,
|
||||||
|
truncate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 中文高手(新加的好玩功能)
|
||||||
|
prompt_ger = ""
|
||||||
|
if random.random() < 0.04:
|
||||||
|
prompt_ger += "你喜欢用倒装句"
|
||||||
|
if random.random() < 0.02:
|
||||||
|
prompt_ger += "你喜欢用反问句"
|
||||||
|
|
||||||
|
reply_styles1 = [
|
||||||
|
("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率
|
||||||
|
("给出非常简短的回复", 0.4), # 40%概率
|
||||||
|
("给出缺失主语的回复,简短", 0.15), # 15%概率
|
||||||
|
("给出带有语病的回复,朴实平淡", 0.05), # 5%概率
|
||||||
|
]
|
||||||
|
reply_style1_chosen = random.choices(
|
||||||
|
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
reply_styles2 = [
|
||||||
|
("不要回复的太有条理,可以有个性", 0.6), # 60%概率
|
||||||
|
("不要回复的太有条理,可以复读", 0.15), # 15%概率
|
||||||
|
("回复的认真一些", 0.2), # 20%概率
|
||||||
|
("可以回复单个表情符号", 0.05), # 5%概率
|
||||||
|
]
|
||||||
|
reply_style2_chosen = random.choices(
|
||||||
|
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
if structured_info:
|
||||||
|
structured_info_prompt = await global_prompt_manager.format_prompt(
|
||||||
|
"info_from_tools", structured_info=structured_info
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
structured_info_prompt = ""
|
||||||
|
|
||||||
|
logger.debug("开始构建prompt")
|
||||||
|
|
||||||
|
# 注入绰号信息
|
||||||
|
nickname_injection_str = ""
|
||||||
|
if global_config.ENABLE_NICKNAME_MAPPING and chat_stream.group_info:
|
||||||
|
try:
|
||||||
|
group_id = str(chat_stream.group_info.group_id)
|
||||||
|
user_ids_in_context = set()
|
||||||
|
if message_list_before_now:
|
||||||
|
for msg in message_list_before_now:
|
||||||
|
sender_id = msg["user_info"].get('user_id')
|
||||||
|
if sender_id:
|
||||||
|
user_ids_in_context.add(str(sender_id))
|
||||||
|
else:
|
||||||
|
logger.warning("Variable 'message_list_before_now' not found for nickname injection in focus prompt.")
|
||||||
|
|
||||||
|
|
||||||
|
if user_ids_in_context:
|
||||||
|
platform = chat_stream.platform
|
||||||
|
# --- 调用批量获取群组绰号的方法 ---
|
||||||
|
all_nicknames_data = await relationship_manager.get_users_group_nicknames(
|
||||||
|
platform, list(user_ids_in_context), group_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_nicknames_data:
|
||||||
|
selected_nicknames = select_nicknames_for_prompt(all_nicknames_data)
|
||||||
|
nickname_injection_str = format_nickname_prompt_injection(selected_nicknames)
|
||||||
|
if nickname_injection_str:
|
||||||
|
logger.debug(f"Injecting nickname info into focus prompt:\n{nickname_injection_str}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting or formatting nickname info for focus prompt: {e}", exc_info=True)
|
||||||
|
logger.debug(f"-------------------nickname_injection_str_______________________\n{nickname_injection_str}\n\n")
|
||||||
|
|
||||||
|
prompt = await global_prompt_manager.format_prompt(
|
||||||
|
"heart_flow_prompt",
|
||||||
|
info_from_tools=structured_info_prompt,
|
||||||
|
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||||||
|
if chat_in_group
|
||||||
|
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
|
||||||
|
chat_talking_prompt=chat_talking_prompt,
|
||||||
|
bot_name=global_config.BOT_NICKNAME,
|
||||||
|
prompt_personality=prompt_personality,
|
||||||
|
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||||||
|
if chat_in_group
|
||||||
|
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
|
||||||
|
current_mind_info=current_mind_info,
|
||||||
|
reply_style2=reply_style2_chosen,
|
||||||
|
reply_style1=reply_style1_chosen,
|
||||||
|
reason=reason,
|
||||||
|
prompt_ger=prompt_ger,
|
||||||
|
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
|
||||||
|
sender_name=sender_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"focus_chat_prompt: \n{prompt}")
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
|
||||||
class PromptBuilder:
|
class PromptBuilder:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.prompt_built = ""
|
self.prompt_built = ""
|
||||||
|
|
@ -154,132 +292,15 @@ class PromptBuilder:
|
||||||
return await self._build_prompt_normal(chat_stream, message_txt, sender_name)
|
return await self._build_prompt_normal(chat_stream, message_txt, sender_name)
|
||||||
|
|
||||||
elif build_mode == "focus":
|
elif build_mode == "focus":
|
||||||
return await self._build_prompt_focus(
|
return await _build_prompt_focus(
|
||||||
reason,
|
reason,
|
||||||
current_mind_info,
|
current_mind_info,
|
||||||
structured_info,
|
structured_info,
|
||||||
chat_stream,
|
chat_stream,
|
||||||
|
sender_name,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]:
|
|
||||||
individuality = Individuality.get_instance()
|
|
||||||
prompt_personality = individuality.get_prompt(x_person=0, level=2)
|
|
||||||
# 日程构建
|
|
||||||
# schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}'''
|
|
||||||
|
|
||||||
if chat_stream.group_info:
|
|
||||||
chat_in_group = True
|
|
||||||
else:
|
|
||||||
chat_in_group = False
|
|
||||||
|
|
||||||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
|
||||||
chat_id=chat_stream.stream_id,
|
|
||||||
timestamp=time.time(),
|
|
||||||
limit=global_config.observation_context_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
chat_talking_prompt = await build_readable_messages(
|
|
||||||
message_list_before_now,
|
|
||||||
replace_bot_name=True,
|
|
||||||
merge_messages=False,
|
|
||||||
timestamp_mode="normal",
|
|
||||||
read_mark=0.0,
|
|
||||||
truncate=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 中文高手(新加的好玩功能)
|
|
||||||
prompt_ger = ""
|
|
||||||
if random.random() < 0.04:
|
|
||||||
prompt_ger += "你喜欢用倒装句"
|
|
||||||
if random.random() < 0.02:
|
|
||||||
prompt_ger += "你喜欢用反问句"
|
|
||||||
|
|
||||||
reply_styles1 = [
|
|
||||||
("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率
|
|
||||||
("给出非常简短的回复", 0.4), # 40%概率
|
|
||||||
("给出缺失主语的回复,简短", 0.15), # 15%概率
|
|
||||||
("给出带有语病的回复,朴实平淡", 0.05), # 5%概率
|
|
||||||
]
|
|
||||||
reply_style1_chosen = random.choices(
|
|
||||||
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
reply_styles2 = [
|
|
||||||
("不要回复的太有条理,可以有个性", 0.6), # 60%概率
|
|
||||||
("不要回复的太有条理,可以复读", 0.15), # 15%概率
|
|
||||||
("回复的认真一些", 0.2), # 20%概率
|
|
||||||
("可以回复单个表情符号", 0.05), # 5%概率
|
|
||||||
]
|
|
||||||
reply_style2_chosen = random.choices(
|
|
||||||
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
if structured_info:
|
|
||||||
structured_info_prompt = await global_prompt_manager.format_prompt(
|
|
||||||
"info_from_tools", structured_info=structured_info
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
structured_info_prompt = ""
|
|
||||||
|
|
||||||
logger.debug("开始构建prompt")
|
|
||||||
|
|
||||||
# 注入绰号信息
|
|
||||||
nickname_injection_str = ""
|
|
||||||
if global_config.ENABLE_NICKNAME_MAPPING and chat_stream.group_info:
|
|
||||||
try:
|
|
||||||
group_id = str(chat_stream.group_info.group_id)
|
|
||||||
user_ids_in_context = set()
|
|
||||||
if message_list_before_now:
|
|
||||||
for msg in message_list_before_now:
|
|
||||||
sender_id = msg["user_info"].get('user_id')
|
|
||||||
if sender_id:
|
|
||||||
user_ids_in_context.add(str(sender_id))
|
|
||||||
else:
|
|
||||||
logger.warning("Variable 'message_list_before_now' not found for nickname injection in focus prompt.")
|
|
||||||
|
|
||||||
|
|
||||||
if user_ids_in_context:
|
|
||||||
platform = chat_stream.platform
|
|
||||||
# --- 调用批量获取群组绰号的方法 ---
|
|
||||||
all_nicknames_data = await relationship_manager.get_users_group_nicknames(
|
|
||||||
platform, list(user_ids_in_context), group_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if all_nicknames_data:
|
|
||||||
selected_nicknames = select_nicknames_for_prompt(all_nicknames_data)
|
|
||||||
nickname_injection_str = format_nickname_prompt_injection(selected_nicknames)
|
|
||||||
if nickname_injection_str:
|
|
||||||
logger.debug(f"Injecting nickname info into focus prompt:\n{nickname_injection_str}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting or formatting nickname info for focus prompt: {e}", exc_info=True)
|
|
||||||
logger.debug(f"-------------------nickname_injection_str_______________________\n{nickname_injection_str}\n\n")
|
|
||||||
|
|
||||||
prompt = await global_prompt_manager.format_prompt(
|
|
||||||
"heart_flow_prompt",
|
|
||||||
info_from_tools=structured_info_prompt,
|
|
||||||
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
|
|
||||||
if chat_in_group
|
|
||||||
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
|
|
||||||
chat_talking_prompt=chat_talking_prompt,
|
|
||||||
bot_name=global_config.BOT_NICKNAME,
|
|
||||||
prompt_personality=prompt_personality,
|
|
||||||
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
|
|
||||||
if chat_in_group
|
|
||||||
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
|
|
||||||
current_mind_info=current_mind_info,
|
|
||||||
reply_style2=reply_style2_chosen,
|
|
||||||
reply_style1=reply_style1_chosen,
|
|
||||||
reason=reason,
|
|
||||||
prompt_ger=prompt_ger,
|
|
||||||
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"focus_chat_prompt: \n{prompt}")
|
|
||||||
|
|
||||||
return prompt
|
|
||||||
|
|
||||||
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
|
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
|
||||||
individuality = Individuality.get_instance()
|
individuality = Individuality.get_instance()
|
||||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,9 @@ class NormalChat:
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
# --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 ---
|
# --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 ---
|
||||||
messages_to_process_initially = list(messages_to_reply) # 创建副本
|
messages_to_process_initially = list(messages_to_reply) # 创建副本
|
||||||
# --- 修改结束 ---
|
# --- 新增:限制最多处理两条消息 ---
|
||||||
|
messages_to_process_initially = messages_to_process_initially[:2]
|
||||||
|
# --- 新增结束 ---
|
||||||
for item in messages_to_process_initially: # 使用副本迭代
|
for item in messages_to_process_initially: # 使用副本迭代
|
||||||
msg_id, (message, interest_value, is_mentioned) = item
|
msg_id, (message, interest_value, is_mentioned) = item
|
||||||
# --- 修改:在处理前尝试 pop,防止竞争 ---
|
# --- 修改:在处理前尝试 pop,防止竞争 ---
|
||||||
|
|
@ -443,7 +445,7 @@ class NormalChat:
|
||||||
logger.error(f"[{self.stream_name}] 任务异常: {exc}")
|
logger.error(f"[{self.stream_name}] 任务异常: {exc}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info(f"[{self.stream_name}] 任务已取消")
|
logger.debug(f"[{self.stream_name}] 任务已取消")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
|
logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -456,12 +458,12 @@ class NormalChat:
|
||||||
"""停止当前实例的兴趣监控任务。"""
|
"""停止当前实例的兴趣监控任务。"""
|
||||||
if self._chat_task and not self._chat_task.done():
|
if self._chat_task and not self._chat_task.done():
|
||||||
task = self._chat_task
|
task = self._chat_task
|
||||||
logger.info(f"[{self.stream_name}] 尝试取消聊天任务。")
|
logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。")
|
||||||
task.cancel()
|
task.cancel()
|
||||||
try:
|
try:
|
||||||
await task # 等待任务响应取消
|
await task # 等待任务响应取消
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info(f"[{self.stream_name}] 聊天任务已成功取消。")
|
logger.info(f"[{self.stream_name}] 结束一般聊天模式。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 回调函数 _handle_task_completion 会处理异常日志
|
# 回调函数 _handle_task_completion 会处理异常日志
|
||||||
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")
|
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,14 @@ class NormalChatGenerator:
|
||||||
sender_name=sender_name,
|
sender_name=sender_name,
|
||||||
chat_stream=message.chat_stream,
|
chat_stream=message.chat_stream,
|
||||||
)
|
)
|
||||||
logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
|
logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
|
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
|
||||||
|
|
||||||
logger.info(f"prompt:{prompt}\n生成回复:{content}")
|
logger.debug(f"prompt:{prompt}\n生成回复:{content}")
|
||||||
|
|
||||||
|
logger.info(f"对 {message.processed_plain_text} 的回复:{content}")
|
||||||
|
|
||||||
info_catcher.catch_after_llm_generated(
|
info_catcher.catch_after_llm_generated(
|
||||||
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
|
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from .llm_client import LLMMessage
|
from .llm_client import LLMMessage
|
||||||
|
|
||||||
entity_extract_system_prompt = """你是一个性能优异的实体提取系统。请从段落中提取出所有实体,并以JSON列表的形式输出。
|
entity_extract_system_prompt = """你是一个性能优异的实体提取系统。请从段落中提取出所有实体,并以JSON列表的形式输出。
|
||||||
|
|
@ -13,7 +11,7 @@ entity_extract_system_prompt = """你是一个性能优异的实体提取系统
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def build_entity_extract_context(paragraph: str) -> List[LLMMessage]:
|
def build_entity_extract_context(paragraph: str) -> list[LLMMessage]:
|
||||||
messages = [
|
messages = [
|
||||||
LLMMessage("system", entity_extract_system_prompt).to_dict(),
|
LLMMessage("system", entity_extract_system_prompt).to_dict(),
|
||||||
LLMMessage("user", f"""段落:\n```\n{paragraph}```""").to_dict(),
|
LLMMessage("user", f"""段落:\n```\n{paragraph}```""").to_dict(),
|
||||||
|
|
@ -38,7 +36,7 @@ rdf_triple_extract_system_prompt = """你是一个性能优异的RDF(资源描
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def build_rdf_triple_extract_context(paragraph: str, entities: str) -> List[LLMMessage]:
|
def build_rdf_triple_extract_context(paragraph: str, entities: str) -> list[LLMMessage]:
|
||||||
messages = [
|
messages = [
|
||||||
LLMMessage("system", rdf_triple_extract_system_prompt).to_dict(),
|
LLMMessage("system", rdf_triple_extract_system_prompt).to_dict(),
|
||||||
LLMMessage("user", f"""段落:\n```\n{paragraph}```\n\n实体列表:\n```\n{entities}```""").to_dict(),
|
LLMMessage("user", f"""段落:\n```\n{paragraph}```\n\n实体列表:\n```\n{entities}```""").to_dict(),
|
||||||
|
|
@ -56,7 +54,7 @@ qa_system_prompt = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def build_qa_context(question: str, knowledge: list[(str, str, str)]) -> List[LLMMessage]:
|
def build_qa_context(question: str, knowledge: list[tuple[str, str, str]]) -> list[LLMMessage]:
|
||||||
knowledge = "\n".join([f"{i + 1}. 相关性:{k[0]}\n{k[1]}" for i, k in enumerate(knowledge)])
|
knowledge = "\n".join([f"{i + 1}. 相关性:{k[0]}\n{k[1]}" for i, k in enumerate(knowledge)])
|
||||||
messages = [
|
messages = [
|
||||||
LLMMessage("system", qa_system_prompt).to_dict(),
|
LLMMessage("system", qa_system_prompt).to_dict(),
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class QAManager:
|
||||||
self.kg_manager = kg_manager
|
self.kg_manager = kg_manager
|
||||||
self.llm_client_list = {
|
self.llm_client_list = {
|
||||||
"embedding": llm_client_embedding,
|
"embedding": llm_client_embedding,
|
||||||
"filter": llm_client_filter,
|
"message_filter": llm_client_filter,
|
||||||
"qa": llm_client_qa,
|
"qa": llm_client_qa,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,6 @@ class Hippocampus:
|
||||||
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
|
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
|
||||||
|
|
||||||
# 从每个关键词获取记忆
|
# 从每个关键词获取记忆
|
||||||
all_memories = []
|
|
||||||
activate_map = {} # 存储每个词的累计激活值
|
activate_map = {} # 存储每个词的累计激活值
|
||||||
|
|
||||||
# 对每个关键词进行扩散式检索
|
# 对每个关键词进行扩散式检索
|
||||||
|
|
@ -511,7 +510,7 @@ class Hippocampus:
|
||||||
"""从文本中提取关键词并获取相关记忆。
|
"""从文本中提取关键词并获取相关记忆。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
topic (str): 记忆主题
|
keywords (list): 输入文本
|
||||||
max_memory_num (int, optional): 返回的记忆条目数量上限。默认为3,表示最多返回3条与输入文本相关度最高的记忆。
|
max_memory_num (int, optional): 返回的记忆条目数量上限。默认为3,表示最多返回3条与输入文本相关度最高的记忆。
|
||||||
max_memory_length (int, optional): 每个主题最多返回的记忆条目数量。默认为2,表示每个主题最多返回2条相似度最高的记忆。
|
max_memory_length (int, optional): 每个主题最多返回的记忆条目数量。默认为2,表示每个主题最多返回2条相似度最高的记忆。
|
||||||
max_depth (int, optional): 记忆检索深度。默认为3。值越大,检索范围越广,可以获取更多间接相关的记忆,但速度会变慢。
|
max_depth (int, optional): 记忆检索深度。默认为3。值越大,检索范围越广,可以获取更多间接相关的记忆,但速度会变慢。
|
||||||
|
|
@ -536,7 +535,6 @@ class Hippocampus:
|
||||||
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
|
logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
|
||||||
|
|
||||||
# 从每个关键词获取记忆
|
# 从每个关键词获取记忆
|
||||||
all_memories = []
|
|
||||||
activate_map = {} # 存储每个词的累计激活值
|
activate_map = {} # 存储每个词的累计激活值
|
||||||
|
|
||||||
# 对每个关键词进行扩散式检索
|
# 对每个关键词进行扩散式检索
|
||||||
|
|
@ -829,7 +827,7 @@ class EntorhinalCortex:
|
||||||
return chat_samples
|
return chat_samples
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list:
|
def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list | None:
|
||||||
"""从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)"""
|
"""从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)"""
|
||||||
try_count = 0
|
try_count = 0
|
||||||
time_window_seconds = random.randint(300, 1800) # 随机时间窗口,5到30分钟
|
time_window_seconds = random.randint(300, 1800) # 随机时间窗口,5到30分钟
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,28 @@ error_code_mapping = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _safely_record(request_content: Dict[str, Any], payload: Dict[str, Any]):
|
||||||
|
image_base64: str = request_content.get("image_base64")
|
||||||
|
image_format: str = request_content.get("image_format")
|
||||||
|
if (
|
||||||
|
image_base64
|
||||||
|
and payload
|
||||||
|
and isinstance(payload, dict)
|
||||||
|
and "messages" in payload
|
||||||
|
and len(payload["messages"]) > 0
|
||||||
|
):
|
||||||
|
if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
|
||||||
|
content = payload["messages"][0]["content"]
|
||||||
|
if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
|
||||||
|
payload["messages"][0]["content"][1]["image_url"]["url"] = (
|
||||||
|
f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
|
||||||
|
f"{image_base64[:10]}...{image_base64[-10:]}"
|
||||||
|
)
|
||||||
|
# if isinstance(content, str) and len(content) > 100:
|
||||||
|
# payload["messages"][0]["content"] = content[:100]
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class LLMRequest:
|
class LLMRequest:
|
||||||
# 定义需要转换的模型列表,作为类变量避免重复
|
# 定义需要转换的模型列表,作为类变量避免重复
|
||||||
MODELS_NEEDING_TRANSFORMATION = [
|
MODELS_NEEDING_TRANSFORMATION = [
|
||||||
|
|
@ -551,7 +573,7 @@ class LLMRequest:
|
||||||
f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {exception.status}, 错误: {exception.message}"
|
f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {exception.status}, 错误: {exception.message}"
|
||||||
)
|
)
|
||||||
# 安全地检查和记录请求详情
|
# 安全地检查和记录请求详情
|
||||||
handled_payload = await self._safely_record(request_content, payload)
|
handled_payload = await _safely_record(request_content, payload)
|
||||||
logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}")
|
logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}")
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"模型 {self.model_name} API请求失败: 状态码 {exception.status}, {exception.message}"
|
f"模型 {self.model_name} API请求失败: 状态码 {exception.status}, {exception.message}"
|
||||||
|
|
@ -565,31 +587,10 @@ class LLMRequest:
|
||||||
else:
|
else:
|
||||||
logger.critical(f"模型 {self.model_name} 请求失败: {str(exception)}")
|
logger.critical(f"模型 {self.model_name} 请求失败: {str(exception)}")
|
||||||
# 安全地检查和记录请求详情
|
# 安全地检查和记录请求详情
|
||||||
handled_payload = await self._safely_record(request_content, payload)
|
handled_payload = await _safely_record(request_content, payload)
|
||||||
logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}")
|
logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {handled_payload}")
|
||||||
raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(exception)}")
|
raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(exception)}")
|
||||||
|
|
||||||
async def _safely_record(self, request_content: Dict[str, Any], payload: Dict[str, Any]):
|
|
||||||
image_base64: str = request_content.get("image_base64")
|
|
||||||
image_format: str = request_content.get("image_format")
|
|
||||||
if (
|
|
||||||
image_base64
|
|
||||||
and payload
|
|
||||||
and isinstance(payload, dict)
|
|
||||||
and "messages" in payload
|
|
||||||
and len(payload["messages"]) > 0
|
|
||||||
):
|
|
||||||
if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
|
|
||||||
content = payload["messages"][0]["content"]
|
|
||||||
if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
|
|
||||||
payload["messages"][0]["content"][1]["image_url"]["url"] = (
|
|
||||||
f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
|
|
||||||
f"{image_base64[:10]}...{image_base64[-10:]}"
|
|
||||||
)
|
|
||||||
# if isinstance(content, str) and len(content) > 100:
|
|
||||||
# payload["messages"][0]["content"] = content[:100]
|
|
||||||
return payload
|
|
||||||
|
|
||||||
async def _transform_parameters(self, params: dict) -> dict:
|
async def _transform_parameters(self, params: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
根据模型名称转换参数:
|
根据模型名称转换参数:
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,6 @@ class PersonInfoManager:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_json_from_text(text: str) -> dict:
|
def _extract_json_from_text(text: str) -> dict:
|
||||||
"""从文本中提取JSON数据的高容错方法"""
|
"""从文本中提取JSON数据的高容错方法"""
|
||||||
parsed_json = None
|
|
||||||
try:
|
try:
|
||||||
# 尝试直接解析
|
# 尝试直接解析
|
||||||
parsed_json = json.loads(text)
|
parsed_json = json.loads(text)
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,6 @@ class InfoCatcher:
|
||||||
|
|
||||||
thinking_log_data = {
|
thinking_log_data = {
|
||||||
"chat_id": self.chat_id,
|
"chat_id": self.chat_id,
|
||||||
# "response_mode": self.response_mode, # 这个也删掉喵~
|
|
||||||
"trigger_text": self.trigger_response_text,
|
"trigger_text": self.trigger_response_text,
|
||||||
"response_text": self.response_text,
|
"response_text": self.response_text,
|
||||||
"trigger_info": {
|
"trigger_info": {
|
||||||
|
|
@ -202,6 +201,8 @@ class InfoCatcher:
|
||||||
"chat_history": self.message_list_to_dict(self.chat_history),
|
"chat_history": self.message_list_to_dict(self.chat_history),
|
||||||
"chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking),
|
"chat_history_in_thinking": self.message_list_to_dict(self.chat_history_in_thinking),
|
||||||
"chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response),
|
"chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response),
|
||||||
|
"heartflow_data": self.heartflow_data,
|
||||||
|
"reasoning_data": self.reasoning_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~
|
# 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~
|
||||||
|
|
@ -209,8 +210,6 @@ class InfoCatcher:
|
||||||
# thinking_log_data["mode_specific_data"] = self.heartflow_data
|
# thinking_log_data["mode_specific_data"] = self.heartflow_data
|
||||||
# elif self.response_mode == "reasoning":
|
# elif self.response_mode == "reasoning":
|
||||||
# thinking_log_data["mode_specific_data"] = self.reasoning_data
|
# thinking_log_data["mode_specific_data"] = self.reasoning_data
|
||||||
thinking_log_data["heartflow_data"] = self.heartflow_data
|
|
||||||
thinking_log_data["reasoning_data"] = self.reasoning_data
|
|
||||||
|
|
||||||
# 将数据插入到 thinking_log 集合中喵~
|
# 将数据插入到 thinking_log 集合中喵~
|
||||||
db.thinking_log.insert_one(thinking_log_data)
|
db.thinking_log.insert_one(thinking_log_data)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dateutil import tz
|
from dateutil import tz
|
||||||
|
|
||||||
|
|
@ -30,6 +29,7 @@ class ScheduleGenerator:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 使用离线LLM模型
|
# 使用离线LLM模型
|
||||||
|
self.enable_output = None
|
||||||
self.llm_scheduler_all = LLMRequest(
|
self.llm_scheduler_all = LLMRequest(
|
||||||
model=global_config.llm_reasoning,
|
model=global_config.llm_reasoning,
|
||||||
temperature=global_config.SCHEDULE_TEMPERATURE + 0.3,
|
temperature=global_config.SCHEDULE_TEMPERATURE + 0.3,
|
||||||
|
|
@ -161,7 +161,7 @@ class ScheduleGenerator:
|
||||||
async def generate_daily_schedule(
|
async def generate_daily_schedule(
|
||||||
self,
|
self,
|
||||||
target_date: datetime.datetime = None,
|
target_date: datetime.datetime = None,
|
||||||
) -> Dict[str, str]:
|
) -> dict[str, str]:
|
||||||
daytime_prompt = self.construct_daytime_prompt(target_date)
|
daytime_prompt = self.construct_daytime_prompt(target_date)
|
||||||
daytime_response, _ = await self.llm_scheduler_all.generate_response_async(daytime_prompt)
|
daytime_response, _ = await self.llm_scheduler_all.generate_response_async(daytime_prompt)
|
||||||
return daytime_response
|
return daytime_response
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ def num_new_messages_since(chat_id: str, timestamp_start: float = 0.0, timestamp
|
||||||
return 0 # 起始时间大于等于结束时间,没有新消息
|
return 0 # 起始时间大于等于结束时间,没有新消息
|
||||||
|
|
||||||
filter_query = {"chat_id": chat_id, "time": {"$gt": timestamp_start, "$lt": _timestamp_end}}
|
filter_query = {"chat_id": chat_id, "time": {"$gt": timestamp_start, "$lt": _timestamp_end}}
|
||||||
return count_messages(filter=filter_query)
|
return count_messages(message_filter=filter_query)
|
||||||
|
|
||||||
|
|
||||||
def num_new_messages_since_with_users(
|
def num_new_messages_since_with_users(
|
||||||
|
|
@ -137,7 +137,7 @@ def num_new_messages_since_with_users(
|
||||||
"time": {"$gt": timestamp_start, "$lt": timestamp_end},
|
"time": {"$gt": timestamp_start, "$lt": timestamp_end},
|
||||||
"user_id": {"$in": person_ids},
|
"user_id": {"$in": person_ids},
|
||||||
}
|
}
|
||||||
return count_messages(filter=filter_query)
|
return count_messages(message_filter=filter_query)
|
||||||
|
|
||||||
|
|
||||||
async def _build_readable_messages_internal(
|
async def _build_readable_messages_internal(
|
||||||
|
|
@ -227,7 +227,7 @@ async def _build_readable_messages_internal(
|
||||||
replace_content = "......(太长了)"
|
replace_content = "......(太长了)"
|
||||||
|
|
||||||
truncated_content = content
|
truncated_content = content
|
||||||
if limit > 0 and original_len > limit:
|
if 0 < limit < original_len:
|
||||||
truncated_content = f"{content[:limit]}{replace_content}"
|
truncated_content = f"{content[:limit]}{replace_content}"
|
||||||
|
|
||||||
message_details.append((timestamp, name, truncated_content))
|
message_details.append((timestamp, name, truncated_content))
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,23 @@ from .willing_manager import BaseWillingManager
|
||||||
|
|
||||||
|
|
||||||
class CustomWillingManager(BaseWillingManager):
|
class CustomWillingManager(BaseWillingManager):
|
||||||
|
async def async_task_starter(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def before_generate_reply_handle(self, message_id: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def after_generate_reply_handle(self, message_id: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def not_reply_handle(self, message_id: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_reply_probability(self, message_id: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def bombing_buffer_message_handle(self, message_id: str):
|
||||||
|
pass
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ class DynamicWillingManager(BaseWillingManager):
|
||||||
is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
|
is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
|
||||||
|
|
||||||
# 获取当前模式的持续时间
|
# 获取当前模式的持续时间
|
||||||
duration = 0
|
|
||||||
if is_high_mode:
|
if is_high_mode:
|
||||||
duration = self.chat_high_willing_duration.get(chat_id, 180) # 默认3分钟
|
duration = self.chat_high_willing_duration.get(chat_id, 180) # 默认3分钟
|
||||||
else:
|
else:
|
||||||
|
|
@ -154,8 +153,6 @@ class DynamicWillingManager(BaseWillingManager):
|
||||||
)
|
)
|
||||||
|
|
||||||
# 根据当前模式计算回复概率
|
# 根据当前模式计算回复概率
|
||||||
base_probability = 0.0
|
|
||||||
|
|
||||||
if in_conversation_context:
|
if in_conversation_context:
|
||||||
# 在对话上下文中,降低基础回复概率
|
# 在对话上下文中,降低基础回复概率
|
||||||
base_probability = 0.5 if is_high_mode else 0.25
|
base_probability = 0.5 if is_high_mode else 0.25
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,8 @@ class LlmcheckWillingManager(MxpWillingManager):
|
||||||
|
|
||||||
current_date = time.strftime("%Y-%m-%d", time.localtime())
|
current_date = time.strftime("%Y-%m-%d", time.localtime())
|
||||||
current_time = time.strftime("%H:%M:%S", time.localtime())
|
current_time = time.strftime("%H:%M:%S", time.localtime())
|
||||||
chat_talking_prompt = ""
|
chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True)
|
||||||
if chat_id:
|
if not chat_id:
|
||||||
chat_talking_prompt = get_recent_group_detailed_plain_text(chat_id, limit=length, combine=True)
|
|
||||||
else:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# if is_mentioned_bot:
|
# if is_mentioned_bot:
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,8 @@ mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
|
||||||
at_bot_inevitable_reply = false # @bot 必然回复
|
at_bot_inevitable_reply = false # @bot 必然回复
|
||||||
|
|
||||||
[focus_chat] #专注聊天
|
[focus_chat] #专注聊天
|
||||||
reply_trigger_threshold = 3.5 # 专注聊天触发阈值,越低越容易进入专注聊天
|
reply_trigger_threshold = 3.6 # 专注聊天触发阈值,越低越容易进入专注聊天
|
||||||
default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
|
default_decay_rate_per_second = 0.95 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
|
||||||
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
|
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
|
||||||
|
|
||||||
# 以下选项暂时无效
|
# 以下选项暂时无效
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue