Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
@@ -124,6 +124,10 @@ async def db_query(
|
|||||||
raise ValueError("query_type must be 'get', 'create', 'update', 'delete' or 'count'")
|
raise ValueError("query_type must be 'get', 'create', 'update', 'delete' or 'count'")
|
||||||
|
|
||||||
async with get_db_session() as session:
|
async with get_db_session() as session:
|
||||||
|
if not session:
|
||||||
|
logger.error("[SQLAlchemy] 无法获取数据库会话")
|
||||||
|
return None if single_result else []
|
||||||
|
|
||||||
if query_type == "get":
|
if query_type == "get":
|
||||||
query = select(model_class)
|
query = select(model_class)
|
||||||
|
|
||||||
@@ -221,7 +225,7 @@ async def db_query(
|
|||||||
# 删除记录
|
# 删除记录
|
||||||
affected_rows = 0
|
affected_rows = 0
|
||||||
for record in records_to_delete:
|
for record in records_to_delete:
|
||||||
session.delete(record)
|
await session.delete(record)
|
||||||
affected_rows += 1
|
affected_rows += 1
|
||||||
|
|
||||||
return affected_rows
|
return affected_rows
|
||||||
@@ -274,6 +278,9 @@ async def db_save(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with get_db_session() as session:
|
async with get_db_session() as session:
|
||||||
|
if not session:
|
||||||
|
logger.error("[SQLAlchemy] 无法获取数据库会话")
|
||||||
|
return None
|
||||||
# 如果提供了key_field和key_value,尝试更新现有记录
|
# 如果提供了key_field和key_value,尝试更新现有记录
|
||||||
if key_field and key_value is not None:
|
if key_field and key_value is not None:
|
||||||
if hasattr(model_class, key_field):
|
if hasattr(model_class, key_field):
|
||||||
|
|||||||
@@ -382,21 +382,19 @@ class BaseAction(ABC):
|
|||||||
# 构造命令数据
|
# 构造命令数据
|
||||||
command_data = {"name": command_name, "args": args or {}}
|
command_data = {"name": command_name, "args": args or {}}
|
||||||
|
|
||||||
response = await send_api.adapter_command_to_stream(
|
success = await send_api.command_to_stream(
|
||||||
action=command_name,
|
command=command_data,
|
||||||
params=args or {},
|
|
||||||
stream_id=self.chat_id,
|
stream_id=self.chat_id,
|
||||||
platform=self.platform
|
storage_message=storage_message,
|
||||||
|
display_message=display_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 根据响应判断成功与否
|
if success:
|
||||||
if response and response.get("status") == "ok":
|
logger.info(f"{self.log_prefix} 成功发送命令: {command_name}")
|
||||||
logger.info(f"{self.log_prefix} 成功执行适配器命令: {command_name}, 响应: {response.get('data')}")
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
error_message = response.get('message', '未知错误')
|
logger.error(f"{self.log_prefix} 发送命令失败: {command_name}")
|
||||||
logger.error(f"{self.log_prefix} 执行适配器命令失败: {command_name}, 错误: {error_message}")
|
|
||||||
return False
|
return success
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"{self.log_prefix} 发送命令时出错: {e}")
|
logger.error(f"{self.log_prefix} 发送命令时出错: {e}")
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ class QZoneService:
|
|||||||
self.cookie_service = cookie_service
|
self.cookie_service = cookie_service
|
||||||
# 如果没有提供 reply_tracker 实例,则创建一个新的
|
# 如果没有提供 reply_tracker 实例,则创建一个新的
|
||||||
self.reply_tracker = reply_tracker if reply_tracker is not None else ReplyTrackerService()
|
self.reply_tracker = reply_tracker if reply_tracker is not None else ReplyTrackerService()
|
||||||
|
# 用于防止并发回复/评论的内存锁
|
||||||
|
self.processing_comments = set()
|
||||||
|
|
||||||
# --- Public Methods (High-Level Business Logic) ---
|
# --- Public Methods (High-Level Business Logic) ---
|
||||||
|
|
||||||
@@ -159,7 +161,7 @@ class QZoneService:
|
|||||||
if self.get_config("monitor.enable_auto_reply", False):
|
if self.get_config("monitor.enable_auto_reply", False):
|
||||||
try:
|
try:
|
||||||
# 传入新参数,表明正在检查自己的说说
|
# 传入新参数,表明正在检查自己的说说
|
||||||
own_feeds = await api_client["list_feeds"](qq_account, 5, is_monitoring_own_feeds=True)
|
own_feeds = await api_client["list_feeds"](qq_account, 5)
|
||||||
if own_feeds:
|
if own_feeds:
|
||||||
logger.info(f"获取到自己 {len(own_feeds)} 条说说,检查评论...")
|
logger.info(f"获取到自己 {len(own_feeds)} 条说说,检查评论...")
|
||||||
for feed in own_feeds:
|
for feed in own_feeds:
|
||||||
@@ -263,37 +265,37 @@ class QZoneService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 直接检查评论是否已回复,不做验证清理
|
# 直接检查评论是否已回复,不做验证清理
|
||||||
comments_to_reply = []
|
comments_to_process = []
|
||||||
for comment in user_comments:
|
for comment in user_comments:
|
||||||
comment_tid = comment.get("comment_tid")
|
comment_tid = comment.get("comment_tid")
|
||||||
if not comment_tid:
|
if not comment_tid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查是否已经在持久化记录中标记为已回复
|
comment_key = f"{fid}_{comment_tid}"
|
||||||
if not self.reply_tracker.has_replied(fid, comment_tid):
|
# 检查持久化记录和内存锁
|
||||||
# 记录日志以便追踪
|
if not self.reply_tracker.has_replied(fid, comment_tid) and comment_key not in self.processing_comments:
|
||||||
logger.debug(
|
logger.debug(f"锁定待回复评论: {comment_key}")
|
||||||
f"发现新评论需要回复 - 说说ID: {fid}, 评论ID: {comment_tid}, "
|
self.processing_comments.add(comment_key)
|
||||||
f"评论人: {comment.get('nickname', '')}, 内容: {comment.get('content', '')}"
|
comments_to_process.append(comment)
|
||||||
)
|
|
||||||
comments_to_reply.append(comment)
|
|
||||||
|
|
||||||
if not comments_to_reply:
|
if not comments_to_process:
|
||||||
logger.debug(f"说说 {fid} 下的所有评论都已回复过或无需回复")
|
logger.debug(f"说说 {fid} 下的所有评论都已回复过或正在处理中")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...")
|
logger.info(f"发现自己说说下的 {len(comments_to_process)} 条新评论,准备回复...")
|
||||||
for comment in comments_to_reply:
|
for comment in comments_to_process:
|
||||||
comment_tid = comment.get("comment_tid")
|
comment_tid = comment.get("comment_tid")
|
||||||
|
comment_key = f"{fid}_{comment_tid}"
|
||||||
nickname = comment.get("nickname", "")
|
nickname = comment.get("nickname", "")
|
||||||
comment_content = comment.get("content", "")
|
comment_content = comment.get("content", "")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reply_content = await self.content_service.generate_comment_reply(content, comment_content, nickname)
|
reply_content = await self.content_service.generate_comment_reply(
|
||||||
|
content, comment_content, nickname
|
||||||
|
)
|
||||||
if reply_content:
|
if reply_content:
|
||||||
success = await api_client["reply"](fid, qq_account, nickname, reply_content, comment_tid)
|
success = await api_client["reply"](fid, qq_account, nickname, reply_content, comment_tid)
|
||||||
if success:
|
if success:
|
||||||
# 标记为已回复
|
|
||||||
self.reply_tracker.mark_as_replied(fid, comment_tid)
|
self.reply_tracker.mark_as_replied(fid, comment_tid)
|
||||||
logger.info(f"成功回复'{nickname}'的评论: '{reply_content}'")
|
logger.info(f"成功回复'{nickname}'的评论: '{reply_content}'")
|
||||||
else:
|
else:
|
||||||
@@ -303,6 +305,11 @@ class QZoneService:
|
|||||||
logger.warning(f"生成回复内容失败,跳过回复'{nickname}'的评论")
|
logger.warning(f"生成回复内容失败,跳过回复'{nickname}'的评论")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"回复'{nickname}'的评论时发生异常: {e}", exc_info=True)
|
logger.error(f"回复'{nickname}'的评论时发生异常: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
# 无论成功与否,都解除锁定
|
||||||
|
logger.debug(f"解锁评论: {comment_key}")
|
||||||
|
if comment_key in self.processing_comments:
|
||||||
|
self.processing_comments.remove(comment_key)
|
||||||
|
|
||||||
async def _validate_and_cleanup_reply_records(self, fid: str, my_replies: List[Dict]):
|
async def _validate_and_cleanup_reply_records(self, fid: str, my_replies: List[Dict]):
|
||||||
"""验证并清理已删除的回复记录"""
|
"""验证并清理已删除的回复记录"""
|
||||||
@@ -335,11 +342,34 @@ class QZoneService:
|
|||||||
rt_con = feed.get("rt_con", "")
|
rt_con = feed.get("rt_con", "")
|
||||||
images = feed.get("images", [])
|
images = feed.get("images", [])
|
||||||
|
|
||||||
if random.random() <= self.get_config("read.comment_possibility", 0.3):
|
# --- 处理评论 ---
|
||||||
comment_text = await self.content_service.generate_comment(content, target_name, rt_con, images)
|
comment_key = f"{fid}_main_comment"
|
||||||
if comment_text:
|
should_comment = random.random() <= self.get_config("read.comment_possibility", 0.3)
|
||||||
await api_client["comment"](target_qq, fid, comment_text)
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
should_comment
|
||||||
|
and not self.reply_tracker.has_replied(fid, "main_comment")
|
||||||
|
and comment_key not in self.processing_comments
|
||||||
|
):
|
||||||
|
logger.debug(f"锁定待评论说说: {comment_key}")
|
||||||
|
self.processing_comments.add(comment_key)
|
||||||
|
try:
|
||||||
|
comment_text = await self.content_service.generate_comment(content, target_name, rt_con, images)
|
||||||
|
if comment_text:
|
||||||
|
success = await api_client["comment"](target_qq, fid, comment_text)
|
||||||
|
if success:
|
||||||
|
self.reply_tracker.mark_as_replied(fid, "main_comment")
|
||||||
|
logger.info(f"成功评论'{target_name}'的说说: '{comment_text}'")
|
||||||
|
else:
|
||||||
|
logger.error(f"评论'{target_name}'的说说失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"评论'{target_name}'的说说时发生异常: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
logger.debug(f"解锁说说: {comment_key}")
|
||||||
|
if comment_key in self.processing_comments:
|
||||||
|
self.processing_comments.remove(comment_key)
|
||||||
|
|
||||||
|
# --- 处理点赞 (逻辑不变) ---
|
||||||
if random.random() <= self.get_config("read.like_possibility", 1.0):
|
if random.random() <= self.get_config("read.like_possibility", 1.0):
|
||||||
await api_client["like"](target_qq, fid)
|
await api_client["like"](target_qq, fid)
|
||||||
|
|
||||||
@@ -714,9 +744,10 @@ class QZoneService:
|
|||||||
logger.error(f"上传图片 {index + 1} 异常: {e}", exc_info=True)
|
logger.error(f"上传图片 {index + 1} 异常: {e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _list_feeds(t_qq: str, num: int, is_monitoring_own_feeds: bool = False) -> List[Dict]:
|
async def _list_feeds(t_qq: str, num: int) -> List[Dict]:
|
||||||
"""获取指定用户说说列表"""
|
"""获取指定用户说说列表 (统一接口)"""
|
||||||
try:
|
try:
|
||||||
|
# 统一使用 format=json 获取完整评论
|
||||||
params = {
|
params = {
|
||||||
"g_tk": gtk,
|
"g_tk": gtk,
|
||||||
"uin": t_qq,
|
"uin": t_qq,
|
||||||
@@ -724,59 +755,74 @@ class QZoneService:
|
|||||||
"sort": 0,
|
"sort": 0,
|
||||||
"pos": 0,
|
"pos": 0,
|
||||||
"num": num,
|
"num": num,
|
||||||
"replynum": 100,
|
"replynum": 999, # 尽量获取更多
|
||||||
"callback": "_preloadCallback",
|
|
||||||
"code_version": 1,
|
"code_version": 1,
|
||||||
"format": "jsonp",
|
"format": "json", # 关键:使用JSON格式
|
||||||
"need_comment": 1,
|
"need_comment": 1,
|
||||||
}
|
}
|
||||||
res_text = await _request("GET", self.LIST_URL, params=params)
|
res_text = await _request("GET", self.LIST_URL, params=params)
|
||||||
json_str = res_text[len("_preloadCallback(") : -2]
|
json_data = orjson.loads(res_text)
|
||||||
json_data = orjson.loads(json_str)
|
|
||||||
|
|
||||||
if json_data.get("code") != 0:
|
if json_data.get("code") != 0:
|
||||||
|
logger.warning(f"获取说说列表API返回错误: code={json_data.get('code')}, message={json_data.get('message')}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
feeds_list = []
|
feeds_list = []
|
||||||
my_name = json_data.get("logininfo", {}).get("name", "")
|
my_name = json_data.get("logininfo", {}).get("name", "")
|
||||||
for msg in json_data.get("msglist", []):
|
|
||||||
# 只有在处理好友说说时,才检查是否已评论并跳过
|
|
||||||
commentlist = msg.get("commentlist")
|
|
||||||
|
|
||||||
# 只有在处理好友说说时,才检查是否已评论并跳过
|
for msg in json_data.get("msglist", []):
|
||||||
if not is_monitoring_own_feeds:
|
# 当读取的是好友动态时,检查是否已评论过,如果是则跳过
|
||||||
|
is_friend_feed = str(t_qq) != str(uin)
|
||||||
|
if is_friend_feed:
|
||||||
|
commentlist_for_check = msg.get("commentlist")
|
||||||
is_commented = False
|
is_commented = False
|
||||||
if isinstance(commentlist, list):
|
if isinstance(commentlist_for_check, list):
|
||||||
is_commented = any(
|
is_commented = any(
|
||||||
c.get("name") == my_name for c in commentlist if isinstance(c, dict)
|
c.get("name") == my_name for c in commentlist_for_check if isinstance(c, dict)
|
||||||
)
|
)
|
||||||
if is_commented:
|
if is_commented:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# --- 安全地处理图片列表 ---
|
# --- 安全地处理图片列表 ---
|
||||||
images = []
|
images = []
|
||||||
pictotal = msg.get("pictotal")
|
if "pic" in msg and isinstance(msg["pic"], list):
|
||||||
if isinstance(pictotal, list):
|
images = [pic.get("url1", "") for pic in msg["pic"] if pic.get("url1")]
|
||||||
images = [
|
elif "pictotal" in msg and isinstance(msg["pictotal"], list):
|
||||||
pic["url1"] for pic in pictotal if isinstance(pic, dict) and "url1" in pic
|
images = [pic.get("url1", "") for pic in msg["pictotal"] if pic.get("url1")]
|
||||||
]
|
|
||||||
|
|
||||||
# --- 安全地处理评论列表 ---
|
# --- 解析完整评论列表 (包括二级评论) ---
|
||||||
comments = []
|
comments = []
|
||||||
|
commentlist = msg.get("commentlist")
|
||||||
if isinstance(commentlist, list):
|
if isinstance(commentlist, list):
|
||||||
for c in commentlist:
|
for c in commentlist:
|
||||||
# 确保评论条目也是字典
|
if not isinstance(c, dict):
|
||||||
if isinstance(c, dict):
|
continue
|
||||||
comments.append(
|
|
||||||
{
|
# 添加主评论
|
||||||
"qq_account": c.get("uin"),
|
comments.append(
|
||||||
"nickname": c.get("name"),
|
{
|
||||||
"content": c.get("content"),
|
"qq_account": c.get("uin"),
|
||||||
"comment_tid": c.get("tid"),
|
"nickname": c.get("name"),
|
||||||
"parent_tid": c.get("parent_tid"),
|
"content": c.get("content"),
|
||||||
}
|
"comment_tid": c.get("tid"),
|
||||||
)
|
"parent_tid": None, # 主评论没有父ID
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 检查并添加二级评论 (回复)
|
||||||
|
if "list_3" in c and isinstance(c["list_3"], list):
|
||||||
|
for reply in c["list_3"]:
|
||||||
|
if not isinstance(reply, dict):
|
||||||
|
continue
|
||||||
|
comments.append(
|
||||||
|
{
|
||||||
|
"qq_account": reply.get("uin"),
|
||||||
|
"nickname": reply.get("name"),
|
||||||
|
"content": reply.get("content"),
|
||||||
|
"comment_tid": reply.get("tid"),
|
||||||
|
"parent_tid": c.get("tid"), # 父ID是主评论的ID
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
feeds_list.append(
|
feeds_list.append(
|
||||||
{
|
{
|
||||||
"tid": msg.get("tid", ""),
|
"tid": msg.get("tid", ""),
|
||||||
@@ -791,6 +837,8 @@ class QZoneService:
|
|||||||
"comments": comments,
|
"comments": comments,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"成功获取到 {len(feeds_list)} 条说说 from {t_qq} (使用统一JSON接口)")
|
||||||
return feeds_list
|
return feeds_list
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取说说列表失败: {e}", exc_info=True)
|
logger.error(f"获取说说列表失败: {e}", exc_info=True)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from src.manager.async_task_manager import async_task_manager, AsyncTask
|
|||||||
from src.plugin_system import EventType, BaseEventHandler
|
from src.plugin_system import EventType, BaseEventHandler
|
||||||
from src.plugin_system.apis import chat_api, person_api
|
from src.plugin_system.apis import chat_api, person_api
|
||||||
from src.plugin_system.base.base_event import HandlerResult
|
from src.plugin_system.base.base_event import HandlerResult
|
||||||
|
from .proactive_thinker_executor import ProactiveThinkerExecutor
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ class ColdStartTask(AsyncTask):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(task_name="ColdStartTask")
|
super().__init__(task_name="ColdStartTask")
|
||||||
self.chat_manager = get_chat_manager()
|
self.chat_manager = get_chat_manager()
|
||||||
|
self.executor = ProactiveThinkerExecutor()
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""任务主循环,周期性地检查是否有需要“破冰”的新用户。"""
|
"""任务主循环,周期性地检查是否有需要“破冰”的新用户。"""
|
||||||
@@ -70,9 +72,9 @@ class ColdStartTask(AsyncTask):
|
|||||||
|
|
||||||
# 【关键步骤】主动创建聊天流。
|
# 【关键步骤】主动创建聊天流。
|
||||||
# 创建后,该用户就进入了机器人的“好友列表”,后续将由 ProactiveThinkingTask 接管
|
# 创建后,该用户就进入了机器人的“好友列表”,后续将由 ProactiveThinkingTask 接管
|
||||||
await self.chat_manager.get_or_create_stream(platform, user_info)
|
stream = await self.chat_manager.get_or_create_stream(platform, user_info)
|
||||||
|
|
||||||
# TODO: 在这里调用LLM,生成一句自然的、符合人设的“破冰”问候语,并发送给用户。
|
await self.executor.execute(stream_id=stream.stream_id, start_mode="cold_start")
|
||||||
logger.info(f"【冷启动】已为新用户 {chat_id} (昵称: {user_nickname}) 创建聊天流并发送问候。")
|
logger.info(f"【冷启动】已为新用户 {chat_id} (昵称: {user_nickname}) 创建聊天流并发送问候。")
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -100,6 +102,7 @@ class ProactiveThinkingTask(AsyncTask):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(task_name="ProactiveThinkingTask")
|
super().__init__(task_name="ProactiveThinkingTask")
|
||||||
self.chat_manager = get_chat_manager()
|
self.chat_manager = get_chat_manager()
|
||||||
|
self.executor = ProactiveThinkerExecutor()
|
||||||
|
|
||||||
def _get_next_interval(self) -> float:
|
def _get_next_interval(self) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -174,7 +177,7 @@ class ProactiveThinkingTask(AsyncTask):
|
|||||||
if time_since_last_active > next_interval:
|
if time_since_last_active > next_interval:
|
||||||
logger.info(f"【日常唤醒】聊天流 {stream.stream_id} 已冷却 {time_since_last_active:.2f} 秒,触发主动对话。")
|
logger.info(f"【日常唤醒】聊天流 {stream.stream_id} 已冷却 {time_since_last_active:.2f} 秒,触发主动对话。")
|
||||||
|
|
||||||
# TODO: 在这里调用LLM,生成一句自然的、符合上下文的问候语,并发送。
|
await self.executor.execute(stream_id=stream.stream_id, start_mode="wake_up")
|
||||||
|
|
||||||
# 【关键步骤】在触发后,立刻更新活跃时间并保存。
|
# 【关键步骤】在触发后,立刻更新活跃时间并保存。
|
||||||
# 这可以防止在同一个检查周期内,对同一个目标因为意外的延迟而发送多条消息。
|
# 这可以防止在同一个检查周期内,对同一个目标因为意外的延迟而发送多条消息。
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import orjson
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.common.logger import get_logger
|
||||||
|
from src.plugin_system.apis import chat_api, person_api, schedule_api, send_api, llm_api, message_api, generator_api, database_api
|
||||||
|
from src.config.config import global_config, model_config
|
||||||
|
from src.person_info.person_info import get_person_info_manager
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProactiveThinkerExecutor:
|
||||||
|
"""
|
||||||
|
主动思考执行器 V2
|
||||||
|
- 统一执行入口
|
||||||
|
- 引入决策模块,判断是否及如何发起对话
|
||||||
|
- 结合人设、日程、关系信息生成更具情境的对话
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# 可以在此初始化所需模块,例如LLM请求器等
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def execute(self, stream_id: str, start_mode: str = "wake_up"):
|
||||||
|
"""
|
||||||
|
统一执行入口
|
||||||
|
Args:
|
||||||
|
stream_id: 聊天流ID
|
||||||
|
start_mode: 启动模式, 'cold_start' 或 'wake_up'
|
||||||
|
"""
|
||||||
|
logger.info(f"开始为聊天流 {stream_id} 执行主动思考,模式: {start_mode}")
|
||||||
|
|
||||||
|
# 1. 信息收集
|
||||||
|
context = await self._gather_context(stream_id)
|
||||||
|
if not context:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 决策阶段
|
||||||
|
decision_result = await self._make_decision(context, start_mode)
|
||||||
|
|
||||||
|
|
||||||
|
if not decision_result or not decision_result.get("should_reply"):
|
||||||
|
reason = decision_result.get("reason", "未提供") if decision_result else "决策过程返回None"
|
||||||
|
logger.info(f"决策结果为:不回复。原因: {reason}")
|
||||||
|
await database_api.store_action_info(
|
||||||
|
chat_stream=self._get_stream_from_id(stream_id),
|
||||||
|
action_name="proactive_decision",
|
||||||
|
action_prompt_display=f"主动思考决定不回复,原因: {reason}",
|
||||||
|
action_done = True,
|
||||||
|
action_data=decision_result
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 规划与执行阶段
|
||||||
|
topic = decision_result.get("topic", "打个招呼")
|
||||||
|
reason = decision_result.get("reason", "无")
|
||||||
|
await database_api.store_action_info(
|
||||||
|
chat_stream=self._get_stream_from_id(stream_id),
|
||||||
|
action_name="proactive_decision",
|
||||||
|
action_prompt_display=f"主动思考决定回复,原因: {reason},话题:{topic}",
|
||||||
|
action_done = True,
|
||||||
|
action_data=decision_result
|
||||||
|
)
|
||||||
|
logger.info(f"决策结果为:回复。话题: {topic}")
|
||||||
|
|
||||||
|
plan_prompt = self._build_plan_prompt(context, start_mode, topic, reason)
|
||||||
|
|
||||||
|
is_success, response, _, _ = await llm_api.generate_with_model(prompt=plan_prompt, model_config=model_config.model_task_config.utils)
|
||||||
|
|
||||||
|
if is_success and response:
|
||||||
|
stream = self._get_stream_from_id(stream_id)
|
||||||
|
if stream:
|
||||||
|
# 使用消息分割器处理并发送消息
|
||||||
|
reply_set = generator_api.process_human_text(response, enable_splitter=True, enable_chinese_typo=False)
|
||||||
|
for reply_type, content in reply_set:
|
||||||
|
if reply_type == "text":
|
||||||
|
await send_api.text_to_stream(stream_id=stream.stream_id, text=content)
|
||||||
|
else:
|
||||||
|
logger.warning(f"无法发送消息,因为找不到 stream_id 为 {stream_id} 的聊天流")
|
||||||
|
|
||||||
|
def _get_stream_from_id(self, stream_id: str):
|
||||||
|
"""根据stream_id解析并获取stream对象"""
|
||||||
|
try:
|
||||||
|
platform, chat_id, stream_type = stream_id.split(":")
|
||||||
|
if stream_type == "private":
|
||||||
|
return chat_api.ChatManager.get_private_stream_by_user_id(platform, chat_id)
|
||||||
|
elif stream_type == "group":
|
||||||
|
return chat_api.ChatManager.get_group_stream_by_group_id(platform, chat_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"解析 stream_id ({stream_id}) 或获取 stream 失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _gather_context(self, stream_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
收集构建提示词所需的所有上下文信息
|
||||||
|
"""
|
||||||
|
stream = self._get_stream_from_id(stream_id)
|
||||||
|
if not stream:
|
||||||
|
logger.warning(f"无法找到 stream_id 为 {stream_id} 的聊天流")
|
||||||
|
return None
|
||||||
|
|
||||||
|
user_info = stream.user_info
|
||||||
|
if not user_info or not user_info.platform or not user_info.user_id:
|
||||||
|
logger.warning(f"Stream {stream_id} 的 user_info 不完整")
|
||||||
|
return None
|
||||||
|
|
||||||
|
person_id = person_api.get_person_id(user_info.platform, int(user_info.user_id))
|
||||||
|
person_info_manager = get_person_info_manager()
|
||||||
|
|
||||||
|
# 获取日程
|
||||||
|
schedules = await schedule_api.ScheduleAPI.get_today_schedule()
|
||||||
|
schedule_context = "\n".join([f"- {s['title']} ({s['start_time']}-{s['end_time']})" for s in schedules]) if schedules else "今天没有日程安排。"
|
||||||
|
|
||||||
|
# 获取关系信息
|
||||||
|
short_impression = await person_info_manager.get_value(person_id, "short_impression") or "无"
|
||||||
|
impression = await person_info_manager.get_value(person_id, "impression") or "无"
|
||||||
|
attitude = await person_info_manager.get_value(person_id, "attitude") or 50
|
||||||
|
|
||||||
|
# 获取最近聊天记录
|
||||||
|
recent_messages = await message_api.get_recent_messages(stream_id, limit=10)
|
||||||
|
recent_chat_history = await message_api.build_readable_messages_to_str(recent_messages) if recent_messages else "无"
|
||||||
|
|
||||||
|
# 获取最近的动作历史
|
||||||
|
action_history = await database_api.db_query(
|
||||||
|
database_api.MODEL_MAPPING["ActionRecords"],
|
||||||
|
filters={"chat_id": stream_id, "action_name": "proactive_decision"},
|
||||||
|
limit=3,
|
||||||
|
order_by=["-time"]
|
||||||
|
)
|
||||||
|
action_history_context = "无"
|
||||||
|
if isinstance(action_history, list):
|
||||||
|
action_history_context = "\n".join([f"- {a['action_data']}" for a in action_history if isinstance(a, dict)]) or "无"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"person_id": person_id,
|
||||||
|
"user_info": user_info,
|
||||||
|
"schedule_context": schedule_context,
|
||||||
|
"recent_chat_history": recent_chat_history,
|
||||||
|
"action_history_context": action_history_context,
|
||||||
|
"relationship": {
|
||||||
|
"short_impression": short_impression,
|
||||||
|
"impression": impression,
|
||||||
|
"attitude": attitude
|
||||||
|
},
|
||||||
|
"persona": {
|
||||||
|
"core": global_config.personality.personality_core,
|
||||||
|
"side": global_config.personality.personality_side,
|
||||||
|
"identity": global_config.personality.identity,
|
||||||
|
},
|
||||||
|
"current_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _make_decision(self, context: Dict[str, Any], start_mode: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
决策模块:判断是否应该主动发起对话,以及聊什么话题
|
||||||
|
"""
|
||||||
|
persona = context['persona']
|
||||||
|
user_info = context['user_info']
|
||||||
|
relationship = context['relationship']
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
# 角色
|
||||||
|
你的名字是{global_config.bot.nickname},你的人设如下:
|
||||||
|
- 核心人设: {persona['core']}
|
||||||
|
- 侧面人设: {persona['side']}
|
||||||
|
- 身份: {persona['identity']}
|
||||||
|
|
||||||
|
# 任务
|
||||||
|
现在是 {context['current_time']},你需要根据当前的情境,决定是否要主动向用户 '{user_info.user_nickname}' 发起对话。
|
||||||
|
|
||||||
|
# 情境分析
|
||||||
|
1. **启动模式**: {start_mode} ({'初次见面/很久未见' if start_mode == 'cold_start' else '日常唤醒'})
|
||||||
|
2. **你的日程**:
|
||||||
|
{context['schedule_context']}
|
||||||
|
3. **你和Ta的关系**:
|
||||||
|
- 简短印象: {relationship['short_impression']}
|
||||||
|
- 详细印象: {relationship['impression']}
|
||||||
|
- 好感度: {relationship['attitude']}/100
|
||||||
|
4. **最近的聊天摘要**:
|
||||||
|
{context['recent_chat_history']}
|
||||||
|
|
||||||
|
# 决策指令
|
||||||
|
请综合以上所有信息,做出决策。你的决策需要以JSON格式输出,包含以下字段:
|
||||||
|
- `should_reply`: bool, 是否应该发起对话。
|
||||||
|
- `topic`: str, 如果 `should_reply` 为 true,你打算聊什么话题?(例如:问候一下今天的日程、关心一下昨天的某件事、分享一个你自己的趣事等)
|
||||||
|
- `reason`: str, 做出此决策的简要理由。
|
||||||
|
|
||||||
|
---
|
||||||
|
示例1 (应该回复):
|
||||||
|
{{
|
||||||
|
"should_reply": true,
|
||||||
|
"topic": "提醒Ta今天下午有'项目会议'的日程",
|
||||||
|
"reason": "现在是上午,Ta下午有个重要会议,我觉得应该主动提醒一下,这会显得我很贴心。"
|
||||||
|
}}
|
||||||
|
|
||||||
|
示例2 (不应回复):
|
||||||
|
{{
|
||||||
|
"should_reply": false,
|
||||||
|
"topic": null,
|
||||||
|
"reason": "虽然我们的关系不错,但现在是深夜,而且Ta今天的日程都已经完成了,我没有合适的理由去打扰Ta。"
|
||||||
|
}}
|
||||||
|
---
|
||||||
|
|
||||||
|
请输出你的决策:
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_success, response, _, _ = await llm_api.generate_with_model(prompt=prompt, model_config=model_config.model_task_config.utils)
|
||||||
|
|
||||||
|
if not is_success:
|
||||||
|
return {"should_reply": False, "reason": "决策模型生成失败"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 假设LLM返回JSON格式的决策结果
|
||||||
|
decision = orjson.loads(response)
|
||||||
|
return decision
|
||||||
|
except orjson.JSONDecodeError:
|
||||||
|
logger.error(f"决策LLM返回的JSON格式无效: {response}")
|
||||||
|
return {"should_reply": False, "reason": "决策模型返回格式错误"}
|
||||||
|
|
||||||
|
def _build_plan_prompt(self, context: Dict[str, Any], start_mode: str, topic: str, reason: str) -> str:
|
||||||
|
"""
|
||||||
|
根据启动模式和决策话题,构建最终的规划提示词
|
||||||
|
"""
|
||||||
|
persona = context['persona']
|
||||||
|
user_info = context['user_info']
|
||||||
|
relationship = context['relationship']
|
||||||
|
|
||||||
|
if start_mode == "cold_start":
|
||||||
|
prompt = f"""
|
||||||
|
# 角色
|
||||||
|
你的名字是{global_config.bot.nickname},你的人设如下:
|
||||||
|
- 核心人设: {persona['core']}
|
||||||
|
- 侧面人设: {persona['side']}
|
||||||
|
- 身份: {persona['identity']}
|
||||||
|
|
||||||
|
# 任务
|
||||||
|
你需要主动向一个新朋友 '{user_info.user_nickname}' 发起对话。这是你们的第一次交流,或者很久没聊了。
|
||||||
|
|
||||||
|
# 决策上下文
|
||||||
|
- **决策理由**: {reason}
|
||||||
|
- **你和Ta的关系**:
|
||||||
|
- 简短印象: {relationship['short_impression']}
|
||||||
|
- 详细印象: {relationship['impression']}
|
||||||
|
- 好感度: {relationship['attitude']}/100
|
||||||
|
|
||||||
|
# 对话指引
|
||||||
|
- 你的目标是“破冰”,让对话自然地开始。
|
||||||
|
- 你应该围绕这个话题展开: {topic}
|
||||||
|
- 你的语气应该符合你的人设,友好且真诚。
|
||||||
|
- 直接输出你要说的第一句话,不要包含任何额外的前缀或解释。
|
||||||
|
"""
|
||||||
|
else: # wake_up
|
||||||
|
prompt = f"""
|
||||||
|
# 角色
|
||||||
|
你的名字是{global_config.bot.nickname},你的人设如下:
|
||||||
|
- 核心人设: {persona['core']}
|
||||||
|
- 侧面人设: {persona['side']}
|
||||||
|
- 身份: {persona['identity']}
|
||||||
|
|
||||||
|
# 任务
|
||||||
|
现在是 {context['current_time']},你需要主动向你的朋友 '{user_info.user_nickname}' 发起对话。
|
||||||
|
|
||||||
|
# 决策上下文
|
||||||
|
- **决策理由**: {reason}
|
||||||
|
|
||||||
|
# 情境分析
|
||||||
|
1. **你的日程**:
|
||||||
|
{context['schedule_context']}
|
||||||
|
2. **你和Ta的关系**:
|
||||||
|
- 详细印象: {relationship['impression']}
|
||||||
|
- 好感度: {relationship['attitude']}/100
|
||||||
|
3. **最近的聊天摘要**:
|
||||||
|
{context['recent_chat_history']}
|
||||||
|
4. **你最近的相关动作**:
|
||||||
|
{context['action_history_context']}
|
||||||
|
|
||||||
|
# 对话指引
|
||||||
|
- 你决定和Ta聊聊关于“{topic}”的话题。
|
||||||
|
- 请结合以上所有情境信息,自然地开启对话。
|
||||||
|
- 你的语气应该符合你的人设以及你对Ta的好感度。
|
||||||
|
- 直接输出你要说的第一句话,不要包含任何额外的前缀或解释。
|
||||||
|
"""
|
||||||
|
return prompt
|
||||||
@@ -319,7 +319,7 @@ class SetEmojiLikeAction(BaseAction):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
success = await self.send_command(
|
success = await self.send_command(
|
||||||
command_name="set_msg_emoji_like",
|
command_name="set_emoji_like",
|
||||||
args={"message_id": message_id, "emoji_id": emoji_id, "set": set_like},
|
args={"message_id": message_id, "emoji_id": emoji_id, "set": set_like},
|
||||||
storage_message=False,
|
storage_message=False,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user